Spring Data是SpringSource基金會下的一個用於簡化數據庫訪問,並支持雲服務的開源框架。其主要目標是使得數據庫的訪問變得方便快捷,並支持map-reduce框架和雲計算數據服務。對於擁有海量數據的項目,可以用Spring Data來簡化項目的開發。
然而針對不同的數據儲存訪問使用相對的類庫來操作訪問。Spring Data中已經爲我們提供了很多業務中常用的一些接口和實現類來幫我們快速構建項目,比如分頁、排序、DAO一些常用的操作。
下面總結一下使用 Spring Data JPA 進行持久層開發大致需要的三個步驟:
(1)聲明持久層的接口,該接口繼承 Repository,Repository 是一個標記型接口,它不包含任何方法,當然如果有需要,Spring Data 也提供了若干 Repository 子接口,其中定義了一些常用的增刪改查,以及分頁相關的方法。
(2)在接口中聲明需要的業務方法。Spring Data 將根據給定的策略(具體策略稍後講解)來爲其生成實現代碼。
(3)在 Spring 配置文件中增加一行聲明,讓 Spring 爲聲明的接口創建代理對象。配置了 後,Spring 初始化容器時將會掃描 base-package 指定的包目錄及其子目錄,爲繼承 Repository 或其子接口的接口創建代理對象,並將代理對象註冊爲 Spring Bean,業務層便可以通過 Spring 自動封裝的特性來直接使用該對象。
此外, 還提供了一些屬性和子標籤,便於做更細粒度的控制。可以在 內部使用 、 來過濾掉一些不希望被掃描到的接口。具體的使用方法見 Spring參考文檔。
應該繼承哪個接口?
前面提到,持久層接口繼承 Repository 並不是唯一選擇。Repository 接口是 Spring Data 的一個核心接口,它不提供任何方法,開發者需要在自己定義的接口中聲明需要的方法。與繼承 Repository 等價的一種方式,就是在持久層接口上使用 @RepositoryDefinition 註解,併爲其指定 domainClass 和 idClass 屬性。如下兩種方式是完全等價的:
public interface UserDao extends Repository<AccountInfo, Long> { …… }
@RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class)
public interface UserDao { …… }
如果持久層接口較多,且每一個接口都需要聲明相似的增刪改查方法,直接繼承 Repository 就顯得有些囉嗦,這時可以繼承 CrudRepository,它會自動爲域對象創建增刪改查方法,供業務層直接使用。開發者只是多寫了 “Crud” 四個字母,即刻便爲域對象提供了開箱即用的十個增刪改查方法。
但是,使用 CrudRepository 也有副作用,它可能暴露了你不希望暴露給業務層的方法。比如某些接口你只希望提供增加的操作而不希望提供刪除的方法。針對這種情況,開發者只能退回到 Repository 接口,然後到 CrudRepository 中把希望保留的方法聲明覆制到自定義的接口中即可。
分頁查詢和排序是持久層常用的功能,Spring Data 爲此提供了 PagingAndSortingRepository 接口,它繼承自 CrudRepository 接口,在 CrudRepository 基礎上新增了兩個與分頁有關的方法。但是,我們很少會將自定義的持久層接口直接繼承自 PagingAndSortingRepository,而是在繼承 Repository 或 CrudRepository 的基礎上,在自己聲明的方法參數列表最後增加一個 Pageable 或 Sort 類型的參數,用於指定分頁或排序信息即可,這比直接使用 PagingAndSortingRepository 提供了更大的靈活性。
JpaRepository 是繼承自 PagingAndSortingRepository 的針對 JPA 技術提供的接口,它在父接口的基礎上,提供了其他一些方法,比如 flush(),saveAndFlush(),deleteInBatch() 等。如果有這樣的需求,則可以繼承該接口。
上述四個接口,開發者到底該如何選擇?其實依據很簡單,根據具體的業務需求,選擇其中之一。筆者建議在通常情況下優先選擇 Repository 接口。因爲 Repository 接口已經能滿足日常需求,其他接口能做到的在 Repository 中也能做到,彼此之間並不存在功能強弱的問題。只是 Repository 需要顯示聲明需要的方法,而其他則可能已經提供了相關的方法,不需要再顯式聲明,但如果對 Spring Data JPA 不熟悉,別人在檢視代碼或者接手相關代碼時會有疑惑,他們不明白爲什麼明明在持久層接口中聲明瞭三個方法,而在業務層使用該接口時,卻發現有七八個方法可用,從這個角度而言,應該優先考慮使用 Repository 接口。
前面提到,Spring Data JPA 在後臺爲持久層接口創建代理對象時,會解析方法名字,並實現相應的功能。除了通過方法名字以外,它還可以通過如下兩種方式指定查詢語句:
Spring Data JPA 可以訪問 JPA 命名查詢語句。開發者只需要在定義命名查詢語句時,爲其指定一個符合給定格式的名字,Spring Data JPA 便會在創建代理對象時,使用該命名查詢語句來實現其功能。
開發者還可以直接在聲明的方法上面使用 @Query 註解,並提供一個查詢語句作爲參數,Spring Data JPA 在創建代理對象時,便以提供的查詢語句來實現其功能。
通過解析方法名創建查詢
框架在進行方法名解析時,會先把方法名多餘的前綴截取掉,比如 find、findBy、read、readBy、get、getBy,然後對剩下部分進行解析。並且如果方法的最後一個參數是 Sort 或者 Pageable 類型,也會提取相關的信息,以便按規則進行排序或者分頁查詢。
在創建查詢時,我們通過在方法名中使用屬性名稱來表達,比如 findByUserAddressZip ()。框架在解析該方法時,首先剔除 findBy,然後對剩下的屬性進行解析,詳細規則如下(此處假設該方法針對的域對象爲 AccountInfo 類型):
- 先判斷 userAddressZip (根據 POJO 規範,首字母變爲小寫,下同)是否爲 AccountInfo 的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,繼續第二步;
- 從右往左截取第一個大寫字母開頭的字符串(此處爲 Zip),然後檢查剩下的字符串是否爲 AccountInfo 的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,則重複第二步,繼續從右往左截取;最後假設 user 爲 AccountInfo 的一個屬性;
- 接着處理剩下部分( AddressZip ),先判斷 user 所對應的類型是否有 addressZip 屬性,如果有,則表示該方法最終是根據 “AccountInfo.user.addressZip” 的取值進行查詢;否則繼續按照步驟 2 的規則從右往左截取,最終表示根據 “AccountInfo.user.address.zip” 的值進行查詢。
可能會存在一種特殊情況,比如 AccountInfo 包含一個 user 的屬性,也有一個 userAddress 屬性,此時會存在混淆。讀者可以明確在屬性之間加上 “_” 以顯式表達意圖,比如 “findByUser_AddressZip()” 或者 “findByUserAddress_Zip()”。
在查詢時,通常需要同時根據多個屬性進行查詢,且查詢的條件也格式各樣(大於某個值、在某個範圍等等),Spring Data JPA 爲此提供了一些表達條件查詢的關鍵字,大致如下:
- And — 等價於 SQL 中的 and 關鍵字,比如 findByUsernameAndPassword(String user, Striang pwd);
- Or — 等價於 SQL 中的 or 關鍵字,比如 findByUsernameOrAddress(String user, String addr);
- Between — 等價於 SQL 中的 between 關鍵字,比如 findBySalaryBetween(int max, int min);
- LessThan — 等價於 SQL 中的 “<”,比如 findBySalaryLessThan(int max);
- GreaterThan — 等價於 SQL 中的”>”,比如 findBySalaryGreaterThan(int min);
- IsNull — 等價於 SQL 中的 “is null”,比如 findByUsernameIsNull();
- IsNotNull — 等價於 SQL 中的 “is not null”,比如 findByUsernameIsNotNull();
- NotNull — 與 IsNotNull 等價;
- Like — 等價於 SQL 中的 “like”,比如 findByUsernameLike(String user);
- NotLike — 等價於 SQL 中的 “not like”,比如 findByUsernameNotLike(String user);
- OrderBy — 等價於 SQL 中的 “order by”,比如 findByUsernameOrderBySalaryAsc(String user);
- Not — 等價於 SQL 中的 “! =”,比如 findByUsernameNot(String user);
- In — 等價於 SQL 中的 “in”,比如 findByUsernameIn(Collection userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數;
- NotIn — 等價於 SQL 中的 “not in”,比如 findByUsernameNotIn(Collection userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數;
下面看一個實例:
package com.szc.dao;
import com.szc.pojo.Employee;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.RepositoryDefinition;
import org.springframework.data.repository.query.Param;
import java.util.List;
/**
* 使用 Repository 接口
* Created by Administrator on 2018/2/2 0002.
*/
//@RepositoryDefinition(domainClass = Employee.class,idClass = Integer.class)
public interface EmployeeRepository extends Repository<Employee,Integer> {
//where name=?
Employee findByName(String name);
//where name=? and age=?
List<Employee> findByNameAndAge(String name,Integer age);
//where name like ?%
List<Employee> findByNameStartingWith(String start);
//where name like ?% and age <?
List<Employee> findByNameStartingWithAndAgeLessThan(String name,Integer age);
//where name in (?,?..) and age <=?
List<Employee> findByNameInOrAgeLessThanEqual(List<String> names,Integer age);
/**
* 自定義查詢 Employee 相當於 table(name="employee") 中定義的表名
* @return
*/
@Query("select o from Employee o where id=(select max(id) from Employee t1)")
Employee getEmployeeByMaxId();
/**
* 使用佔位符進行參數綁定
* */
@Query("select o from Employee o where o.name=?1 and o.age=?2 ")
List<Employee> listEmployeeByNameAndAge(String name,Integer age);
/**
* 使用命名參數激進型參數綁定
* */
@Query("select o from Employee o where o.name=:name and o.age=:age ")
List<Employee> listEmployeeByNameAndAge2(@Param("name") String name1, @Param("age") Integer age);
/**
* like 使用佔位符進行參數綁定
* */
@Query("select o from Employee o where name like %?1%")
List<Employee> likeEmployeeByLikeName(String name);
/**
* like 使用命名參數進行參數綁定
* */
@Query("select o from Employee o where name like :name%")
List<Employee> likeEmployeeByLikeName2(@Param("name") String name);
@Query(nativeQuery = true,value = "select count(1) from employee;")
long getCount();
}
下面是測試類
package com.szc.dao;
import com.szc.pojo.Employee;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
/**
* Unit test for simple App.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
public class EmployeeRepositoryTest {
private ApplicationContext applicationContext=null;
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void testBean(){
applicationContext=new ClassPathXmlApplicationContext("beans.xml");
DataSource dataSource=applicationContext.getBean(DataSource.class);
}
@Test
public void testEmployeeRepository(){
Employee employee=employeeRepository.findByName("zhangsan");
System.out.println(employee);
}
@Test
public void testFindByNameAndAge(){
List<Employee> list=employeeRepository.findByNameAndAge("zhangsan",20);
System.out.println(list);
}
@Test
public void testFindByNameStartingWith(){
List<Employee> list=employeeRepository.findByNameStartingWith("z");
System.out.println(list);
}
@Test
public void testFindByNameStartingWithAndAgeLessThan(){
List<Employee> list=employeeRepository.findByNameStartingWithAndAgeLessThan("z",16);
System.out.println(list);
}
@Test
public void testfindByNameInOrAgeLessThanEqual(){
List<String> list=new ArrayList<>();
list.add("zhangsan");
list.add("lishi");
list.add("zhangwu");
List<Employee> listEmployees=employeeRepository.findByNameInOrAgeLessThanEqual(list,16);
System.out.println(listEmployees);
}
@Test
public void testGetEmployeeByMaxId(){
Employee employee=employeeRepository.getEmployeeByMaxId();
System.out.println(employee);
}
@Test
public void testListEmployeeByNameAndAge(){
List<Employee> list=employeeRepository.listEmployeeByNameAndAge("zhangsan",20);
System.out.println(list);
}
@Test
public void testListEmployeeByNameAndAge2(){
List<Employee> list=employeeRepository.listEmployeeByNameAndAge2("zhangsan",20);
System.out.println(list);
}
@Test
public void testLikeEmployeeByLikeName(){
List<Employee> list=employeeRepository.likeEmployeeByLikeName("w");
System.out.println(list);
}
@Test
public void testLikeEmployeeByLikeName2(){
List<Employee> list=employeeRepository.likeEmployeeByLikeName2("w");
System.out.println(list);
}
@Test
public void testGetCount(){
long num=employeeRepository.getCount();
System.out.println(num);
}
}