Skip to content

Commit

Permalink
[Feature] Product Search (#16)
Browse files Browse the repository at this point in the history
* Inject Hibernate5Module to support JPA lazy objects in JSON

* Json include transient field use @JsonProperty

* Add tests for Products API

* Update version to 0.2.3

* Move Jackson config location

* Add ProductSearchDto

* Remove @NotNull

* Add valid flag

* Rename

* Draft

* change indexBase

* update hibernate search config

* Use moreLikeThis() to implement searchByExample()

* Update tests

* Remove @EnableWebMvc

* Fix CORS

* Add addCorsMappings() in WebMvcConfig

* Update docs
  • Loading branch information
YanzheL authored May 10, 2019
1 parent a80009d commit cfb44b6
Show file tree
Hide file tree
Showing 23 changed files with 328 additions and 61 deletions.
14 changes: 13 additions & 1 deletion api-gate/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@
<groupId>com.linkedin.urls</groupId>
<version>0.1.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-hibernate5 -->
<dependency>
<artifactId>jackson-datatype-hibernate5</artifactId>
<groupId>com.fasterxml.jackson.datatype</groupId>
<version>2.9.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-search-orm -->
<dependency>
<artifactId>hibernate-search-orm</artifactId>
<groupId>org.hibernate</groupId>
<version>5.11.1.Final</version>
</dependency>
</dependencies>
<description>Demo project for Spring Boot</description>
<groupId>com.llzw</groupId>
Expand Down Expand Up @@ -269,5 +281,5 @@
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<version>0.2.2-SNAPSHOT</version>
<version>0.2.3-SNAPSHOT</version>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ProductRepository
extends PagingAndSortingRepository<Product, Long>, JpaSpecificationExecutor<Product> {
extends
PagingAndSortingRepository<Product, Long>,
JpaSpecificationExecutor<Product>,
ProductSearchableRepository {

//PagingAndSortingRepository继承CrudRepository,支持实现分页排序相关方法
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.llzw.apigate.persistence.dao;

import com.llzw.apigate.persistence.entity.Product;
import java.util.List;

public interface ProductSearchableRepository {

List<Product> searchByNameWithCustomQuery(String text);

List<Product> searchByIntroductionWithCustomQuery(String text);

List<Product> searchByNameOrIntroductionWithCustomQuery(String text);

List<Product> searchByExample(Object example);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.llzw.apigate.persistence.dao;

import com.llzw.apigate.persistence.entity.Product;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.lucene.search.Query;
import org.hibernate.search.engine.ProjectionConstants;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;

public class ProductSearchableRepositoryImpl implements ProductSearchableRepository {

private static boolean indexed = false;

@PersistenceContext
private EntityManager entityManager;

public List<Product> searchByNameWithCustomQuery(String text) {
init();
Query query = getQueryBuilder()
.simpleQueryString()
.onFields("name")
.matching(text)
.createQuery();
return getJpaQuery(query).getResultList();
}

public List<Product> searchByIntroductionWithCustomQuery(String text) {
init();
Query query = getQueryBuilder()
.simpleQueryString()
.onFields("introduction")
.matching(text)
.createQuery();
return getJpaQuery(query).getResultList();
}

public List<Product> searchByNameOrIntroductionWithCustomQuery(String text) {
init();
Query combinedQuery = getQueryBuilder()
.simpleQueryString()
.onFields("name", "introduction")
.matching(text)
.createQuery();
return getJpaQuery(combinedQuery).getResultList();
}

@Override
public List<Product> searchByExample(Object example) {
init();
Query query = getQueryBuilder()
.moreLikeThis()
.excludeEntityUsedForComparison()
.comparingAllFields()
.toEntity(example)
.createQuery();
return getJpaQuery(query)
.setProjection(ProjectionConstants.THIS)
.getResultList();
}

private FullTextQuery getJpaQuery(Query luceneQuery) {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
return fullTextEntityManager.createFullTextQuery(luceneQuery, Product.class);
}

private QueryBuilder getQueryBuilder() {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
return fullTextEntityManager.getSearchFactory()
.buildQueryBuilder()
.forEntity(Product.class)
.get();
}

private void init() {
if (indexed) {
return;
}
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
try {
fullTextEntityManager.createIndexer().startAndWait();
indexed = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
@AllArgsConstructor
public class SearchCriterion {
public class JpaSearchCriterion {

private String key;
private String operation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;

public class SearchCriterionSpecification<T> implements Specification<T> {
public class JpaSearchSpecification<T> implements Specification<T> {

SearchCriterion criterion;
JpaSearchCriterion criterion;

public SearchCriterionSpecification(SearchCriterion criterion) {
public JpaSearchSpecification(JpaSearchCriterion criterion) {
this.criterion = criterion;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
import org.springframework.data.jpa.domain.Specification;

// TODO: It may lead to SQL injection.
public class SearchCriterionSpecificationFactory {
public class JpaSearchSpecificationFactory {

public static <T> List<Specification<T>> of(List<SearchCriterion> criteria) {
public static <T> List<Specification<T>> of(List<JpaSearchCriterion> criteria) {
List<Specification<T>> specifications = new ArrayList<>();
for (SearchCriterion criterion : criteria) {
specifications.add(new SearchCriterionSpecification<>(criterion));
for (JpaSearchCriterion criterion : criteria) {
specifications.add(new JpaSearchSpecification<>(criterion));
}
return specifications;
}

public static <T> Specification<T> and(List<SearchCriterion> criteria) {
public static <T> Specification<T> and(List<JpaSearchCriterion> criteria) {
List<Specification<T>> specifications = of(criteria);
Specification<T> cur = null;
for (Specification<T> specification : specifications) {
Expand All @@ -26,7 +26,7 @@ public static <T> Specification<T> and(List<SearchCriterion> criteria) {
return cur;
}

public static <T> Specification<T> or(List<SearchCriterion> criteria) {
public static <T> Specification<T> or(List<JpaSearchCriterion> criteria) {
List<Specification<T>> specifications = of(criteria);
Specification<T> cur = null;
for (Specification<T> specification : specifications) {
Expand All @@ -47,7 +47,7 @@ public static <T> Specification<T> or(List<SearchCriterion> criteria) {
* example.field1 = value1;
* example.field2 = value2;
* example.field3 = null;
* Specification specification = SearchCriterionSpecificationFactory.fromExample(example);
* Specification specification = JpaSearchSpecificationFactory.fromExample(example);
* </pre>
* The result specification will contain JPA query constrains similar to:
* <pre>
Expand All @@ -61,7 +61,7 @@ public static <T> Specification<T> or(List<SearchCriterion> criteria) {
* @return Constructed JPA Specification
*/
public static <T, D> Specification<T> fromExample(D obj) throws IllegalAccessException {
List<SearchCriterion> criteria = new ArrayList<>();
List<JpaSearchCriterion> criteria = new ArrayList<>();
Class c = obj.getClass();
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
Expand All @@ -70,7 +70,7 @@ public static <T, D> Specification<T> fromExample(D obj) throws IllegalAccessExc
if (value == null) {
continue;
}
criteria.add(new SearchCriterion(field.getName(), "=", value.toString()));
criteria.add(new JpaSearchCriterion(field.getName(), "=", value.toString()));
}
return and(criteria);
}
Expand All @@ -85,7 +85,7 @@ public static <T, D> Specification<T> fromExample(D obj) throws IllegalAccessExc
* Map<String, String> constrains = new HashMap<>();
* constrains.put("field1", ">=");
* constrains.put("field2", "=");
* Specification specification = SearchCriterionSpecificationFactory.fromExample(example, constrains);
* Specification specification = JpaSearchSpecificationFactory.fromExample(example, constrains);
* </pre>
* The result specification will contain JPA query constrains similar to:
* <pre>
Expand All @@ -94,11 +94,11 @@ public static <T, D> Specification<T> fromExample(D obj) throws IllegalAccessExc
* Fields with {@code null} value will be ignored.
*
* @param constraints field constrains map.
* @see SearchCriterionSpecificationFactory#fromExample(Object)
* @see JpaSearchSpecificationFactory#fromExample(Object)
*/
public static <T, D> Specification<T> fromExample(D obj, Map<String, String> constraints)
throws IllegalAccessException, NoSuchFieldException {
List<SearchCriterion> criteria = new ArrayList<>();
List<JpaSearchCriterion> criteria = new ArrayList<>();
Class c = obj.getClass();
for (Map.Entry<String, String> entry : constraints.entrySet()) {
String fieldName = entry.getKey();
Expand All @@ -109,7 +109,7 @@ public static <T, D> Specification<T> fromExample(D obj, Map<String, String> con
if (value == null) {
continue;
}
criteria.add(new SearchCriterion(fieldName, op, value.toString()));
criteria.add(new JpaSearchCriterion(fieldName, op, value.toString()));
}
return and(criteria);
}
Expand All @@ -123,7 +123,7 @@ public static <T, D> Specification<T> fromExample(D obj, Map<String, String> con
// constrains.put("field1", ">=");
// constrains.put("field2", "=");
// try {
// SearchCriterionSpecificationFactory f = new SearchCriterionSpecificationFactory();
// JpaSearchSpecificationFactory f = new JpaSearchSpecificationFactory();
// f.field1 = 1;
// f.field2 = 2.1;
// fromExample(f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import java.util.Date;
import javax.persistence.Column;
Expand Down Expand Up @@ -42,6 +43,7 @@ public class Payment extends BaseEntity {
protected Order order;

@Transient
@JsonProperty
protected String orderString;

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.TermVector;
import org.springframework.transaction.annotation.Transactional;

@Indexed
@Entity
@Data
@EqualsAndHashCode(callSuper = true)
Expand All @@ -47,13 +51,15 @@ public class Product extends BaseEntity {

@Column(nullable = false)
@NonNull
@Field(termVector = TermVector.YES)
protected String name;

@ElementCollection
protected List<String> mainImageFiles;

@Lob
@NonNull
@Field(termVector = TermVector.YES)
protected String introduction;

@Column(nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.llzw.apigate.persistence.dao.OrderRepository;
import com.llzw.apigate.persistence.dao.ProductRepository;
import com.llzw.apigate.persistence.dao.StockRepository;
import com.llzw.apigate.persistence.dao.customquery.SearchCriterionSpecificationFactory;
import com.llzw.apigate.persistence.dao.customquery.JpaSearchSpecificationFactory;
import com.llzw.apigate.persistence.entity.AddressBean;
import com.llzw.apigate.persistence.entity.Order;
import com.llzw.apigate.persistence.entity.Product;
Expand Down Expand Up @@ -75,7 +75,7 @@ public List<Order> search(OrderSearchDto example, User relatedUser, Pageable pag
try {
// Result orders may contain other user's order, so we should filter them out.
return orderRepository
.findAll(SearchCriterionSpecificationFactory.fromExample(example), pageable)
.findAll(JpaSearchSpecificationFactory.fromExample(example), pageable)
.getContent().stream()
.filter(o -> o.belongsToUser(relatedUser))
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.llzw.apigate.persistence.entity.Product;
import com.llzw.apigate.persistence.entity.User;
import com.llzw.apigate.web.dto.ProductCreateDto;
import com.llzw.apigate.web.dto.ProductSearchDto;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
Expand All @@ -19,5 +20,5 @@ public interface ProductService {

Optional<Product> findById(Long id);

List<Product> findAll(Pageable pageable);
List<Product> search(Pageable pageable, ProductSearchDto example) throws RestApiException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import com.llzw.apigate.persistence.entity.Product;
import com.llzw.apigate.persistence.entity.User;
import com.llzw.apigate.web.dto.ProductCreateDto;
import com.llzw.apigate.web.dto.ProductSearchDto;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.Setter;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -99,8 +101,25 @@ public Optional<Product> findById(Long id) {
}

@Override
public List<Product> findAll(Pageable pageable) {
return productRepository.findAll(pageable).getContent();
public List<Product> search(Pageable pageable, ProductSearchDto dto) throws RestApiException {
String nameQueryString = dto.getName();
String introductionQueryString = dto.getName();
String global = dto.getGlobal();
List<Product> result;
if (global != null) {
result = productRepository.searchByNameOrIntroductionWithCustomQuery(global);
} else if (nameQueryString != null || introductionQueryString != null) {
Product example = new Product();
example.setName(nameQueryString);
example.setIntroduction(introductionQueryString);
result = productRepository.searchByExample(example);
} else {
result = productRepository.findAll(pageable).getContent();
}
if (!dto.isValid()) {
result = result.stream().filter(Product::isValid).collect(Collectors.toList());
}
return result;
}

/**
Expand Down
Loading

0 comments on commit cfb44b6

Please sign in to comment.