1. 程式人生 > >spring data jpa Specification的使用

spring data jpa Specification的使用

看了很多前言不搭後語的教程,終於找到一個靠譜的

引入:
Spring Data是SpringSource基金會下的一個用於簡化資料庫訪問,並支援雲服務的開源框架。其主要目標是使得資料庫的訪問變得方便快捷,並支援map-reduce框架和雲端計算資料服務。對於擁有海量資料的專案,可以用Spring Data來簡化專案的開發。

然而針對不同的資料儲存訪問使用相對的類庫來操作訪問。Spring Data中已經為我們提供了很多業務中常用的一些介面和實現類來幫我們快速構建專案,比如分頁、排序、DAO一些常用的操作。

今天主要是對Spring Data下的JPA模組進行講解。

為什麼說Spring Data能幫助我們快速構建專案呢,因為Spring Data已經在資料庫訪問層上幫我們實現了公用功能了,而我們只需寫一個介面去繼承Spring Data提供給我們介面,便可實現對資料庫的訪問及操作,類似於spring-orm的TemplateDAO。

----------------------------------------------邪惡的分割-------------------------------------------------------

核心介面:
1 public interface Repository<T, ID extends Serializable> { 

2   

3 }


這個介面只是一個空的介面,目的是為了統一所有Repository的型別,其介面型別使用了泛型,泛型引數中T代表實體型別,ID則是實體中id的型別。

再來看一下Repository的直接子介面CrudRepository中的方法:
01 public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { 

02   

03     <S extends T> S save(S entity); 

04   

05     <S extends T> Iterable<S> save(Iterable<S> entities); 

06   

07     T findOne(ID id); 

08   

09     boolean exists(ID id); 

10   

11     Iterable<T> findAll(); 

12   

13     Iterable<T> findAll(Iterable<ID> ids); 

14   

15     long count(); 

16   

17     void delete(ID id); 

18   

19     void delete(T entity); 

20   

21     void delete(Iterable<? extends T> entities); 

22   

23     void deleteAll(); 

24 }

此介面中的方法大多是我們在訪問資料庫中常用的一些方法,如果我們要寫自己的DAO類的時候,只需定義個介面來整合它便可使用了。

再來看看Spring Data未我們提供分頁和排序的Repository的介面PagingAndSortingRepository:

1 public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { 

2   

3     Iterable<T> findAll(Sort sort); 

4   

5     Page<T> findAll(Pageable pageable); 

6 }

這些Repository都是spring-data-commons提供給我們的核心介面,spring-data-commons是Spring Data的核心包。這個介面中為我們提供了資料的分頁方法,以及排序方法。[b]看吧,spring-data讓我們省了很多心了,一切都按照這個規範進行構造,就連業務系統中常用到的一些操作都為我們考慮到了,而我們只需更用心的去關注業務邏輯層。[/b]spring-data將repository的顆粒度劃得很細,其實我覺得spring的框架中將每個類的顆粒度都劃得很細,這主要也是為了責任分離。

----------------------------------------------邪惡的分割線------------------------------------------------------

JPA實現:
針對spring-data-jpa又提供了一系列repository介面,其中有JpaRepository和JpaSpecificationExecutor,這2個介面又有什麼區別呢,我們分別來看看這2個介面的原始碼。

JpaRepository.class

01 public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> { 

02   

03     List<T> findAll(); 

04   

05     List<T> findAll(Sort sort); 

06   

07     <S extends T> List<S> save(Iterable<S> entities); 

08     void flush(); 

09   

10     T saveAndFlush(T entity); 

11   

12     void deleteInBatch(Iterable<T> entities); 

13   

14     void deleteAllInBatch();

這個類繼承自PagingAndSortingRepository,看其中的方法,可以看出裡面的方法都是一些簡單的操作,並未涉及到複雜的邏輯。當你在處理一些簡單的資料邏輯時,便可繼承此介面,看一個小例子吧。本文JPA供應者選擇的是Hibernate EntityManager,當然讀者們也可以選擇其他的JPA供應者,比如EclipseLink、OpenJPA,反正JPA是個標準,在無須修改的情況下便可移植。

先定義一使用者實體類User.class:

01 @Entity 

02 @Table( name = "spring_data_user" ) 

03 @PrimaryKeyJoinColumn( name = "id" ) 

04 public class User extends IdGenerator{ 

05   

06     private static final long serialVersionUID = 1L; 

07       

08     private String name; 

09     private String username; 

10     private String password; 

11     private String sex; 

12     private Date birth; 

13     private String address; 

14     private String zip; 

15           

16         //省略getter和setter 

17 }

Id生成策略是採用的表生成策略,這裡就不貼程式碼了,spring的配置檔案我也就不貼出來了,反正就那些東西,網上一查,遍地都是。後續我會在將demo附上來。

實體類是有了,現在得寫一個持久層,這樣才能操作資料庫啊,現在我們來看一下持久層。IUserDao.class:
1 @Repository("userDao") 

2 public interface IUserDao extends JpaRepository<User, Long>{}

再在spring的配置檔案中加上以下程式碼。

7   

8     <jpa:repositories base-package="org.tea.springdata.**.dao" /> 

9 </beans>

加上這段後Spring就會將指定包中@Repository的類註冊為bean,將bean託管給Spring。這樣定義完了就OK了!哦,就這樣就可以操作資料庫了?
是的,前面我就已經說了,Spring data已經幫我們寫好一個實現類了,而簡單的操作我們只須這樣繼承JpaRepository就可以做CRUD操作了。再寫個業務類來測試一把吧。由於我用的Cglib來動態代理,所以就不定義介面了,直接定義類UserService.class:

01 @Service("userService") 

02 public class UserService { 

03       

04     @Autowired

05     private IUserDao dao; 

06       

07     public void save(User user) { 

08         dao.save(user); 

09     } 

10   

11     public void delete(Long id) { 

12         dao.delete(id); 

13     } 

14   

15     public void update(User user) { 

16         dao.saveAndFlush(user); 

17     } 

18   

19     public List<User> findAll() { 

20         return dao.findAll(); 

21     } 

22 }

來寫一單元測試。

01 public class UserServiceTest { 

02       

03     private static ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); 

04       

05     private static UserService userService = (UserService) context.getBean("userService"); 

06       

07     public void saveUser() { 

08         StopWatch sw = new StopWatch(getClass().getSimpleName()); 

09         sw.start("Add a user information."); 

10         User u = new User(); 

11         u.setName("John"); 

12         u.setSex("Man"); 

13         u.setUsername("JohnZhang"); 

14         u.setPassword("123456"); 

15         u.setBirth(new Date()); 

16         userService.save(u); 

17         sw.stop(); 

18         System.err.println(sw.prettyPrint()); 

19     } 

20   

21      public static void main(String[] args) { 

22         UserServiceTest test = new UserServiceTest(); 

23         test.saveUser(); 

24     } 

25 }

綠了,高興了,測試通過!
額,都沒用Junit怎麼會綠呢,開個玩笑。
其餘繼承下來的操作方法,大家都可以自己測試一下,如沒意外,應該都會測試通過。

這只是spring data jpa簡單的使用,而往往在專案中這一點功能並不能滿足我們的需求。這是當然的,在業務中查詢是一件非常頭疼的事,畢竟不可能只是對一張表的查詢是吧? 其實在業務中往往會涉及到多張表的查詢,以及查詢時需要的各種條件。當然這不用擔心,畢竟這是對JPA的支援,而我們在用JPA原生態API的時候往往可能會把一些個方法寫得很凌亂,沒得一個具體的規範來寫自己的方法在後期維護上肯定會很困難。當然你自己也可以封裝一些方法來使用,而當我們使用到Spring Data JPA時,它已經幫助我們完成了這個方法的規範了。

來一起看一下複雜查詢時它為我們提供的介面。

JpaSpecificationExecutor.class

01 public interface JpaSpecificationExecutor<T> { 

02   

03     T findOne(Specification<T> spec); 

04   

05     List<T> findAll(Specification<T> spec); 

06   

07     Page<T> findAll(Specification<T> spec, Pageable pageable); 

08   

09     List<T> findAll(Specification<T> spec, Sort sort); 

10   

11     long count(Specification<T> spec); 

12 }

在這個接口裡面出現次數最多的類就是Specification.class,而這個類主要也就是圍繞Specification來打造的,Specification.class是Spring Data JPA提供的一個查詢規範,而你只需圍繞這個規範來設定你的查詢條件便可,我們來看一下Specification.class這個介面中有些什麼東西。

Specification.class

view sourceprint?1 public interface Specification<T> { 

2   

3     Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); 

4 }

只有一個方法toPredicate,而其中的引數大家並不陌生,都是JPA規範中的,ROOT查詢中的條件表示式、CriteriaQuery條件查詢設計器、CriteriaBuilder條件查詢構造器,而我們在使用複雜物件查詢時,實現該方法用JPA去構造物件查詢便可。

下面來看一個小例子:

1 @Repository("userDao") 

2 public interface IUserDao extends JpaSpecificationExecutor<User>{ 

3 }

仍然只是一個空介面,這次繼承的是JpaSpecificationExecutor了。
再寫一測試用例:查詢使用者表中name包含Sam的記錄,並分頁按照birth排倒序

 
01 public class UserDaoTest { 

02   

03     private static ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); 

04   

05     private static IUserDao userDao = (IUserDao) context.getBean("userDao"); 

06   

07     public void findBySpecAndPaginate() { 

08         Page<User> page = userDao.findAll(new Specification<User>() { 

09             @Override

10             public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { 

11                 root = query.from(User.class); 

12                 Path<String> nameExp = root.get("name"); 

13                 return cb.like(nameExp, "%Sam%"); 

14             } 

15   

16         }, new PageRequest(1, 5, new Sort(Direction.DESC, new String[] { "birth" }))); 

17   

18         StringBuilder stout = new StringBuilder(" 以下是姓名包含Sam人員資訊 : ").append("\n"); 

19         stout.append("| 序號 | username | password | name | sex | birth |").append("\n"); 

20         int sortIndex = 1; 

21         for (User u : page.getContent()) { 

22             stout.append(" | ").append(sortIndex); 

23             stout.append(" | ").append(u.getUsername()); 

24             stout.append(" | ").append(u.getPassword()); 

25             stout.append(" | ").append(u.getName()); 

26             stout.append(" | ").append(u.getSex()); 

27             stout.append(" | ").append(u.getBirth()); 

28             stout.append(" | \n"); 

29             sortIndex++; 

30         } 

31         System.err.println(stout); 

32     } 

33   

34     public static void main(String[] args) { 

35         UserDaoTest test = new UserDaoTest(); 

36         test.findBySpecAndPaginate(); 

37     } 

38 }

當然,這只是一個測試,很簡單的一個條件查詢方法。你也可以設計複雜的查詢來得到自己所需的結果,我這只是寫一個很簡單的方法來帶大家入門。

在來一個分割線

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

介面:Page<T> findAll(Specification<T> spec, Pageable pageable);
Page<User> findAll(name,age,pn,ps){
  dao.findAll(Specifications.where(getWhereClause(name,age)),
  new PageRequest(pn-1,ps));
}

private Specification<User> getWhereClause(name,age) {
  return new Specification<User>() {
    @Override
    public Predicate toPredicate(Root<User> r, CriteriaQuery<?> q, CriteriaBuilder cb) {
    Predicate predicate = cb.conjunction();
       if (StringUtils.isNotBlank(name)) {
         predicate.getExpressions().add(
           cb.like(r.<String>get("name"), "%" + StringUtils.trim(name) + "%")
         );
       }
       ...
       return predicate;
    }
  };
}
—————————————————————————————————————————————————————————————————————
最後,如過你需要使用JpaSpecificationExecutor,就必須要繼承一個Repository
@Repository
public interface PtpAdminlogDao extends JpaSpecificationExecutor<PtpAdminlog>,CrudRepository<PtpAdminlog,Integer>{
    
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————