Spring Data JPA 是在 JPA 規范的基礎上進行進一步封裝的產物,和之前的 JDBC、slf4j 這些一樣,只定義了一系列的接口。具體在使用的過程中,一般接入的是 Hibernate 的實現,那么具體的 Spring Data JPA 可以看做是一個面向對象的 ORM。雖然后端實現是 Hibernate,但是實際配置和使用比 Hibernate 簡單不少,可以快速上手。如果業務不太復雜,個人覺得是要比 Mybatis 更簡單好用。
本文就簡單列一下具體的知識點,詳細的用法可以見參考文獻中的博客。本文具體會涉及到 JPA 的一般用法、事務以及對應 Hibernate 需要掌握的點。
基本使用
- 創建項目,選擇相應的依賴。一般不直接用 mysql 驅動,而選擇連接池。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version>
</dependency>
- 配置全局 yml 文件。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.21.30.61:3306/gpucluster?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username:
password:
jpa:
hibernate:
ddl-auto: update
open-in-view: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
show_sql: false
format_sql: true
logging:
level:
root: info # 是否需要開啟 sql 參數日志
org.springframework.orm.jpa: DEBUG
org.springframework.transaction: DEBUG
org.hibernate.engine.QueryParameters: debug
org.hibernate.engine.query.HQLQueryPlan: debug
org.hibernate.type.descriptor.sql.BasicBinder: trace
hibernate.ddl-auto: update
實體類中的修改會同步到數據庫表結構中,慎用。show_sql
可開啟 hibernate 生成的 sql,方便調試。logging
下的幾個參數用於顯示 sql 的參數。
- 創建實體類並添加 JPA 注解
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String address;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
- 創建對應的 Repository
實現 JpaRepository 接口,生成基本的 CRUD 操作樣板代碼。並且可根據 Spring Data JPA 自帶的 Query Lookup Strategies 創建簡單的查詢操作,在 IDEA 中輸入 findBy
等會有提示。
public interface IUserRepository extends JpaRepository<User,Long> {
List<User> findByName(String name);
List<User> findByAgeAndCreateTimeBetween(Integer age, LocalDateTime createTime, LocalDateTime createTime2);
}
查詢
默認方法
Repository 繼承了 JpaRepository
后會有一系列基本的默認 CRUD 方法,例如:
List<T> findAll();
Page<T> findAll(Pageable pageable);
T getOne(ID id);
T S save(T entity);
void deleteById(ID id);
聲明式查詢
Repository 繼承了 JpaRepository
后,可在接口中定義一系列方法,它們一般以 findBy
、countBy
、deleteBy
、existsBy
等開頭,如果使用 IDEA,輸入以下關鍵字后會有響應的提示。例如:
public interface IUserRepository extends JpaRepository<User,Integer>{
User findByUsername(String username);
Integer countByDept(String dept);
}
對於一些單表多字段查詢,使用這種方式就非常舒服了,而且完全 oop 思想,不需要思考具體的 SQL 怎么寫。但有個問題,字段多了之后方法名會很長,調用的時候會比較難受,這個時候可以利用 jdk8 的特性將它縮短,當然這種情況也可以直接用 @Query
寫 HQL 或 SQL 解決。
User findFirstByEmailContainsIgnoreCaseAndField1NotNullAndField2NotNull(final String email);
default User getByEmail(final String email) {
return findFirstByEmailContainsIgnoreCaseAndField1NotNullAndField2NotNull(email);
}
常見的操作可見 [附錄 - 支持的方法關鍵詞](### 支持的方法關鍵詞)
使用注解和 SQL
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Long> {
@Query(nativeQuery = true, value = "select * from user where tel = ?1")
List<User> getUser(String tel);
@Modifying
@Transactional
@Query("delete from User u where u.active = false")
void deleteInactiveUsers();
}
@Query
中可寫 HQL 和 SQL,如果是 SQL,則nativeQuery = true
。
復雜查詢 Specification
// 復雜查詢,創建 Specification
private Page<OrderInfoEntity> getOrderInfoListByConditions(String tel, int pageSize, int pageNo, String beginTime, String endTime) {
Specification<OrderInfoEntity> specification = new Specification<OrderInfoEntity>() {
@Override
public Predicate toPredicate(Root<OrderInfoEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicate = new ArrayList<>();
if (!Strings.isNullOrEmpty(beginTime)) {
predicate.add(cb.greaterThanOrEqualTo(root.get("createTime"), DateUtils.getDateFromTimestamp(beginTime)));
}
if (!Strings.isNullOrEmpty(endTime)) {
predicate.add(cb.lessThanOrEqualTo(root.get("createTime"), DateUtils.getDateFromTimestamp(endTime)));
}
if (!Strings.isNullOrEmpty(tel)) {
predicate.add(cb.equal(root.get("userTel"), tel));
}
return cb.and(predicate.toArray(new Predicate[predicate.size()]));
}
};
Sort sort = new Sort(Sort.Direction.DESC, "createTime");
Pageable pageable = new PageRequest(pageNo - 1, pageSize, sort);
return orderInfoRepository.findAllEntities(specification, pageable);
}
子查詢
Specification<UserProject> specification = (root, criteriaQuery, criteriaBuilder) -> {
Subquery subQuery = criteriaQuery.subquery(String.class);
Root from = subQuery.from(User.class);
subQuery.select(from.get("userId")).where(criteriaBuilder.equal(from.get("username"), "mqy6289"));
return criteriaBuilder.and(root.get("userId").in(subQuery));
};
return userProjectRepository.findAll(specification);
刪除和修改
- 刪除
- 直接使用默認的
deleteById()
。 - 使用申明式查詢創建對應的刪除方法
deleteByXxx
。 - 使用 SQL\HQL 注解刪除
- 新增和修改
調用 save 方法,如果是修改的需要先查出相應的對象,再修改相應的屬性。
事務
Spring Boot 默認集成事務,所以無須手動開啟使用 @EnableTransactionManagement
注解,就可以用 @Transactional
注解進行事務管理。需要使用時,可以查具體的參數。
@Transactional
注解的使用,具體可參考:透徹的掌握 Spring 中 @transactional 的使用。
談談幾點用法上的總結:
- 持久層方法上繼承
JpaRepository
,對應實現類SimpleJpaRepository
中包含@Transactional(readOnly = true)
注解,因此默認持久層中的 CRUD 方法均添加了事務。 - 申明式事務更常用的是在 service 層中的方法上,一般會調用多個 Repository 來完成一項業務邏輯,過程中可能會對多張數據表進行操作,出現異常一般需要級聯回滾。一般操作,直接在 Serivce 層方法中添加
@Transactional
即可,默認使用數據的隔離級別,默認所有 Repository 方法加入 Service 層中的事務。 @Transactional
注解中最核心的兩個參數是propagation
和isolation
。前者用於控制事務的傳播行為,指定小事務加入大事務還是所有事務均單獨運行等;后者用於控制事務的隔離級別,默認和 MySQL 保持一致,為不可重復讀。我們也可以通過這個字段手動修改單個事務的隔離級別。具體的應用場景可見我另一篇博客 談談事務的隔離性及在開發中的應用。- 同一個 service 層中的方法調用,如果添加了
@Transactional
會啟動 hibernate 的一級緩存,相同的查詢多次執行會進行 Session 層的緩存,否則,多次相同的查詢作為事務獨立執行,則無法緩存。 - 如果你使用了關系注解,在懶加載的過程中一般都會遇到過
LazyInitializationException
這個問題,可通過添加@Transactional
,將 session 托管給 Spring Transaction 解決。 - 只讀事務的使用。可在 service 層中全局配置只讀事務
@Transactional(readOnly =true)
,對於具有讀寫的事務可在對應方法中覆蓋即可。在只讀事務無法進行寫入操作,這樣在事務提交前,hibernate 就會跳過 dirty check,並且 Spring 和 JDBC 會有多種的優化,使得查詢更有效率。
JPA Audit
JPA 自帶的 Audit 可以通過 AOP 的形式注入,在持久化操作的過程中添加創建和更新的時間等信息。具體使用方法:
- 申明實體類,需要在類上加上注解 @EntityListeners(AuditingEntityListener.class)。
- 在 Application 啟動類中加上注解 @EnableJpaAuditing
- 在需要的字段上加上 @CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy 等注解。
如果只需要更新創建和更新的時間是不需要額外的配置的。
數據庫關系
如果需要進行級聯查詢,可用 JPA 的 @OneToMany、@ManyToMany 和 @OneToOne 來修飾,當然,碰到出現一對多等情況的時候,可以手動將多的一方的數據去查詢出來填充進去。
由於數據庫設計的不同,注解在使用上也會存在不同。這里舉一個 OneToMany 的例子。
倉庫和貨物是一對多關系,並且在設計上,Goods 表中包含 Repository 的外鍵,則在 Repository 添加注解,Goods 上不需要。
@Entity
public class Repository{
@OneToMany(cascade = {CascadeType.ALL})
@JoinColumn(name = "repo_id")
private List<Goods> list;
}
public class Goods{
}
具體可參考:@OneToMany、@ManyToOne 以及 @ManyToMany 講解(五)
JPA 的這幾個注解和 Hibernate 的關聯度比較大,而且一般適合於 code first 的形式,也就是說先有實體類后生成數據庫。在這里我並不建議沒有學習過 Hibernate 直接上手 Spring Data JPA 的人去使用這些注解,因為一旦加上關系注解后,從查詢的角度雖然方便了,但是涉及到一些級聯的操作,例如刪除、修改等操作,容易采坑。需要額外去了解 Hibernate 的緩存刷新機制。
多數據源
默認單數據源的情況下,我們只需要將自己的 Repository 實現 JpaRepository 接口即可,通過 Spring Boot 的 Auto Configuration 會自動幫我們注入所需的 Bean,例如 LocalContainerEntityManagerFactoryBean
、EntityManager
、DataSource
。
但是在多數據源的情況下,就需要根據配置文件去條件化創建這些 Bean 了。
- 配置文件添加多個數據源信息
spring:
datasource:
hangzhou: # datasource1
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.17.11.72:3306/gpucluster?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
shanghai: # datasource2
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.21.30.61:3306/gpucluster?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
jpa:
open-in-view: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
- 數據源 bean 注入
@Slf4j
@Configuration
public class DataSourceConfiguration {
@Bean(name = "HZDataSource")
@Primary
@Qualifier("HZDataSource")
@ConfigurationProperties(prefix = "spring.datasource.hangzhou")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(name = "SHDataSource")
@Qualifier("SHDataSource")
@ConfigurationProperties(prefix = "spring.datasource.shanghai")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
}
- 注入 JPA 相關的 bean(一個數據源一個配置文件)
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactoryHZ",
transactionManagerRef = "transactionManagerHZ",
basePackages = {"cn.com.arcsoft.app.repo.jpa.hz"},
repositoryBaseClass = IBaseRepositoryImpl.class)
public class RepositoryHZConfig {
private final DataSource HZDataSource;
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
public RepositoryHZConfig(@Qualifier("HZDataSource") DataSource HZDataSource, JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
this.HZDataSource = HZDataSource;
this.jpaProperties = jpaProperties;
this.hibernateProperties = hibernateProperties;
}
@Primary
@Bean(name = "entityManagerFactoryHZ")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryHZ(EntityManagerFactoryBuilder builder) {
// springboot 2.x
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
jpaProperties.getProperties(), new HibernateSettings());
return builder.dataSource(HZDataSource)
.properties(properties)
.packages("cn.com.arcsoft.app.entity")
.persistenceUnit("HZPersistenceUnit")
.build();
}
@Primary
@Bean(name = "transactionManagerHZ")
public PlatformTransactionManager transactionManagerHZ(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactoryHZ(builder).getObject());
}
}
- 在之前配置的對應的包中添加相應的 repository 就可以了。如果數據源數據庫是相同的,可實現一個主的 repository,其余繼承一下。
@Primary
@Qualifier("volumeHZRepository")
public interface IVolumeRepository extends IBaseRepository<Volume, Integer> {
Volume findByUserIdAndIp(Integer userId, String ip);
}
@Qualifier("volumeSHRepository")
public interface IVolumeSHRepository extends IVolumeRepository {
}
JPA 與 Hibernate
在使用 Spring Data JPA 的時候,雖然底層是 Hibernate 實現的,但是我們在使用的過程中完全沒有感覺,因為我們在使用 JPA 規范提供的 API 來操作數據庫。但是遇到一些復雜的業務,或許任然需要關注 Hibernate,或者 JPA 底層的一些實現,例如 EntityManager 和 EntityManagerFactory 的創建和使用。
下面我就講講最核心的兩點。
對象生命周期
用過 Mybatis 的都知道,它屬於半自動的 ORM,僅僅是將 SQL 執行后的結果映射到具體的對象,雖然它也做了對查詢結果的緩存,但是一旦數據查出來封裝到實體類后,就和數據庫無關了。但是 JPA 后端的 Hibernate 則不同,作為全自動的 ORM,它自己有一套比較復雜的機制,用於處理對象和數據庫中的關系,兩者直接會進行綁定。
首先在 Hibernate 中,對象就不再是基本的 Java POJO 了,而是有四種狀態。
- 臨時狀態 (transient): 剛用 new 語句創建,還未被持久化的並且不在 Session 的緩存中的實體類。
- 持久化狀態 (persistent): 已被持久化,並且在 Session 緩存中的實體類。
- 刪除狀態 (removed): 不在 Session 緩存中,而且 Session 已計划將其從數據庫中刪除的實體類。
- 游離狀態 (detached): 已被持久化,但不再處於 Session 的緩存中的實體類。
需要特別關注的是持久化狀態的對象,這類對象一般是從數據庫中查詢出來的,同時會存在 Session 緩存中,由於存在緩存清理與 dirty checking 機制,當修改了對象的屬性,無需手動執行 save 方法,當事務提高后,改動會自動提交到數據庫中去。
緩存清理與 dirty checking
當事務提交后,會進行緩存清理操作,所有 session 中的持久化對象都會進行 dirty checking。簡單描述一下過程:
- 在一個事務中的各種查詢結果都會緩存在對應的 session 中,並且存一份快照。
- 在事務 commit 前,會調用
session.flush()
進行緩存清理和 dirty checking。將所有 session 中的對象和對應快照進行對比,如果發生了變化,則說明該對象 dirty。 - 執行 update 和 delete 等操作將 session 中變化的數據同步到數據庫中。
開啟只讀事務可屏蔽 dirty checking,提高查詢效率。
Troubleshooting
- Jpa 與 lombok 配合使用的問題產生 StackOverflowError
使用 Hibernate 的關系注解 @ManyToMany 時使用 @Data,執行查詢時會出現 StackOverflowError 異常。主要是因為 @Data 幫我們實現了 hashCode() 方法出現了問題,出現了循環依賴。
解決方法:在關系字段上加上 @EqualsAndHashCode.Exclude 即可。
@EqualsAndHashCode.Exclude
@ManyToMany(fetch = FetchType.LAZY,cascade = {CascadeType.PERSIST})
private Set<User> membersSet;
Lombok.hashCode issue with “java.lang.StackOverflowError: null”
- Spring boot JPA:Unknown entity 解決方法
在采用兩個大括號初始化對象后,再調用 JPA 的 save 方法時會拋出 Unknown entity 這個異常,這是 JPA 無法正確識別匿名內部類導致的。
解決方法:手動 new 一個對象再調用 set 方法賦值。
Spring boot JPA:Unknown entity 解決方法
- 使用關系注解時產生的 LazyInitializationException 異常
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
如果使用 Hibernate 關系注解,可能會遇到這個問題,這是因為在 session 關閉后 get 對象中懶加載的值產生的。
解決方法:
- 在實體類中添加注解
@Proxy(lazy = false)
- 在 services 層的方法中添加
@Transactional
,將 session 管理交給 spring 事務
總結
本文主要講了下 Spring Data JPA 的基本使用和一些個人經驗。
ORM 發展至今,從 Hibernate 到 JPA,再到現在的 Spring Data JPA。可以看到是一個不斷簡化的過程,過去大段的 xml 已經沒有了,僅保留基本的 sql 字符串即可。Spring Data JPA 雖然配置和使用起來簡單,但由於它的底層依然是 Hibernate 實現的,因此有些東西仍然需要去了解。就目前使用而言,有以下幾點感受:
- 要用好 Spring Data JPA,Hibernate 的相關機制還是需要有一定的了解的,例如前面提到的對象聲明周期及 Session 刷新機制等。如果不了解,容易出現一些莫名其妙的問題。
- 如果是新手,個人不推薦使用關系注解。 技術本身就是一步步在簡化,如果不是非常復雜的例如 ERP 系統,沒必要去使用 JPA 和 Hibernate 原生的東西,完全可以手動多次查詢操作來代替關系注解。之所以這么講,是因為對 JPA 的關系注解的使用,以及各種級聯操作的類型理解不深,會存在一些隱患。
參考文獻
- SpringBoot 整合 Spring-data-jpa
- Spring Boot(五):Spring Boot Jpa 的使用
- Spring Data JPA 進階(六):事務和鎖
- Spring Data JPA - Reference Documentation
附錄
支持的方法關鍵詞
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 | findByFirstname,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 | findByAgeIsNull | … 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(附加參數綁定 %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(與前置綁定的參數 %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(參數綁定包裝 %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection
|
… where x.age in ?1 |
NotIn | findByAgeNotIn(Collection
|
… 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) |
Top 或者 First | findTopByNameAndAge,findFirstByNameAndAge | where … limit 1 |
Topn 或者 Firstn | findTop2ByNameAndAge,findFirst2ByNameAndAge | where … limit 2 |
Distinct | findDistinctPeopleByLastnameOrFirstname | select distinct …. |
count | countByAge,count | select count(*) |