前面基本上将spirng-data-jpa常用查询写清楚了,一般如果不是复杂的查询基本上都能满足了,而且我们并没有做太多的事情,花费时间大多是在entity层实体的配置。现在我们将介绍下在复杂情况下的查询方法的使用:
- 常用技术使用
- 原生sql查询
- 动态sql(两种方式:Criteria、继承JpaSpecificationExecutor)
- 多表多条件复杂查询
- 动态条件查询(复杂条件 in、join 等)
- 批量操作、EntityManager状态分析
- 常用注解总结
- json解析时延迟加载问题
@Query 原生sql查询
前面说过dao层spring-data-jpa会默认解析以findBy开头方法命自动组装成sql,虽然这种方式使用快捷便利,但是难免会出现一些复杂跨多张表的情况,而且多表之间没有关联的情况,为此我们可以直接使用sql查询。
在spring-data-jpa中 用@Query表示dao层方法自己实现。
/**
* 用户信息dao
* Created by hsh on 18/08/30.
*/
public interface UserInfoRepository extends
JpaRepository<UserInfo, Integer>,
JpaSpecificationExecutor<UserInfo> {
@Query("select u from UserInfo u where u.id=?1 ")
UserInfo findById(Integer id);
/**
* 原生分页 查询条件不能为空
*/
Page<UserInfo> findByUNameContainingAndUNumberEqualsAndIdEquals
(String uName, String uNumber, Integer id, Pageable pageable);
}
这是我们原先的dao层,在此基础之上我们用上@Query注解,至于括号里面的则是JPQL 语句,属性hql应该对JPQL
不默认,这里就不延伸了,后文会介绍下JPQL。
至于 where u.id=?1
?1 则表示的是第一个参数,这里面则是Integer id,如果有多个参数话,依次类推?2、?3等,而且使用的时候没有顺序的,例如:
@Query("select u from UserInfo u where u.password=?2 and u.UName=?1 ")
UserInfo findByUserNameAndAndPassword(String userName,String password);
当然@Query是支持原生sql的,例如:
@Query(value = "select * from user_info where id=:id",nativeQuery = true)
UserInfo findById(Integer id);
nativeQuery 意思是本地化查询,也就是原生sql查询。
顺道补充下,在@Query中,参数占位符的使用方式:
1. 如上面我们给的例子,使用 ?1 这种形式。
2. 使用 :参数名称 的形式,例如第二个例子
3. 使用 @Param注解的形式,例如:
@Query(value = "select * from user_info where id=:id1",nativeQuery = true)
UserInfo findById(@Param("id1")Integer id);
其实总结下来只是一种机制:如果有注解,则用注解,没有注解默认为参数名称,使用时候可以直接名称或者用坐标表示。mybatis其实也是种形式。
动态查询(两种方式:Criteria API、继承JpaSpecificationExecutor)
接下来是重点了,mybatis非常流行,有一定原因是因为它的丰富的动态标签。当然spirng-data-jpa也是支持动态查询的,一共两种方式:
1. 通过JPA的Criteria(标准) API实现
2. dao层接口继承JpaSpecificationExecutor
只是简单的说下怎么使用Criteria API 查询:
- EntityManager获取CriteriaBuilder
- CriteriaBuilder创建CriteriaQuery
- CriteriaQuery指定要查询的表,得到Root< UserInfo>,Root代表要查询的表,其实也就是个UserInfo的包装对象
- CriteriaBuilder创建条件Predicate,Predicate 其实就是谓语,断言的意思相对于SQL的where条件,可多个
- 通过EntityManager创建TypedQuery
- TypedQuery执行查询,返回结果
举个列子:
public class UserInfoDaoImpl {
@PersistenceContext(unitName = "entityManagerFactory")
EntityManager em;
public List<UserInfo> getUserInfo(UserInfo userInfo) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery< UserInfo> query = builder.createQuery(UserInfo.class);
Root< UserInfo> root = query.from(UserInfo.class);
Predicate p1 = builder.like(root.< String> get("uName"), "%" + userInfo.getName() + "%");
Predicate p2 = builder.equal(root.< String> get("password"), userInfo.getPassword());
query.where(p1, p2);
List<UserInfo> userInfos = em.createQuery(query).getResultList();
return userInfos;
}
}
解释下:
1. 这是个 dao层实现类,在前面配置的时候,我们制定了实现类是以Impl结尾,默认且必须与dao接口在同一个文件夹。
2. @PersistenceContext 表示的是 持久化单元上下文
3. unitName与LocalContainerEntityManagerFactoryBean类的容器对象的名称一致
4. builder.like 与builder.equal 就相当于构建了两个where 条件 name like = ? and password = ?
的形式
5. query.where(p1,p2) 就相当于拼接sql 将 select * from user_info
与 where name like = ? and password = ?
拼装在一起
6. getSingleResult或者getResultList返回结果,这里jpa的单个查询如果为空的话会报异常
代码虽然简单明了,且步骤清晰,总结就是四步 :创建builder => 创建Query => 构造条件 => 查询,但是其实除了第三步,其他对我们来说完全是一模一样的模板,所以 spring-data-jpa 给我做了这些事情。
dao层接口继承JpaSpecificationExecutor
我们先将上面的代码用第二种方式写出了,在具体分析:
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
private UserInfoRepository userInfoRepository;
@Override
public List<UserInfo> getUserInfoList(final UserInfo userInfo) {
return userInfoRepository.findAll(new Specification<UserInfo>() {
public Predicate toPredicate(Root<UserInfo> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (userInfo != null && userInfo.getUName() != null)
predicates.add(criteriaBuilder.like(root.<String>get("uName"), "%" + userInfo.getUName() + "%"));
if (userInfo != null && userInfo.getPassword() != null)
predicates.add(criteriaBuilder.equal(root.<String>get("password"), userInfo.getPassword()));
if (predicates.size() > 0)
return criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
return null;
}
});
}
}
首先我们调用了userInfoRepository.findAll(new Specification< UserInfo>() {});方法,来自于JpaSpecificationExecutor< T> 接口,他是这么定义的:
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> var1);
List<T> findAll(Specification<T> var1);
Page<T> findAll(Specification<T> var1, Pageable var2);
List<T> findAll(Specification<T> var1, Sort var2);
long count(Specification<T> var1);
}
而 Specification 是个接口,内部是这么定义的
public interface Specification<T> {
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
}
到此我们知道原来Specification这个接口其实就是返回一个Predicate 对象,前面说了Predicate 其实就是在组装where条件语句。那么 JpaSpecificationExecutor 接口下面的定义的方法,其实就是采用策略模式,接口参数为Specification,前面也说了框架内部会自动实现,而且这个实现就是SimpleJpaRepository ,我们所有的Dao层接口默认实现类都是他
public class SimpleJpaRepository<T, ID extends Serializable>
implements JpaRepository<T, ID>, JpaSpecificationExecutor< T> {
我们看下SimpleJpaRepository中 List< T> findAll(Specification< T> var1)的实现:
public List<T> findAll(Specification<T> spec) {
return this.getQuery(spec, (Sort)null).getResultList();
}
这里自己调用getQuery 方法,而getQuery是这样的:
protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
return this.getQuery(spec, this.getDomainClass(), sort);
}
其中getDomainClass() 返回类型是Class< T> 也就是说返回的是一个类型,
那么getQuery(spec, this.getDomainClass(), sort) 的代码如下:
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
if(sort != null) {
query.orderBy(QueryUtils.toOrders(sort, root, builder));
}
return this.applyRepositoryMethodMetadata(this.em.createQuery(query));
}
到这一觉都恍然大悟,原来它做的事情还是一开始我们写的Criteria API的写法啊,问题就在这句话里面了
Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
applySpecificationToCriteria方法实现:
private <S, U extends T> Root<U> applySpecificationToCriteria
(Specification<U> spec, Class<U> domainClass, CriteriaQuery<S> query) {
Assert.notNull(domainClass, "Domain class must not be null!");
Assert.notNull(query, "CriteriaQuery must not be null!");
Root<U> root = query.from(domainClass);
if(spec == null) {
return root;
} else {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);
if(predicate != null) {
query.where(predicate);
}
return root;
}
}
看到Predicate predicate = spec.toPredicate(root, query, builder);
这句话没有,不就是在调用我们在接口里面写的匿名内部类的实现嘛。
所以一开始就说了 继承JpaSpecificationExecutor接口与Criteria API 写法是一致的,就是封装了一下,就和我们平时写JDBC时候,都会封装一个工具类差不多,只不过这是人家Spring封装的。
多表多条件复杂查询
上面我们大致了解到Spring-data-jpa 两种动态查询的写法,并且着重的分析了下 继承JpaSpecificationExecutor接口的实现以及原理。下面就要来点复杂的东西了,来看看 如果多表多条件查询改怎么做。
前面我们做过关联关系查询配置,并且配置了一对一,一对多,多对多的配置。通过配置我们能查到关联实体的信息,现在我们用 findAll(Specification<T> var1, Pageable var2 )
来实验下:
public Page<UserInfo> findUserInfo(final UserInfoVo userInfo, PageRequest request) throws Exception {
return userInfoRepository.findAll(new Specification<UserInfo>() {
public Predicate toPredicate(Root<UserInfo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (userInfo != null && userInfo.getIds() != null) {
predicates.add(cb.equal(root.<Integer>get("id"), userInfo.getId()));
}
if (userInfo != null && userInfo.getUName() != null)
predicates.add(cb.like(root.<String>get("uName"), "%" + userInfo.getUName() + "%"));
if (userInfo != null && userInfo.getUNumber() != null)
predicates.add(cb.equal(root.<String>get("uNumber"), userInfo.getUNumber()));
if (userInfo != null && userInfo.getAddress() != null)
//查询用户详情信息
predicates.add(cb.like(
root.<UserDetails>get("userDetails").<String>get("address"),
"%" + userInfo.getAddress() + "%"));
if (predicates.size() > 0)
return query.where(
predicates.toArray(new Predicate[predicates.size()])
).getRestriction();
return null;
}
}, request);
}
- UserInfoVo 只是基础了UserInfo,并且多了一个查询条件address,并不是UserDetails中的address;
- PageRequest 前面也说过它是一个分页对象,例如下面就是一个 请求第一页,每页数量为10,并且以Id 降序的分页条件
new PageRequest(0,10, new Sort(Sort.Direction.DESC, new String[]{"id"}))
- root.< UserDetails>get(“userDetails”) 就是调用 UserInfo 中 UserDetails对象,整句话意思是如果查询参数‘address’不为空,就查询UserDetails实体对应的表中含有查询参数‘address’的用户信息,这里使用了一个多级的get,这个是spring-data-jpa支持的,就是嵌套对象的属性,这种做法一般我们叫方法的级联调用,就是调用的时候返回自己本身。
动态条件查询(复杂条件 in、join )
其实能明白 上面的例子之后,Spring-data-jpa 可以说已经揭开神秘的面纱了,后面的无非是补充一些常用的知识点而已,哈哈,还是通过上面的例子我补充下In和join的用法感觉这两个还是比较常用:
public Predicate toPredicate(Root<UserInfo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (userInfo != null && userInfo.getIds() != null) {
String[] ids = userInfo.getIds().split(",");
//①
CriteriaBuilder.In<Integer> in = cb.in(root.<Integer>get("id"));
for (String id : ids) {
in.value(Integer.valueOf(id));
}
predicates.add(in);
}
if (userInfo != null && userInfo.getUName() != null)
predicates.add(cb.like(root.<String>get("uName"), "%" + userInfo.getUName() + "%"));
if (userInfo != null && userInfo.getUNumber() != null)
predicates.add(cb.equal(root.<String>get("uNumber"), userInfo.getUNumber()));
if (userInfo != null && userInfo.getAddress() != null)
Join<UserInfo, UserDetails> userDetails = root.join("userDetails", JoinType.LEFT);
//②
predicates.add(cb.like(userDetails.<String>get("address"), "%" + userInfo.getAddress() + "%"));
//③
// predicates.add(cb.like(
// root.<UserDetails>get("userDetails").<String>get("address"),
// "%" + userInfo.getAddress() + "%"));
if (predicates.size() > 0)
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
return null;
}
- 首先说下userInfo这个查询参数的id 默认是一个“1,2,3”这样形式,默认查询多个ID,所以在①中CriteriaBuilder 调用 in方法并声明这个In的参数类型为Integer,CriteriaBuilder.In是这么定义的:
public interface In<T> extends Predicate {
Expression<T> getExpression();
CriteriaBuilder.In<T> value(T var1);
CriteriaBuilder.In<T> value(Expression<? extends T> var1);
}
其实还是个Predicate ,所以在value()完所以Id之后直接predicates.add(in)
了;
- ②和③其实表达的是一个意思,只不过两种不同的写法,②使用的join的方式,相对而言我还是比较喜欢用③的写法。
到这基本上将动态查询说清楚了,Spring-data-jpa对于我们来说应该不是那么陌生了,后面的话还将继续探讨一些问题:
- 批量操作、EntityManager状态分析
- 常用注解总结
- json解析时延迟加载问题