Spring Boot系列——Spring Data JPA(超全)

網上關於Spring Data JPA的博文有很多,但都是零零散散的,所以就寫了這麼一篇文章,也作爲自己學習的總結吧。本文涉及的所有代碼可以查看github

1. Spring Data JPA、JPA和Hibernate的關係

關於這三者的關係網上已經有很多解釋了,我就簡單說一下吧。JPA是一套規範(提供統一的接口和抽象類),Hibernate正是實現JPA規範的優秀ORM框架之一,而Spring Data JPA進一步對Hibernate進行了封裝,是Spring提供的一套簡化的 JPA 開發的框架,使其操作起來更簡單。所以Spring Data JPA的提供商是Hibernate,即幹活的其實是Hibernate

2. 相關依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

3. 配置數據庫連接和JPA

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: validate #等同於hibernate.hbm2ddl.auto
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect  #配置數據庫的方言,因爲不同的數據庫有不同的語法
    open-in-view: true #對hibernate來說ToMany關係默認是延遲加載,而ToOne關係則默認是立即加載;而在mvc的controller中脫離了persisent contenxt,於是entity變成了detached狀態,這個時候要使用延遲加載的屬性時就會拋出LazyInitializationException異常,而Open Session In View指在解決這個問題
    show-sql: true #在控制檯中打印sql語句

上面的配置中需要單獨說一下 spring.jpa.hibernate.ddl-auto=create這個配置選項。

這個屬性常用的選項有五種:

  1. create:每次重新啓動項目都會重新創新表結構,會導致數據丟失
  2. create-drop:每次啓動項目時創建表結構,關閉項目時刪除表結構
  3. update:每次啓動項目會更新表結構,如果表存在只是更新而不是重新創建
  4. validate:驗證表結構,不對數據庫進行任何更改
  5. none:不使用Hibernate Auto DDL功能,💡在生產環境中最好使用這個

但是,一定要不要在生產環境使用 ddl 自動生成表結構,一般推薦手寫 SQL 語句配合 Flyway 來做這些事情。

4. 創建數據表

/*創建客戶表*/
    CREATE TABLE cst_customer (
      cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客戶編號(主鍵)',
      cust_name varchar(32) NOT NULL COMMENT '客戶名稱(公司名稱)',
      cust_source varchar(32) DEFAULT NULL COMMENT '客戶信息來源',
      cust_industry varchar(32) DEFAULT NULL COMMENT '客戶所屬行業',
      cust_level varchar(32) DEFAULT NULL COMMENT '客戶級別',
      cust_address varchar(128) DEFAULT NULL COMMENT '客戶聯繫地址',
      cust_phone varchar(64) DEFAULT NULL COMMENT '客戶聯繫電話',
      PRIMARY KEY (`cust_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

5. 創建實體類

/**
 * 實體類 
 * 1.主要建立實體類和數據表之間的映射關係:
 *   @Entity 指明當前類爲實體類
 *   @Table 指定實體類和哪個數據表建立映射關係
 * 
 * 2.建立實體類成員變量和數據表字段之間的映射關係
 *   @Id 聲明當前成員變量對應數據表中的主鍵
 *   @GeneratedValue 指定主鍵的生成策略
 *   @column 指明當前成員變量具體和數據表中哪個字段建立映射關係 以上註解都來自javax.persistence包
 */
@Entity
@Table(name = "tb_customer")
public class Customer {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "cust_id")
	private Long custId;

	@Column(name = "cust_name")
	private String custName;

	@Column(name = "cust_source")
	private String custSource;

	@Column(name = "cust_industry")
	private String custIndustry;

	@Column(name = "cust_level")
	private String custLevel;

	@Column(name = "cust_address")
	private String custAddress;

	@Column(name = "cust_phone")
	private String custPhone;
    
    //省略getter和setter

對於上面的註解,💡重點說一下@GeneratedValue的生成策略:

基於annotationhibernate主鍵標識爲@Id, 其生成規則由@GeneratedValue設定。這裏的@id@GeneratedValue都是JPA的標準用法。

@GeneratedValue JPA提供的四種標準用法爲TABLE,SEQUENCE,IDENTITY,AUTO

1️⃣IDENTITY

主鍵由數據庫自動生成(主要是自動增長型,即自增主鍵,mysql支持,oracle不支持)

@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY) 
private Long custId;

2️⃣ ​SEQUENCE

根據底層數據庫的序列來生成主鍵,條件是數據庫支持序列(mysql不支持,oracle支持)

@Id  
@GeneratedValue(strategy = GenerationType.SEQUENCE,generator="payablemoney_seq")  
@SequenceGenerator(name="payablemoney_seq", sequenceName="seq_payment")  
private Long custId;

//@SequenceGenerator源碼中的定義
@Target({TYPE, METHOD, FIELD})   
@Retention(RUNTIME)  
public @interface SequenceGenerator {  
   //表示該表主鍵生成策略的名稱,它被引用在@GeneratedValue中設置的“generator”值中
   String name();  
   //屬性表示生成策略用到的數據庫序列名稱。
   String sequenceName() default "";  
   //表示主鍵初識值,默認爲0
   int initialValue() default 0;  
   //表示每次主鍵值增加的大小,例如設置1,則表示每次插入新記錄後自動加1,默認爲50
   int allocationSize() default 50;  
 }

💡其他JPA中的註解可以看看JPA常用註解這篇文章

6. 創建操作數據庫的Dao接口

Spring Data JPAspring提供的一款對於數據訪問層(Dao層)的框架,使用Spring Data JPA,只需要按照框架的規範提供dao接口,不需要實現類就可以完成數據庫的增刪改查、分頁查詢等方法的定義,極大的簡化了我們的開發過程。只要定義一個dao接口繼承JpaRepositoryJpaSpecificationExecutor接口就行,等到調用的時候會通過動態代理實現相對應的接口。

1️⃣JpaResponse<所操作的實體類的類型,相對應主鍵的類型>​,主要實現對數據庫的增刪改查。

2️⃣​JpaSpecificationExecutor<所操作的實體類的類型>,主要用於複雜查詢,比如分頁。

public interface CustomerDao extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {

}

6.1 JPA自帶方法實戰

1.保存用戶到數據庫

Customer customer = new Customer();
customer.setCustName("張三");
customerDao.save(customer);  //<S extends T> S save(S entity); T爲實體類類型

2.更新用戶

更新操作也要通過 save()方法來實現,比如:

Customer customer = new Customer();
customer.setCustId(7L);
//更新客戶名字
customer.setCustName("李四");
customerDao.save(customer); //按customer對象進行所有字段的更新

這裏的save()方法,先會到數據庫中查詢是否有這個id的客戶,如果有就進行更新操作,如果沒有則進行保存操作。

3.根據 id 查找用戶

Optional<Customer> optional = customerDao.findById(2L);
Customer customer = optional.get();
System.out.println(customer);

❌在老的API中,可以通過T findOne(ID id)方法進行查詢一個的操作,但是最新的API對這進行了更改,變成了<S extends T> Optional<S> findOne(Example<S> example);,大家務必要注意。

💡除了findById()可以查詢一個,方法T getOne(ID id)也可以實現這個功能,但是這個方法採用的是延遲加載(得到的是一個代理對象,而不是實體對象本身,所以如果沒有查詢到該記錄就會拋出異常。並且容易拋出LazyInitializationException異常,可以加上@Transaction來解決。因爲這個方法的複雜性,所以該方法儘量少用)。而findById()採用的是立即加載,得到的是一個對象,如果沒有查詢到該記錄,返回null

4.根據 id 刪除用戶

customerDao.deleteById(3L);  //void deleteById(ID id);
//或者
Customer customer = new Customer();
customer.setCustId(3L);
customerDao.delete(customer);   //void delete(T entity);
//上面兩種方法本質都是通過主鍵刪除

5.查詢所有

List<Customer> findAll = customerDao.findAll();

7. 使用JPQL查詢

使用Spring Data JPA提供的查詢方法已經可以解決大部分的應用場景,但是對於某些業務來說,我們還需要靈活的構造查詢條件,這時就可以使用@Query註解,結合JPQL的語句方式完成查詢。JPQL與原生SQL語句類似,並且完全面向對象,通過類名和屬性訪問,而不是表名和表的屬性,這種語言編寫的查詢語句具有可移植性,能編譯成多個主流數據庫使用的SQL

💡JPQL語句支持兩種方式的參數定義方式: 命名參數和位置參數。

public interface CustomerDao extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {

	//使用位置參數,?後面的數字是參數的索引(從1開始)
	@Query("select c from Customer c where c.custName=?1")
	List<Customer> queryByName(String custName);

	//使用命名參數,方式爲":+參數名",當然也可以不使用@Param標識
    //@Query("from Customer where custName=:custName") 這個也可以
	@Query("select c from Customer c where c.custName=:custName")
	List<Customer> queryByName2(@Param("custName") String custName);
    
    // 當然也可以不使用@Param標識,但是不可以位置參數和命名參數同時混合用
	@Query("select c from Customer c where c.custName=:custName and c.custLevel=:custLevel")
	List<Customer> queryByNameAndLevel(String custName, String custLevel);
}

對於更新和刪除操作,需要多添加一個@Modifying註解

// 更新操作,必須加上@Modifying註解,而且只能返回void或int/Integer類型的數據,如果爲int代表影響的行數
@Query("update Customer set custName=:name where id=:id")
@Modifying
int updateById(Long id, String name);

//同時更新和刪除的測試用例也有些差別
/**
 * 測試更新,執行update或delete操作必須在事務中,所以必須加上@Transactional註解
 * 並且因爲在測試中事務默認是回滾的,所以這個測試不會更改數據庫的數據,可以加上@Rollback(false)避免回滾
 */
@Test
@Transactional
@Rollback(false)
public void testUpdateById() {
	int effectedNum = customerDao.updateById(1L, "李四");
	System.out.println(effectedNum);
}

其他關於JPQL的操作可以查看jpql的學習

8. 使用SQL查詢

Spring Data Jpa不僅支持JPQL查詢,還支持原生的SQL查詢。同樣在Dao的自定義方法上面加上@Query聲明。

/**
 * JPQL操作的是對象和屬性,而SQL操作的是表和字段List<Customer>
 * 
 * @Query 註解中value賦值JPQL或SQL 
 *		  nativeQuery: false 表示不使用本地查詢,即使用JPQL 
 *					   true  表示使用本地查詢,即使用SQL
 *  從下面的返回值我們可以看到,返回的對象只能拆分成一個個屬性保存在數組中 
 */
@Query(value = "select * from cst_customer where cust_name=:name", nativeQuery = true)
List<Object[]> queryAll(String name);

// 測試使用原生SQL
@Test
public void testQueryAll() {
	List<Object[]> queryAll = customerDao.queryAll("李四");
	for (Object[] obj : queryAll) {
		System.out.println(Arrays.toString(obj));
	}
}

//同樣也支持分頁,具體使用方法見下面
@Query(value = "select * from cst_customer", nativeQuery = true)
Page<Customer> queryAllAndPage(Pageable pageable);

💡和JPQL一樣,更新和刪除操作必須加上@Modifying註解,並且如果一個方法調用聲明瞭@Modifying註解的方法,那麼該方法必須加上事務。

9. 方法名命名規則查詢

方法命名規則查詢就是根據方法的名字(約定命名規範),就能創建查詢,本質是對JPQL語句的進一步封裝,會自動生成JPQL語句,JPQL在運行時編譯成SQL。只需要按照Spring Data JPA提供的方法命名規則定義方法的名稱,就可以完成查詢工作。Spring Data JPA在程序執行的時候會根據方法名稱進行解析,並自動生成查詢語句進行查詢。

public interface CustomerDao extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
	/**
	 * 比如這裏通過客戶名稱和客戶等級來查詢,約定使用findBy開頭的命名方法
	 * 後面接上相對應的屬性名稱(首字母大寫),並且注意參數的順序必須和方法名中一致(但是具體形參名字可任意) 下面等同於JPQL @Query("from Customer where custName=?1 and custLevel=?2")
	 * 命名規則爲:find+全局修飾+By+實體屬性名稱+限定詞+連接詞+(其他實體屬性)+OrderBy+排序屬性+排序方向
	 */
	List<Customer> findByCustNameAndCustLevel(String custName, String level);
}

// 等同於JPQL:from Customer where custId in(?1) and custName like ?2
List<Customer> findByCustIdInAndCustNameLike(List<Long> ids, String name);

💡也可以使用方法命名實現分頁查詢

//在Dao接口中定義方法
// 分頁+in 查詢
Page<Customer> findByCustIdIn(Collection<Long> ids, Pageable pageable);

//測試
// 分頁查詢+in
@Test
public void testInAndPageable() {
	Long[] arr = new Long[] { 1L, 2L, 3L, 4L, 5L, 6L, 7L };
	List<Long> ids = Arrays.asList(arr);
	int pageNum = 0;
	int pageSize = 2;
	Pageable pageable = PageRequest.of(pageNum, pageSize);
	Page<Customer> page = customerDao.findByCustIdIn(ids, pageable);
}

命名規範如下:(大概爲findBy+屬性名findBy+屬性名+查詢方式)

Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

10. Specification動態查詢

有時我們在查詢某個實體的時候,給定的條件是不固定的,這時就需要動態構建相應的查詢語句,在Spring Data JPA中可以通過JpaSpecificationExecutor接口查詢。相比JPQL,其優勢是類型安全,更加的面向對象。對於JpaSpecificationExecutor,這個接口基本是圍繞着Specification接口來定義的。我們可以簡單的理解爲,Specification構造的就是查詢條件。

💡其中​Specification接口中有一個方法,只要重寫這個方法就可以構造出查詢條件

 //構造查詢條件
    /**
    *	Root	        :Root接口,代表查詢的根對象,可以通過root獲取實體中的屬性
    *	CriteriaQuery	:代表一個頂層查詢對象,用來自定義查詢
    *	CriteriaBuilder	:用來構建查詢,此對象裏有很多條件方法,比如like(模糊查詢),equal(精確查詢)     *                     等構造條件的方法,也有組合條件的方法and()、or()
    **/
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
條件查詢

下面我們將展示如何實現單個條件的查詢:

/**
 * 因爲我們的Dao接口繼承了JpaSpecificationExecutor接口,所以findOne,findAll,count這幾個方法都可以直接使用
 */
@Test
public void testFindAll() {
	// 使用匿名內部類重寫Specification接口中的方法,構造查詢條件
    // 其中Specification需要提供泛型,是實體類的類型
	Specification<Customer> spec = new Specification<Customer>() {
		// 重寫toPredicate方法,構造出查詢條件
		@Override
		public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
			// 獲取對象屬性路徑
			Path<Object> path = root.get("custName");
			// 類型轉換,Expression爲Path的父接口,參數爲屬性類型字節碼
			Expression<String> as = path.as(String.class);
			// 精確匹配名字,獲取查新條件,第一個參數爲需要比較的屬性,第二個參數爲需要比較的值
			Predicate predicate = cb.equal(as, "李四");
			return predicate;
			//上面的操作等同於 return cb.equal(root.get("custName").as(String.class), "李四");
		}
	};
	List<Customer> customers = customerDao.findAll(spec);
	System.out.println(customers);
}

也可以同時組合幾個條件進行查詢:

@Test
public void testFindOne() {
	// 使用匿名內部類重寫Specification接口中的方法,構造查詢條件
	Specification<Customer> spec = new Specification<Customer>() {
		@Override
		public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> newQuery, CriteriaBuilder cb) {
			//構造第一個查詢條件
			Predicate p=cb.like(root.get("custName").as(String.class),"李四%");
			//構造第二個查詢條件,並且和第一個進行組合(and()爲與,or()爲或)
			p=cb.and(p,cb.equal(root.get("custLevel").as(String.class), "2"));
			return p;
		}
	};
	Optional<Customer> optional = customerDao.findOne(spec);
	System.out.println(optional.get());
}

⭕️構造跟SQL語句中in匹配相類似的查詢條件和上面方法有些區別,具體如下:

/**
 * Expression<T>接口中方法 Predicate in(Expression<Collection<?>>
 * values);可以實現使用in構造條件
 */
@Test
public void testFindAllIn() {
	Long[] arr = new Long[] { 1L, 2L, 3L, 4L };
	List<Long> idList = Arrays.asList(arr);
	List<Customer> customers = customerDao.findAll(new Specification<Customer>() {
		@Override
		public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> newQuery, CriteriaBuilder cb) {
            //只需要使用Root<Customer> root這一個參數
			Predicate predicate = root.get("custId").as(Long.class).in(idList);
			return predicate;
		}
	});
	System.out.println(customers);
}
條件+排序查詢
/**
 * 條件+排序查詢
 */
@Test
public void testFindAllSort() {
	// 構造查詢條件
	Specification<Customer> spec = new Specification<Customer>() {
		@Override
		public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> newQuery, CriteriaBuilder cb) {
			return cb.like(root.get("custName").as(String.class), "李四%");
		}
	};
	// 構造排序 第一個參數:排序規則 Sort.Direction.DESC(降序) Sort.Direction.ASC(升序) 第二個參數:按哪個屬性名排序,可以是多個,第一個屬性值相同按第二個再排序(排序規則依然是第一個)
	Sort sort = new Sort(Sort.Direction.DESC, "custLevel", "custId");
	List<Customer> customers = customerDao.findAll(spec, sort);
	for (Customer customer : customers) {
		System.out.println(customer);
	}
}
條件+分頁查詢
/**
 * 條件+分頁查詢
 */
@Test
public void testFindAllPageable() {
	// 代表沒有條件限制
	Specification<Customer> spec = null;
	int pageNum = 0;
	int pageSize = 3;
	// 使用PageRequest實現Pageable接口,第一個參數爲頁碼(從0開始),第二個參數爲每頁的數量,這裏使用的是無排序,所以無須第三個參數
	Pageable pageable = PageRequest.of(pageNum, pageSize);
	Page<Customer> page = customerDao.findAll(spec, pageable);
	// 獲取整頁的數據
	List<Customer> customers = page.getContent();
	// 獲取下一頁的頁碼,注意頁碼從0開始算
	int pageNumber = page.nextPageable().getPageNumber();
	// 獲取總條數
	long totalElements = page.getTotalElements();
	// 獲取總頁數
	int totalPages = page.getTotalPages();
}

本文涉及的所有代碼可以查看github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章