上一篇我们说到了spring-data-rest 和spring-data-jpa 使用的主要意义。主要目的就是为了减少模板化的代码,那么针对某个特定的repository的查询,我们是不是也可以将查询简化呢?答案当然是肯定的。
我们先来看一下jpa有哪些查询方法:
1.Query methods
The JPA module supports defining a query manually as String or have it being derived from the
method name.
JPA 模块支持手动定义一个查询字符串或者从一个方法名中导出查询。
用法如下:
public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
对应的生成的查询为:select u from User u where u.emailAddress = ?1 and u.lastname = ?2.
当然你也可以直接指定查询语句:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}若是想使用原生的sql,只需要开启 nativeQuery =true
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery =
true)
User findByEmailAddress(String emailAddress);
}
如果我们只是在后台内部做查询,不涉及到前端web请求查询,那么命名查询是一个不错的选择。因为如果走web请求,想要实现动态查询,那么你就不得不去写很多个方法或者写很多条查询语句了。
2.Stored proceduresThe JPA 2.1 specification introduced support for calling stored procedures via the JPA criteria query
API. We Introduced the @Procedure annotation for declaring stored procedure metadata on a
repository method.
JPA2.1 规范通过JPA条件查询API引进了对存储过程的支持。在一个库方法上,通过使用@Procedure声明存储过程元数据。
@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer
.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type =
Integer.class) })
public class User {}在方法上调用存储过程:
@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);
因为我们涉及到的都是单表查询,而且没有复杂的逻辑要处理,所以存储过程还是用在它该用的地方吧!
3.Specifications
Specifications can easily be used to build an extensible set of
predicates on top of an entity that then can be combined and used with JpaRepository without the
need to declare a query (method) for every needed combination。
Specifications 可以容易地在一个predicate实体之上构造可扩展的predicates集合,它可以被用来组合,而不需要为每次的组合声明一个查询(方法)。
看到这里,看到了希望,因为它很明确的说明出了查询条件是可以组合的。
要想使用Specifications,我们自己的repository一定要继承JpaSpecificationExecutor接口
public interface CustomerRepository extends CrudRepository<Customer, Long>,
JpaSpecificationExecutor {
List<T> findAll(Specification<T> spec);
}
下面是如何构造自己的specification
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(_Customer.createdAt), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) {
return new Specification<Customer>() {
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));它的用法很简单,关键是它为我们提供了一种动态查询的可能。
4.Query by Example
Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows
dynamic query creation and does not require to write queries containing field names. In fact,
Query by Example does not require to write queries using store-specific query languages at all.
通过 Example查询是一种通过实现简单接口的很友好的查询技术。它允许创建动态查询,并且不需要写包含 域的名字的查询。实际上,QBE不需要写任何特定存储查询语言。
看到这里心里不免又窃喜了一下,看来要实现动态查询还有第二种选择。
先来看看用法:
• Probe: That is the actual example of a domain object with populated fields.(带着填充属性的实体类)• ExampleMatcher: The ExampleMatcher carries details on how to match particular fields. It can be
reused across multiple Examples.(样例匹配器携带了关于如何匹配特定域的详细信息)
• Example: An Example consists of the probe and the ExampleMatcher. It is used to create the query.
(包含了探针和样例匹配器的一个样例,它被用来创建查询)
实体类:
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}Person person = new Person(); ①
person.setFirstname("Dave"); ②
ExampleMatcher matcher = ExampleMatcher.matching() ③
.withIgnorePaths("lastname") ④
.withIncludeNullValues() ⑤
.withStringMatcherEnding(); ⑥
Example<Person> example = Example.of(person, matcher); ⑦①创建实例 ②设置属性
③创建一个样例匹配器,即使没有任何配置,但在这时已经可用
④构造了一个忽略“lastname”属性的ExampleMatcher
⑤构造了一个忽略“lastname”属性并且包含空值的ExampleMatcher
⑥构造了一个忽略“lastname”属性并且包含空值的,匹配字符串后缀的ExampleMatcher
⑦创建一个基于Person和已配置过的ExampleMatcher的Example
用它就和上面使用Specifications 类似:
public interface PersonRepository extends JpaRepository<Person, String> { … }
public class PersonService {
@Autowired PersonRepository personRepository;
public List<Person> findPeople(Person probe) {
return personRepository.findAll(Example.of(probe));
}
}使用它的好处和不足之处:
When to use
• Querying your data store with a set of static or dynamic constraints(使用一组动态或者静态约束条件来查询你的数据)
• Frequent refactoring of the domain objects without worrying about breaking existing queries(频繁的重构你的实体对象不需要担心破坏已有的查询)
• Works independently from the underlying data store API(独立于底层数据存储API而工作)
Limitations
• Query predicates are combined using the AND keyword(查询条件都是通过and组合的)
• No support for nested/grouped property constraints like firstname = ?0 or (firstname = ?1 and
lastname = ?2)(不支持嵌套的/分组的属性约束)
• Only supports starts/contains/ends/regex matching for strings and exact matching for other
property types(只支持开始、包含、结束、正则配置字符串,并且其他属性类型只支持精确匹配)
看到这里,心里又不免倒吸了一口凉气,看来别无选择了,只能通过Specifications下手了。
接下来说一说自己是如何实现web请求实现对一个特定Repository的动态查询。
1.定义一个查询类来解析web请求中的参数
QueryExpression
public class QueryExpression implements Serializable {
private static final long serialVersionUID = 7394678134085338254L;
public enum Operator {
EQ, NEQ, GT, LT, GTE, LTE, LIKE, REGEXP, NREGEXP, IREGEXP, NIREGEXP, ISNULL, ISNOTNULL, IN,
}
/**
* domain field such as `name`; `user.fullname`;
*/
private String field;
/**
* operator such as `EQ`; `LT`;
*/
private Operator operator;
/**
* field value to query.
*/
private String value;
/**
* and Expression, the quety express is `field` and `ands` and `ors`
*/
private Set<QueryExpression> ands;
/**
* or Expression, the quety express is `field` and `ands` and `ors`
*/
private Set<QueryExpression> ors;
public QueryExpression() {
super();
}
public QueryExpression(String field, Operator operator, String value) {
super();
this.field = field;
this.operator = operator;
this.value = value;
}
public void or(QueryExpression... ors) {
if (this.ors == null) {
this.ors = new HashSet<>();
}
for (QueryExpression exp : ors) {
this.ors.add(exp);
}
}
public void and(QueryExpression... ands) {
if (this.ands == null) {
this.ands = new HashSet<>();
}
for (QueryExpression exp : ands) {
this.ands.add(exp);
}
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
public Operator getOperator() {
return operator;
}
public void setOperator(Operator operator) {
this.operator = operator;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Collection<QueryExpression> getAnds() {
return ands;
}
public void setAnds(Collection<QueryExpression> ands) {
if (ands == null || ands.size() == 0) {
this.ands = null;
return;
}
this.ands = new HashSet<>();
this.ands.addAll(ands);
}
public Collection<QueryExpression> getOrs() {
return ors;
}
public void setOrs(Collection<QueryExpression> ors) {
if (ors == null || ors.size() == 0) {
this.ors = null;
return;
}
this.ors = new HashSet<>();
this.ors.addAll(ors);
}
}2.解析web请求中的参数,生成Specification
public class RequestQueryHandlerImpl implements RequestQueryHandler {
protected static final Logger logger = LoggerFactory.getLogger(RequestQueryHandlerImpl.class);
protected final ObjectMapper objectMapper;
protected final SecurityUserDetailsService securityUserDetailsService;
public RequestQueryHandlerImpl(SecurityUserDetailsService securityUserDetailsService, ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.securityUserDetailsService = securityUserDetailsService;
}
@Override
public QueryExpression buildQueryExpress(NativeWebRequest httpRequest) {
try {
List<QueryExpression> expressions = new ArrayList<>();
String requestQuery = httpRequest.getParameter(REQUEST_PARAMETER_QUERY_NAME);
if (!StringUtils.isEmpty(requestQuery)) {
logger.debug("Query Parameter Expression {}", requestQuery);
QueryExpression expression = objectMapper.readValue(requestQuery, QueryExpression.class);
if (expression != null) {
expressions.add(expression);
} else {
logger.info("Query Expression is not correctly organized,it is parsed as null!");
}
}
String[] headerQuery = httpRequest.getHeaderValues(REQUEST_HEADER_QUERY_NAME);
if (headerQuery != null) {
for (String query : headerQuery) {
if (!StringUtils.isEmpty(query)) {
logger.debug("Query Header Expression {}", query);
QueryExpression expression = objectMapper.readValue(query, QueryExpression.class);
if (expression != null) {
expressions.add(expression);
} else {
logger.info("Query Expression is not correctly organized, it is parsed as null!");
}
}
}
}
if (expressions.size() == 1) {
return expressions.get(0);
}
QueryExpression expression = new QueryExpression();
expression.setAnds(expressions);
return expressions.size() == 0 ? null : expression;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public <T> Specification<T> buildSpecification(QueryExpression expression) {
if (expression == null) {
return null;
}
return new Specification<T>() {
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return buildPredicate(root, query, builder, expression);
}
};
}
public <T> Specification<T> buildSpecificationWithFilter(QueryExpression expression) {
SecurityUser currentUser = securityUserDetailsService.currentSecurityUser();
if (expression == null && currentUser == null) {
return null;
}
return new Specification<T>() {
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new LinkedList<>();
if (expression != null) {
Predicate predicate = buildPredicate(root, query, builder, expression);
if (predicate != null) {
predicates.add(predicate);
}
}
if (currentUser != null) {
Predicate predicate = buildPredicate(root, query, builder, currentUser);
if (predicate != null) {
predicates.add(predicate);
}
}
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}
/**
*
* @param root
* @param query
* @param builder
* @param currentUser
* @return
*/
protected <T> Predicate buildPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder,
SecurityUser currentUser) {
Class<?> type = root.getJavaType();
if (!AbstractSecurity.class.isAssignableFrom(type)) {
throw new RuntimeException(type.getName() + " can not assignable from " + AbstractSecurity.class.getName());
}
Path<UUID> pathAssignTo = root.get(AbstractSecurity.assignTo);
Predicate nullExp = pathAssignTo.isNull();
Collection<UUID> assignTos = securityUserDetailsService.currentAssignTos();
if (assignTos.isEmpty()) {
return nullExp;
}
Predicate inExp = pathAssignTo.in(assignTos);
return builder.or(nullExp, inExp);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected <T> Predicate buildPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder,
QueryExpression expression) {
List<Predicate> predicates = new LinkedList<>();
Predicate predicate = null;
String field = expression.getField();
if (field != null) {
String[] names = field.split("\\.");
if (names == null) {
names = new String[] { field };
}
Path path = root.get(names[0]);
for (int i = 1; i < names.length; i++) {
path = path.get(names[i]);
}
switch (expression.getOperator()) {
case EQ:
predicate = builder.equal(path, valueOf(root, names, expression.getValue()));
break;
case NEQ:
predicate = builder.notEqual(path, valueOf(root, names, expression.getValue()));
break;
case REGEXP:
predicate = builder.isTrue(builder.function("RegExp", Boolean.class, path.as(String.class),
builder.literal(expression.getValue())));
break;
case NREGEXP:
predicate = builder.isTrue(builder.function("NotRegExp", Boolean.class, path.as(String.class),
builder.literal(expression.getValue())));
break;
case IREGEXP:
predicate = builder.isTrue(builder.function("RegExpIgnoreCase", Boolean.class, path.as(String.class),
builder.literal(expression.getValue())));
break;
case NIREGEXP:
predicate = builder.isTrue(builder.function("NotRegExpIgnoreCase", Boolean.class, path.as(String.class),
builder.literal(expression.getValue())));
break;
case LIKE:
predicate = builder.like(path.as(String.class), expression.getValue());
break;
case GT:
predicate = builder.greaterThan(path, (Comparable) valueOf(root, names, expression.getValue()));
break;
case LT:
predicate = builder.lessThan(path, (Comparable) valueOf(root, names, expression.getValue()));
break;
case GTE:
predicate = builder.greaterThanOrEqualTo(path,
(Comparable) valueOf(root, names, expression.getValue()));
break;
case LTE:
predicate = builder.lessThanOrEqualTo(path, (Comparable) valueOf(root, names, expression.getValue()));
break;
case ISNULL:
predicate = builder.isNull(path);
break;
case ISNOTNULL:
predicate = builder.isNotNull(path);
break;
case IN:
Class<?> domainType = root.getJavaType();
Class<?> clazz = root.getModel().getBindableJavaType();
try {
for (int i = 0; i < names.length; i++) {
clazz = getField(clazz, names[i]);
}
} catch (NoSuchFieldException e) {
throw new RuntimeException("class `" + domainType + "` has No `"
+ StringUtils.arrayToDelimitedString(names, ".") + "` Field", e);
}
predicate = path.in(valuesOf(expression.getValue(), clazz));
break;
}
}
if (predicate != null) {
predicates.add(predicate);
}
if (expression.getAnds() != null) {
for (QueryExpression exp : expression.getAnds()) {
predicate = buildPredicate(root, query, builder, exp);
if (predicate != null) {
predicates.add(predicate);
}
}
}
List<Predicate> ors = new LinkedList<>();
if (expression.getOrs() != null) {
for (QueryExpression a : expression.getOrs()) {
predicate = buildPredicate(root, query, builder, a);
if (predicate != null) {
ors.add(predicate);
}
}
}
if (ors.size() > 0) {
predicate = builder.or(ors.toArray(new Predicate[ors.size()]));
if (predicate != null) {
predicates.add(predicate);
}
}
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
}
protected <T> Collection<T> valuesOf(String strValue, Class<T> type) {
try {
JavaType javaType;
javaType = objectMapper.getTypeFactory().constructParametrizedType(ArrayList.class, List.class, type);
return objectMapper.readValue(strValue, javaType);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected Object valueOf(Class<?> domainType, String[] names, String strValue) {
if (strValue == null) {
return null;
}
Class<?> clazz = domainType;
if (names != null) {
try {
for (int i = 0; i < names.length; i++) {
clazz = getField(clazz, names[i]);
}
} catch (NoSuchFieldException e) {
throw new RuntimeException("class `" + domainType.getName() + "` has No `"
+ StringUtils.arrayToDelimitedString(names, ".") + "` Field", e);
}
}
Object value;
if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
value = Boolean.valueOf(strValue);
} else if (Short.class.equals(clazz) || short.class.equals(clazz)) {
value = Short.valueOf(strValue);
} else if (Integer.class.equals(clazz) || int.class.equals(clazz)) {
value = Integer.valueOf(strValue);
} else if (Long.class.equals(clazz) || long.class.equals(clazz)) {
value = Long.valueOf(strValue);
} else if (Float.class.equals(clazz) || float.class.equals(clazz)) {
value = Float.valueOf(strValue);
} else if (Double.class.equals(clazz) || double.class.equals(clazz)) {
value = Double.valueOf(strValue);
} else if (UUID.class.equals(clazz)) {
value = UUID.fromString(strValue);
} else if (Date.class.equals(clazz) || DateTime.class.equals(clazz) || java.sql.Date.class.equals(clazz)) {
try {
value = new Date(Long.parseLong(strValue));
} catch (NumberFormatException e) {
throw new RuntimeException(e);
}
} else {
value = strValue;
}
return value;
}
protected <T> Object valueOf(Root<T> root, String[] names, String strValue) {
Class<?> clazz = root.getModel().getBindableJavaType();
return valueOf(clazz, names, strValue);
}
protected Class<?> getField(Class<?> clazz, String name) throws NoSuchFieldException {
try {
Field f = clazz.getDeclaredField(name);
return f.getType();
} catch (NoSuchFieldException e) {
try {
Method get = clazz.getMethod("get" + Character.toUpperCase(name.charAt(0)) + name.substring(1),
new Class<?>[] {});
Class<?> param = get.getReturnType();
if (param == null || param.equals(void.class) || param.equals(Void.class)) {
throw new NoSuchMethodException();
}
return param;
} catch (NoSuchMethodException ee) {
clazz = clazz.getSuperclass();
if (clazz != null) {
return getField(clazz, name);
}
throw e;
}
}
}
3.根据Jpa 原来的参数解析器,替换为自己的QueryJpaArgumentResolver,在这里我们继承RootResourceInformationHandlerMethodArgumentResolver
并重写resolveArgument()和postProcess()方法。大致的思路就是从webRequest中解析出查询参数,然后组装成Specification,再把Specification交给RepositoryAdapter代理去处理。
public class QueryJpaArgumentResolver extends RootResourceInformationHandlerMethodArgumentResolver {
private final Repositories repositories;
private final RepositoryInvokerFactory invokerFactory;
private final ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver;
@Autowired
private RequestQueryHandler requestQueryHandler;
public QueryJpaArgumentResolver(Repositories repositories, RepositoryInvokerFactory invokerFactory,
ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver) {
super(repositories, invokerFactory, resourceMetadataResolver);
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(invokerFactory, "invokerFactory must not be null!");
Assert.notNull(resourceMetadataResolver, "ResourceMetadataHandlerMethodArgumentResolver must not be null!");
this.repositories = repositories;
this.invokerFactory = invokerFactory;
this.resourceMetadataResolver = resourceMetadataResolver;
}
@Override
public RootResourceInformation resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
ResourceMetadata resourceMetadata = resourceMetadataResolver.resolveArgument(parameter, mavContainer,
webRequest, binderFactory);
Class<?> domainType = resourceMetadata.getDomainType();
RepositoryInvoker repositoryInvoker = invokerFactory.getInvokerFor(domainType);
PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(domainType);
// TODO reject if ResourceMetadata cannot be resolved
return new RootResourceInformation(resourceMetadata, persistentEntity,
postProcess(parameter, repositoryInvoker, domainType, webRequest.getParameterMap(), webRequest));
}
@Override
protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker, Class<?> domainType,
Map<String, String[]> parameters) {
return super.postProcess(parameter, invoker, domainType, parameters);
}
protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker, Class<?> domainType,
Map<String, String[]> parameters, NativeWebRequest webRequest) {
QueryExpression expression = requestQueryHandler.buildQueryExpress(webRequest);
Specification<?> specification;
if (AbstractSecurity.class.isAssignableFrom(domainType)) {
specification = requestQueryHandler.buildSpecificationWithFilter(expression);
} else {
specification = requestQueryHandler.buildSpecification(expression);
}
if (specification != null) {
return new QueryRepositoryInvokerAdapter(repositories.getRepositoryFor(domainType),
repositories.getRepositoryInformationFor(domainType), new DefaultFormattingConversionService(),
specification);
}
return super.postProcess(parameter, invoker, domainType, parameters);
}
}
这里还有一些细节没有贴出,整个实现的思路大致如上。现在我们可以“一劳永逸“的接收web端的参数,并实现所有实体的动态查询(不包括多表联合查询)。
有时候就是要去“偷懒”,因为有些重复的工作实在是没必要。
本文介绍了如何使用 Spring Data JPA 的 Specifications 功能进行动态查询,详细讲解了 Specifications 的优势和使用方法,以及如何结合 Web 请求处理动态查询。同时对比了 Query by Example 的查询方式,最后分享了自己实现 Web 请求动态查询的步骤和思路。

被折叠的 条评论
为什么被折叠?



