系列文章
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 查詢:
- 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解析時延遲加載問題