天天看點

spring-data-jpa應用詳細總結

零、前言

前面jpa詳解 中也說了spring-data-jpa 秉承spring的優良傳統(簡化java開發),在jpa的基礎上進一步抽象簡化,下面就說說spring是如果簡化jpa的。

簡化思路:

我們平時開發持久化層(dao)時,都需要寫一個個接口,例如findByName(String name), findByAddress(String address),如果開發人員命名規範,我們即使不看實作内容,也能知道這個接口在幹什麼。

以此思路,spring-data-jpa的願景:

開發人員寫一個接口,按照規範命名接口名,然後就直接可以使用。

這就是spring-data-jpa設計人員的核心想法。

而要使用spring-data-jpa,需要下面幾個步驟:

  1. 添加依賴
  2. 配置
  3. 繼承Repository接口

一、添加依賴

<!-- spring test -->
	<dependency>  
	    <groupId>org.springframework</groupId>  
	    <artifactId>spring-test</artifactId>  
	    <version>${spring.version}</version>
	</dependency>
	
	<!-- spring data jpa -->
	<dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-jpa</artifactId>
		<version>1.9.6.RELEASE</version>
	</dependency>
           

小結:

  1. 使用spring-data-jpa,需要引入spring-data-jpa依賴包
  2. 使用spring的測試架構可以友善的完成本文所需要的demo示例

二、配置applicationContext.xml

使用spring的架構,就需要配置spring的預設配置檔案

<!-- 開啟注解事務 -->
<tx:annotation-driven transaction-manager="transactionManager" />

<!--使用spring data jpa的配置,需要在beans中 增加對jpa命名空間的引用-->
<jpa:repositories 
		base-package="com.yc"
		entity-manager-factory-ref="entityManagerFactory"
		transaction-manager-ref="transactionManager">
</jpa:repositories>

<!--實體管理器配置-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
</bean>

<!--事務管理器配置-->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
	<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
           

三個關鍵配置的關系如下:

spring-data-jpa應用詳細總結

小結:

  1. entityManagerFactory:配置負責管理資料庫連接配接以及實體類管理。上面是最簡化的配置,沒有寫實體管理器是如何關聯資料庫的,其實是預設去找classpath*:META-INF/persistence.xml這個檔案。entityManagerFactory提供了靈活的配置屬性,persistence.xml中的配置都可以寫到這裡。
  2. transactionManager:事務管理器。
  3. jpa:repositories是最關鍵的一個聲明配置,有了這句配置,spring就會到指定目錄下為繼承spring-data-jpa接口的接口建立代理對象。

三、繼承Repository接口

使用spring-data-jpa的方式就是讓持久層(dao)接口繼承Repository接口,以此來免去寫實作類的麻煩。

dao接口示例代碼如下:

public interface StudentDao extends Repository<Student, Integer> {
	
	List<Student> findByNameAndTel(String name, String tel);

}
           

小結:

  1. Repository接口需要使用泛型,第一個是該接口的域對象類型(也就是要查詢的表對應的實體類),第二個是該域對象的主鍵類型。
  2. 在dao接口中聲明方法,例如上面的findByNameAndTel,後面的參數要和方法名對應。
  3. 這樣一個隻有接口的dao層方法就可以使用了,實作的邏輯就是從Student表中按照name和tel查詢所有的資料。

四、測試

以上面的StudentDao為例,測試代碼如下:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration("/applicationContext.xml") 
public class SpringJpaTest {

	@Autowired 
    private StudentDao studentDao;
	
	@Test
	public void testMethod() {
		List<Student> list1 = studentDao.findByNameAndTel("xxx", "xxxx");
		System.out.println(list1.get(0).getId());
	}

}
           

小結:

  1. @ContextConfiguration表示加載配置檔案
  2. @Autowired注入dao層接口類
  3. 和直接使用junit一樣對@Test方法進行執行

五、詳解

上面是spring-data-jpa主推的文法,但也不僅限于這一種寫法,畢竟業務場景多樣,這一種寫法是無法完成複雜的業務邏輯的,下面詳細說明,spring-data-jpa支援的幾種書寫方式。

自定義接口名

Repository接口中沒有任何方法,資料庫操作的方式就是直接寫一個意圖明确的規範接口名,下面是示例代碼:

public interface StudentPepositoryDao extends Repository<Student, Integer> {

	/**
	 * 新增資料<p>
	 * 固定這麼寫,别想把save改成其他名字,架構還沒智能到可以猜你的心思;<p>
	 * 傳回值就是插入的對象類型(改成其他類型,雖然不影響插入資料,但會報格式轉換錯誤)
	 * @param stu
	 * @return
	 */
	Student save(Student stu);
	
	/**
	 * 批量新增資料<p>
	 * 固定寫法,不要把參數改成其他類型,如List<Student>也是會報錯的
	 * @param stuList
	 * @return
	 */
	Iterable<Student> save(Iterable<Student> stuList);
	
	/**
	 * 根據id删除對象<p>
	 * 删除接口沒有傳回值
	 * @param id
	 */
	void delete(Integer id);
	
	/**
	 * 根據對象删除對象
	 * @param stu
	 */
	void delete(Student stu);
	
	/**
	 * 批量删除<p>
	 * 還是固定寫法,不要把參數改成其他類型,如List<Student>也是會報錯的
	 * @param stuList
	 */
	void delete(Iterable<Student> stuList);
	
	//錯誤寫法
//	void deleteByName(String name);
	
	/**
	 * 查詢并排序
	 * @param sort
	 * @return
	 */
	List<Student> findAll(Sort sort);
	
	/**
	 * 分頁查詢
	 * @param pageable
	 * @return
	 */
	Page<Student> findAll(Pageable pageable);
	
	/**
	 * 普通分頁查詢
	 * @param pageable
	 * @return
	 */
	Page<Student> findByName(String name, Pageable pageable);
	
}
           

測試代碼:

@Autowired 
    private StudentPepositoryDao studentPepositoryDao;
	
	@Test
	public void testMethod() {
		List<Student> list1 = studentDao.findByNameAndTel("saveTest", "111");
		System.out.println(list1.get(0).getId());
	}
	
	@Test
	public void saveTest1() {
		Student sd = new Student();
		sd.setName("saveTest");
		sd.setTel("111");
		//神奇吧,隻用一個空借口就能插入資料,連事務注解都可以不寫
		Student saveEntity = studentPepositoryDao.save(sd);
		System.out.println("id=" + saveEntity.getId());
	}
	
	
	@Test
	public void deleteTest1() {
		studentPepositoryDao.delete(20);
	}
	
	/**
	 * 修改,用的還是save方法<p>
	 * 因為spring-data-jpa基于jpa,在jpa中就是通過修改托管态的值來達到同步修改資料庫的目的。
	 * 是以這裡也是同樣的道理,不要試圖new一個對象,然後自己添加一個id,然後去save
	 */
	@Test
	public void updateTest() {
		List<Student> list1 = studentDao.findByNameAndTel("saveTest2", "111-1");//托管态
		Student updateEntity = list1.get(0);
		updateEntity.setName("saveTest2-update");
		studentPepositoryDao.save(updateEntity);
		
	}
	
	/**
	 * 目的:根據字段名删除
	 * 這樣的寫法是會報錯的,因為spring-data-jpa沒有“内置”
	 */
	@Test
	public void deleteTest() {
//		studentPepositoryDao.deleteByName("saveTest2");
	}
	
	/**
	 * 簡單排序查詢
	 */
	@Test
	public void simpleSortFindTest() {
		Sort sort = new Sort(Sort.Direction.DESC,"id");
		List<Student> res = studentPepositoryDao.findAll(sort);
		for (Student std : res) {
			System.out.println(std.getId() + " -- " + std.getName());
		}
	}
           

小結:

使用Repository,有兩種方法:

  1. 固定寫法的方法,包括增(save)、删(delete)、改(save),查詢所有(findAll),删除所有(deleteAll),其他沒有列舉的就是不能随便寫的方法名
  2. 固定書寫規則的方法,就是各種個性化的查詢,根據規則寫方法名,就可以自定義查詢條件

使用擴充接口

spring-data-jpa還提供了一些其他接口可供使用。這些接口也都是繼承自Repository這個核心接口。

CrudRepository:繼承Repository,實作了一組CRUD相關的方法。

PagingAndSortingRepository:繼承CrudRepository,實作了一組分頁排序相關的方法。

JpaRepository:繼承PagingAndSortingRepository,實作一組JPA規範相關的方法。

JpaSpecificationExecutor:比較特殊,不屬于Repository體系,實作一組JPA Criteria查詢相關的方法。

CrudRepository

檢視CrudRepository接口源碼,可以看到包含一些常用的增删改查方法:

<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
           

是不是很眼熟————就是前面我們使用Repository寫的那些方法。

還有下面的PagingAndSortingRepository接口。

PagingAndSortingRepository

包含的方法:

//查詢所有實體類并排序
Iterable<T> findAll(Sort sort);
//分頁查詢
Page<T> findAll(Pageable pageable);
           

小結:

有人可能有疑問:既然其他接口功能這麼強大,Repository接口方法又是各種限制,方法名也不能亂寫,是不是Repository接口就沒有用處了? 當然不是,這裡有一個接口安全的問題:如果有一張表的資料是不能修改的,對應dao層接口還是繼承了CrudRepository,那麼該表的資料就有被篡改的風險。是以,需要對外暴露多少接口方法,還是看情況而定。

Jpql

jpal是jpa中的一個重要概念,可以靈活的控制一些複雜語句。在spring-data-jpa中也可以使用,完成一些純粹使用Repository接口無法完成的任務。比如 前面說到一個寫法deleteByName,是會報錯的,因為Repository無法領會我們的接口名的意義。Repository結合jpql就完成根據條件删除更新對象。

先看看基本寫法

public interface StudentJpqlDao extends Repository<Student, Integer> {

	@Query(value = "select d from Student d where d.name=:name")
	List<Student> getStudentByName(@Param("name")String name);
           

測試代碼

@Test
	public void simpleJpql() {
		List<Student> list = studentJpqlDao.getStudentByName("mary");
		for (Student std : list) {
			System.out.println(std.getId() + " -- " + std.getName());
		}
	}
           

删除、修改寫法如下

/**
	 * 兩個注解不能少:
	 * @Modifying
	 * @Transactional
	 * @param name
	 */
	@Transactional
	@Modifying
	@Query(value = "delete from Student d where d.name=:name")
	void deleteByName(@Param("name")String name);
	
	/**
	 * 删除、更新 兩個注解不能少:
	 * @Modifying
	 * @Transactional
	 */
	@Transactional
	@Modifying
	@Query(value = "update Student d set d.name=:name where d.id=:id")
	void updateByName(@Param("id")Integer id, @Param("name")String name);
           

小結:

在實作Repository(或其子接口)中,使用注解@Query,在注解value屬性中寫正常的jpql語句。需要注意的動态綁定參數的兩種方式:

  1. 序号型
@Query(value = "select d from Student d where d.name=?1 and d.tel=?2")
           

? + 數字

,表示參數清單裡的 第一個參數、第二個參數

2. 參數名綁定

也就是上面執行個體所示的,使用

: + 參數名

。需要注意的是參數清單裡對應的要用注解@Param(“參數名”)來配合使用,一般推薦這種寫法,不容易因為改動參數而綁定錯誤。

3. 删除、修改需要增加注解@Modifying和@Transactional

4. 就像之前說jpql一樣,jpql在spring-data-jpa裡也無法完成新增功能

靜态jpql

在講jpa時也就說了,可以在實體類上用注解@NamedQuery寫“全局通用”的查詢方法,在spring-data-jpa這裡也是一樣的。

就像我們寫程式不建議大量使用全局變量一樣,這裡也不建議用這種方式,因為會讓實體類承載過多内容,可能因為命名沖突引起不易排查的bug。

建立查詢順序

既然有這麼多規則,那麼當我們寫了一個接口時,spring-data-jpa是如何判斷我們用的是什麼規則。

Spring Data JPA 在為接口建立代理對象時,如果發現同時存在多種上述情況可用,它該優先采用哪種政策呢?為此,jpa:repositories 提供了 query-lookup-strategy 屬性,用以指定查找的順序。它有如下三個取值:

  1. create — 通過解析方法名字來建立查詢。即使有符合的命名查詢,或者方法通過 @Query 指定的查詢語句,都将會被忽略。
  2. create-if-not-found — 如果方法通過 @Query 指定了查詢語句,則使用該語句實作查詢;如果沒有,則查找是否定義了符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則通過解析方法名字來建立查詢。這是 query-lookup-strategy 屬性的預設值。
  3. use-declared-query — 如果方法通過 @Query 指定了查詢語句,則使用該語句實作查詢;如果沒有,則查找是否定義了符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則抛出異常。

六、應用總結

和原生jpa相比,除了省去很多重複的代碼工作量,spring-data-jpa的“面向接口程式設計”還具有代碼集中易維護的優勢,層級分明,一旦出現某個共性錯誤,集中改動也友善。不像原生jpa(或hibernate),servies層穿插各種資料庫操作語句。是以個人建議大部分資料庫操作使用spring-data-jpa,有特殊的複雜業務場景才去使用原生jpa方法(比如需要精細控制事務送出)。不到萬不得已不去用原生sql,因為既然我們用orm架構就是獲得了orm屏蔽資料庫的優勢,一旦使用了原生sql,就失去了這一大優勢。 而且一個項目如果使用的技術過于雜,也增加了其他人的維護難度和學習成本。

七、示例代碼下載下傳

碼雲

八、參考連結

Hibernate和Spring Data JPA的差別

使用Spring Data JPA 簡化JPA開發

一步步實作:JPA的基本增删改查CRUD(jpa基于hibernate)

JPA簡單使用

易百教程

JPA和Hibernate的關系

JPA之常用 基本注解

JPA EntityManager的四個主要方法 ——persist,merge,refresh和remove

Spring Data Jpa 實體狀态分析

Spring Transaction 使用

JPA和事務管理

事務屬性之7種傳播行為

JPA緩存

Spring中Transactional注解

JAP JPQL相關文法

詳解JPA 2.0動态查詢機制:Criteria API