Spring Data Jpa必殺技:12大實戰技巧讓開發 & 性能飆升300%
環境:SpringBoot3.4.2
1. 簡介
在基于Spring Boot 項目開發中,有一個關鍵組件是 Spring Data JPA,這是一個在數據處理方面表現出色的 API。它在我們的開發過程中具有不可忽視的重要性。
它不僅僅是一個工具,而是一個強大的工具,能夠顯著提升我們的開發流程,讓我們有信心和能力高效地處理復雜的數據管理任務。
最常用的默認注解包括:
- @Repository:用于標記或注解一個扮演數據訪問對象(DAO)角色的類,通常我們是不需要寫該注解的。
- @Query:允許開發者使用原生查詢的注解。
隨著時間的推移,系統的數據量也隨之增大,我們都遇到過一些常見的挑戰,比如查詢速度慢、管理復雜的關系、理解復雜的原生查詢或優化接口效率。很多人都會遇到這些問題,解決這些問題對于提升我們的開發流程至關重要。
接下來,我們將由淺入深的詳細介紹Spring Data JPA實戰技巧開發。
2.實戰案例
2.1 基本概念
開發中,在定義接口時,我們通常可以繼承:Repository,CrudRepository,PagingAndSortingRepository以及JpaRepository接口。
Repository
Repository 是最基本的接口,通常不包含任何方法。它不提供任何功能,但作為 Spring Data JPA 中所有其他倉庫接口的基礎接口。
public interface UserRepository extends Repository<User, Long> {
// 沒有預定義的方法
}
不建議使用該接口,因為它沒有提供任務功能,僅起到了標記作用。
CrudRepository
CrudRepository 接口提供了 CRUD 操作。如果你需要基本的數據訪問能力而不需要排序或分頁,該接口非常適合你。
public interface UserRepository extends CrudRepository<User, Long> {
// 提供了基本的 CRUD 方法
}
常用的基本方法:
- save(S entity):保存給定的實體
- findById(ID id):通過 ID 檢索實體
- existsById(ID id):返回是否存在具有給定 ID 的實體
- findAll():返回所有實體
- deleteById(ID id):刪除具有給定 ID 的實體
PagingAndSortingRepository
該接口添加了分頁和排序的方法。這在處理大型數據集并在頁面上顯示數據時非常有用。這個接口也包含了 CRUD 操作。
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
// CRUD 方法加上分頁和排序
}
這個接口的額外方法包括:
- findAll(Pageable pageable):返回符合 Pageable 對象中提供的分頁限制的實體頁面。
- findAll(Sort sort):返回按給定選項排序的所有實體。
JpaRepository
這個接口添加了 JPA 特定的方法,并提供了一套完整的 JPA 相關方法,如批量操作、自定義查詢和刷新控制。這使得它成為 JPA 應用程序中最強大和最靈活的選項。
public interface UserRepository extends JpaRepository<User, Long> {
// 完整的 CRUD 方法、分頁、排序和 JPA 特定方法
}
這個接口提供了一套全面的 JPA 相關操作方法,允許你執行各種任務而無需額外的接口或自定義代碼。
使用 JpaRepository,你可以通過遵循 findBy 后跟屬性名的命名約定來定義自定義查詢方法。Spring Data JPA 將根據方法名自動生成查詢。
public interface UserRepository extends JpaRepository<User, Long> {
// 通過 ID 查找實體
Optional<User> findById(Long id) ;
// 查找所有具有給定名稱的實體
List<User> findByName(String name) ;
// 查找年齡大于給定值的實體
List<User> findByAgeGreaterThan(int age) ;
}
為什么選擇 JpaRepository?
- 功能全面:它結合了 CRUD、分頁、排序和 JPA 特定操作在一個接口中。
- 便捷性:通過提供所有必要的方法簡化了代碼。
- 性能:通過批量操作和刷新控制優化了性能。
- 靈活性:允許自定義查詢方法并支持 JPQL(Java 持久化查詢語言)和原生 SQL 查詢。
2.2 使用 Specification 和 Criteria Builder
在使用 Spring Data JPA 時,有時我們需要更復雜的查詢,這些查詢無法通過簡單的查詢方法輕松實現。這時,Specification 和 Criteria Builder 就派上了用場,它們允許你構建動態查詢并處理復雜的場景。
Specification
Specification 是 Spring Data JPA 中的一個函數式接口,用于基于 JPA 條件創建動態查詢。它提供了一種以編程方式構建查詢的方法。當查詢條件在編譯時未知時,它非常有用。
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class UserSpecification {
public static Specification<MyEntity> hasName(String name) {
return (Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("name"), name);
};
}
}
使用
private final UserRepository userRepository ;
Specification<User> spec = UserSpecification.hasName("Pack");
List<User> results = userRepository.findAll(spec) ;
Criteria Builder
Criteria Builder API 是 JPA 的一部分,允許創建類型安全的查詢。它提供了一種使用 Java 對象而不是硬編碼字符串來動態構建查詢的方式。如下示例:
@Service
public class UserService {
@PersistenceContext
private EntityManager em;
public List<User> findByName(String name) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
// 構建查詢
query.select(root).where(cb.equal(root.get("name"), name));
return em.createQuery(query).getResultList();
}
}
你可以結合多個條件來構建更復雜的查詢。例如,你可以使用 and 和 or 來組合條件。
public class UserSpecification {
public static Specification<User> hasName(String name) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("name"), name);
};
}
public static Specification<User> hasStatus(Integer status) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("status"), status);
};
}
public static Specification<User> hasAgeGreaterThan(int age) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.greaterThan(root.get("age"), age);
};
}
public static Specification<User> hasNameAndStatus(String name, Integer status) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
Predicate namePredicate = cb.equal(root.get("name"), name);
Predicate statusPredicate = cb.equal(root.get("status"), status);
return cb.and(namePredicate, statusPredicate);
};
}
}
// 使用
Specification<User> spec = UserSpecification.hasNameAndStatus("Pack", 1) ;
List<User> results = userRepository.findAll(spec) ;
結合多個規范可以創建更靈活和可重用的查詢條件。
Specification<User> spec = Specification.where(UserSpecification.hasName("Pack"))
.and(UserSpecification.hasStatus(1))
.and(UserSpecification.hasAgeGreaterThan(25));
List<User> results = myEntityRepository.findAll(spec);
2.3 開發技巧
為了最大化 Spring Data JPA 的效用,遵循一些提示和技巧是至關重要的。這些可以幫助你優化應用程序、避免常見陷阱,并確保你的代碼是可維護和高效的。
使用懶加載
默認情況下,將實體關系設置為 FetchType.LAZY,這意味著相關實體在訪問之前不會從數據庫加載。雖然這可以節省資源,但如果處理不當,也可能導致 N+1 選擇問題。
最佳實踐:對于大型或很少訪問的關系,使用懶加載。對于頻繁訪問的關系,考慮使用迫切加載。
@Entity
public class Customer {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "customer")
private List<Order> orders ;
}
優化查詢
能用一個構建良好的查詢完成任務時,就不要運行多個查詢。必要時,使用 JPQL、Criteria API 或原生查詢來優化性能。
最佳實踐:使用自定義查詢或規范將相關查詢合并為單個數據庫訪問操作。
@Query("SELECT e FROM User e JOIN FETCH e.orders WHERE e.name = :name")
List<MyEntity> findByNameWithOrders(@Param("name") String name);
利用緩存
緩存可以顯著提高應用程序的性能,通過減少數據庫命中次數。Spring 提供了與 Ehcache、Redis 等緩存解決方案的輕松集成。
最佳實踐:緩存不經常更改的頻繁訪問數據。
private final UserRepository userRepository ;
@Cacheable("users")
public List<User> findAll() {
return userRepository.findAll();
}
批量處理
在保存或刪除多個實體時,批量處理可以減少數據庫往返次數并提高性能。
最佳實踐:使用 saveAll 進行批量插入,使用 deleteInBatch 進行批量刪除。
private final UserRepository userRepository ;
public void saveUsers(List<User> users) {
userRepository.saveAll(users);
}
public void deleteUsers(List<User> users) {
userRepository.deleteInBatch(users);
}
適當的使用事務
確保你的數據庫操作被正確地包裹在事務中,以維護數據完整性。使用 Spring 的 @Transactional 注解來管理事務。
最佳實踐:在服務層使用 @Transactional,以確保方法內的所有操作都是單個事務的一部分。
@Service
public class UserService {
private final UserRepository userRepository ;
@Transactional
public void updateUsers(List<User> users) {
for (User user: users) {
userRepository.save(user) ;
}
}
}
save方法的內部是使用了 @Transactional 注解。
避免N+1問題
N+1 選擇問題發生在應用程序為了獲取 N 個實體的集合(每個實體都有自己的相關實體)而發出 N+1 個數據庫查詢時,這會嚴重影響性能。
最佳實踐:在你的 JPQL 查詢中使用 JOIN FETCH 來在單個查詢中獲取相關實體。
@Query("SELECT e FROM Customer e JOIN FETCH e.orders WHERE e.status = :status")
List<Customer> findByStatusWithOrders(@Param("status") Integer status);
日志記錄&監控
在開發過程中啟用 SQL 日志記錄,以了解 Hibernate 生成的查詢。這可以幫助識別和優化低效的查詢。
最佳實踐:使用日志記錄來監控 SQL 查詢和性能指標。
spring:
jpa:
show-sql=true
properties
hibernate:
'[format_sql]': true
# 慢SQL閾值
'[log_slow_query]': 1000
使用投影
有時,你只需要幾個字段而不是整個實體。使用投影來僅選擇必要的數據。
最佳實踐:使用投影來僅獲取所需的字段,減少從數據庫傳輸的數據量。
public interface UserProjection {
String getName();
String getStatus();
}
@Query("SELECT e.name AS name, e.status AS status FROM User e WHERE e.age > :age")
List<UserProjection> queryUser(@Param("age") int age) ;
使用視圖
有時,你的選擇查詢會變得更加復雜。創建虛擬表或表視圖可以幫助簡化數據訪問。
最佳實踐:使用視圖可以簡化 SELECT 語句,減少復雜性并避免潛在錯誤。
審計功能
Spring Data 支持實體變更審計(創建者/修改者/時間),我們能通過注解非常方便的應用審計功能。通過 @CreatedBy 和 @LastModifiedBy 來獲取創建或修改實體的用戶,以及 @CreatedDate 和 @LastModifiedDate 來獲取更改發生的時間。
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
接下來,還需要提供如下組件用來獲取用戶。
@Component
public class PackAuditorAware implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.of("pack") ;
}
}
鎖機制
要指定查詢方法使用的鎖模式,可以在查詢方法上使用 @Lock 注解,具體示例如下:
public interface ProductRepository extends JpaRepository<Product, Long> {
@Transactional
@Lock(LockModeType.PESSIMISTIC_READ)
public Product findByName(String name) ;
}
注意:這里我們需要 @Transactional 事務注解。
運行輸出SQL如下:
SELECT
p1_0.id,
p1_0.NAME,
p1_0.price,
p1_0.quantity
FROM
t_product p1_0
WHERE
p1_0.NAME =? FOR SHARE
自動添加了 FOR SHARE。
流式查詢
可以使用 Java 8 的 Stream<T> 作為返回類型來增量處理查詢方法的結果。并非將查詢結果包裝在 Stream 中,而是使用特定于數據存儲的方法來執行流處理,如以下示例所示:
@Query("select u from User u")
Stream<User> findUserByStream();
流可能會封裝底層特定于數據存儲的資源,因此在使用后必須關閉。您可以使用 close() 方法手動關閉流,或者使用 Java 7 中的 try-with-resources 塊,如下例所示:
try (Stream<User> stream = repository.findUserByStream()) {
stream.forEach(…) ;
}