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解析時延遲加載問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章