文章目錄
網上關於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
這個配置選項。
這個屬性常用的選項有五種:
create
:每次重新啓動項目都會重新創新表結構,會導致數據丟失create-drop
:每次啓動項目時創建表結構,關閉項目時刪除表結構update
:每次啓動項目會更新表結構,如果表存在只是更新而不是重新創建validate
:驗證表結構,不對數據庫進行任何更改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
的生成策略:
基於annotation
的hibernate
主鍵標識爲@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 JPA
是spring
提供的一款對於數據訪問層(Dao
層)的框架,使用Spring Data JPA
,只需要按照框架的規範提供dao
接口,不需要實現類就可以完成數據庫的增刪改查、分頁查詢等方法的定義,極大的簡化了我們的開發過程。只要定義一個dao
接口繼承JpaRepository
和JpaSpecificationExecutor
接口就行,等到調用的時候會通過動態代理實現相對應的接口。
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