spring-data-jpa 入门三:常用技术使用之复杂查询

系列文章
spring-data-jpa 入门
spring-data-jpa 入门二:常用技术使用之关联关系查询配置

前面基本上将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 查询:

  1. EntityManager获取CriteriaBuilder
  2. CriteriaBuilder创建CriteriaQuery
  3. CriteriaQuery指定要查询的表,得到Root< UserInfo>,Root代表要查询的表,其实也就是个UserInfo的包装对象
  4. CriteriaBuilder创建条件Predicate,Predicate 其实就是谓语,断言的意思相对于SQL的where条件,可多个
  5. 通过EntityManager创建TypedQuery
  6. 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_infowhere 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);
    }
  1. UserInfoVo 只是基础了UserInfo,并且多了一个查询条件address,并不是UserDetails中的address;
  2. PageRequest 前面也说过它是一个分页对象,例如下面就是一个 请求第一页,每页数量为10,并且以Id 降序的分页条件

        new PageRequest(0,10, new Sort(Sort.Direction.DESC, new String[]{"id"}))
  3. 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;
}
  1. 首先说下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)了;

  1. ②和③其实表达的是一个意思,只不过两种不同的写法,②使用的join的方式,相对而言我还是比较喜欢用③的写法。

到这基本上将动态查询说清楚了,Spring-data-jpa对于我们来说应该不是那么陌生了,后面的话还将继续探讨一些问题:

  • 批量操作、EntityManager状态分析
  • 常用注解总结
  • json解析时延迟加载问题
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章