1. JPA 和 Hibernate 有什么區別?
JPA 是 Java 官方提出的一套 ORM 規范,它只定義了實體映射、關系管理、查詢接口等標準,不包含具體實現。Hibernate 是對 JPA 規范的最常用實現,提供了完整的 ORM 功能,并擴展了許多 JPA 沒有的高級特性。而 Spring Data JPA 是 Spring 提供的對 JPA 的封裝,它簡化了 Repository 層的開發,支持方法名自動派生查詢。在 Spring Boot 項目中,Spring Data JPA 默認使用 Hibernate 作為底層 JPA 實現。
2. JPA 常見注解作用?
JPA 通過 @Entity
將類標記為持久化實體,使用 @Id
和 @GeneratedValue
標識主鍵,使用 @Column
映射字段,并通過 @OneToMany
、@ManyToOne
等注解描述實體間關系。還可以通過 @Temporal
、@Transient
控制字段行為。實際項目中常結合 Hibernate 提供的擴展注解實現時間自動填充等高級功能。
注解 | 作用 | 示例 |
| 聲明這是一個實體類,對應數據庫表 |
|
| 映射表名(可選) |
|
| 標記主鍵字段 |
|
| 主鍵生成策略 |
|
策略 | 說明 |
| 自增(MySQL 常用) |
| 數據庫序列(Oracle 常用) |
| 使用一張表生成主鍵 |
| 自動選擇適合的策略(默認) |
注解 | 作用 | 示例 |
| 映射字段名(可選) |
|
| 忽略此字段,不參與映射 | 不入庫的字段 |
| 大字段(如 text、blob) | 文本/二進制 |
| 映射日期時間類型(Date) |
|
值 | 映射類型(Java Date) |
| 只保存日期(yyyy-MM-dd) |
| 只保存時間(HH:mm:ss) |
| 保存日期+時間 |
注解 | 關系類型 | 對應關系 |
| 一對一 | 用戶 - 身份證 |
| 一對多 | 用戶 - 訂單 |
| 多對一 | 訂單 - 用戶 |
| 多對多 | 用戶 - 角色 |
| 指定外鍵列名 | 單向關系中用 |
| 中間表配置 | 多對多時使用 |
| 指定關系被維護方 | 用于雙向關系 |
| 聯級操作 | PERSIST、REMOVE 等 |
| 加載策略 | LAZY / EAGER |
類型 | 含義說明 |
| 保存時級聯保存( ) |
| 合并更新時級聯更新( ) |
| 刪除時級聯刪除( ) |
| 刷新實體狀態時也刷新關聯對象 |
| 分離實體時也分離關聯對象 |
| 包含以上所有操作(常用) |
注解 | 作用 |
| 插入前執行 |
| 插入后執行 |
| 更新前執行 |
| 更新后執行 |
| 刪除前執行 |
| 刪除后執行 |
| 加載后執行 |
3. Spring Data JPA 如何實現自定義 SQL?
Spring Data JPA 實現自定義 SQL 的常用方法是通過 @Query 注解編寫 JPQL 或原生 SQL,并配合 @Modifying 實現更新刪除操作。對于更復雜或動態查詢,可以通過自定義 Repository 實現類,利用 EntityManager 執行原生或 JPQL 查詢,或者使用 JPA Criteria API 結合 Specification 來動態構造查詢條件。
1. 使用 @Query
注解寫 JPQL 或原生 SQL
public interface UserRepository extends JpaRepository<User, Long> {// JPQL 查詢(面向實體類和屬性名)@Query("select u from User u where u.name = ?1")List<User> findByNameJPQL(String name);// 原生 SQL 查詢,nativeQuery = true@Query(value = "select * from user where name = ?1", nativeQuery = true)List<User> findByNameNative(String name);
}
2. 使用命名參數(推薦,代碼更易讀)
@Query("select u from User u where u.name = :name and u.age > :age")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
3. 定義更新或刪除操作(需要加 @Modifying
)
@Modifying
@Query("update User u set u.status = :status where u.id = :id")
int updateStatus(@Param("status") Integer status, @Param("id") Long id);
注意:執行更新或刪除時,方法上必須加 @Modifying
注解,并且調用前通常要加事務(@Transactional
)。
4.設計一個 SqlServiceImpl
作為 SQL 服務組件,內部封裝基于 EntityManager
的通用查詢和更新方法。
@Service
public class SqlServiceImpl implements SqlService {@PersistenceContextprivate EntityManager em;@Overridepublic <T> List<T> queryList(String sql, Map<String, Object> params, Class<T> resultClass) {Query query = em.createNativeQuery(sql, resultClass);setParams(query, params);return query.getResultList();}@Overridepublic int executeUpdate(String sql, Map<String, Object> params) {Query query = em.createNativeQuery(sql);setParams(query, params);return query.executeUpdate();}private void setParams(Query query, Map<String, Object> params) {if (params != null) {params.forEach(query::setParameter);}}
}
4. JPA 的緩存機制?MyBatis 的緩存區別在哪
JPA 默認支持一級緩存,綁定在 EntityManager 生命周期內,避免重復查詢;可選開啟二級緩存,在多個 EntityManager 之間共享實體,提高性能。常用的二級緩存實現包括 EhCache、Redis 等。對于跨事務的高頻讀取場景,通過二級緩存可以顯著減少數據庫壓力。但需注意緩存一致性和適配分布式場景的問題。
JPA 的二級緩存和 MyBatis 的二級緩存在原理上類似,都是跨 Session 的共享緩存機制,目的是避免重復查詢、提升性能。它們分別綁定在 EntityManager 和 SqlSession 上,區別在于 JPA 是基于實體對象的緩存,而 MyBatis 更偏向 SQL 結果級別。二者都要求結合緩存一致性策略合理使用,否則可能造成數據不一致。
JPA 的實體緩存由于基于實體主鍵,緩存粒度細,命中率高,因此訪問效率通常優于 MyBatis 結果緩存。MyBatis 結果緩存依賴完全相同的 SQL 和參數,命中率相對較低,且緩存的結果需要反序列化成對象,性能略遜一籌。但 MyBatis 緩存更靈活,適合復雜查詢場景。實際選擇要結合業務特點。
MyBatis 在執行寫操作后會直接清除當前 SqlSession 或 Mapper 對應的緩存區域,防止讀取臟數據,但不會維護緩存一致性。而 JPA 則是基于實體生命周期管理緩存,CUD 操作后會自動更新緩存中對應實體的狀態,確保緩存始終與數據庫一致。JPA 緩存更新更智能,但控制相對復雜;MyBatis 緩存簡單粗暴,控制更靈活。
5. JPA 如何實現分頁?
在 JPA 中分頁可以通過 JpaRepository
接口提供的 findAll(Pageable pageable)
方法快速實現,也可以結合自定義 @Query
和 Pageable 參數實現分頁。對于更復雜的查詢,可使用 EntityManager 執行原生 SQL 并手動設置 setFirstResult()
和 setMaxResults()
進行分頁。整體上,JPA 分頁底層是通過 limit offset
實現的,支持多種方式。
Page<T> findAll(Pageable pageable);Pageable pageable = PageRequest.of(0, 10, Sort.by("createTime").descending());
Page<User> userPage = userRepository.findAll(pageable);@Query("SELECT u FROM User u WHERE u.status = :status")
Page<User> findByStatus(@Param("status") Integer status, Pageable pageable);Page<User> page = userRepository.findByStatus(1, PageRequest.of(1, 20));Query query = entityManager.createNativeQuery("SELECT * FROM user WHERE status = ?", User.class);
query.setParameter(1, 1);
query.setFirstResult(0); // offset
query.setMaxResults(10); // page size
List<User> resultList = query.getResultList();
6. 什么是 N+1 查詢問題?JPA 如何防止 N+1 查詢問題?
N+1 查詢問題是指:你執行了 1 條查詢語句獲取了主實體列表,但為了獲取每個主實體的關聯數據,又觸發了 N 條額外的查詢,共執行 N+1 次查詢。
在實際項目中,我們采用 JPA 管理實體關系,但在復雜查詢場景中做了區分:
- 對于一對多的單集合,我們使用
@EntityGraph
或JOIN FETCH
一次性加載 - 對于多個集合的場景,為了避免笛卡爾積,我們拆分為多條 SQL 查詢并在服務層聚合
- 一對一可以直接 fetch,多對多則更建議拆分或優化結構處理
這種方式兼顧了查詢性能與代碼結構,是我們在 JPA 使用上的一套標準規范。
- 對于一對多的單集合,我們使用
EntityGraph 和 JOIN FETCH 能有效解決一對一和一對多的單集合場景。但當面對多個集合或多對多結構時,使用多個 JOIN FETCH 會導致笛卡爾積,帶來嚴重性能問題。我們會采用分批拉取子表數據并按主鍵分組組裝的方式替代,避免了真正的 N+1 查詢問題,同時保持數據結構清晰和查詢效率。
N+1 查詢問題廣泛存在于 ORM 框架中,雖然最常出現在懶加載的查詢場景中,但更新(saveAll)、插入(saveAll)、刪除操作中如果未啟用批處理,同樣也可能出現 N+1 的問題。因此我們通常會在 JPA 配置中啟用 hibernate.jdbc.batch_size
等批處理參數,同時在邏輯層避免逐條操作,通過批處理語句或一次性提交操作來優化性能。而且saveAll()
和 JPA 的自動更新(dirty checking)本質上是針對每個實體單獨生成一條 UPDATE
SQL,即使開啟了 Hibernate 的 JDBC 批量功能,也只是把多條 SQL 放到同一個批次網絡發送,數據庫還是執行多條 UPDATE
,只不過減少了網絡開銷。
- 如果你想真正做到“一條 SQL 更新多條數據”,必須使用 JPQL 的批量更新語句或者原生 SQL,比如:
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.age > :age")
int bulkUpdate(@Param("status") Integer status, @Param("age") Integer age);
這條語句就是一條 SQL 在數據庫執行,效率最高。
在 Spring Data JPA 中,我們通常使用 deleteByIdIn(List<Long> ids)
實現批量刪除,這種方式底層執行的是一條 DELETE IN 語句,效率高且不加載實體,適合不需要觸發實體事件的場景。相比手動遍歷刪除,它更適合大批量數據刪除,尤其是在沒有復雜業務邏輯時。
7. JPA/Hibernate 的懶加載和立即加載?
在 JPA 中,懶加載和立即加載控制著關聯實體什么時候加載。懶加載可以在真正訪問字段時再查詢,提升查詢效率,是默認行為;而立即加載則在主表查詢時立即加載全部關聯數據,使用方便但可能帶來性能問題。我們通常對一對多、多對多使用懶加載,并結合 EntityGraph、Join Fetch 等方式顯式控制加載時機,避免產生 N+1 查詢或懶加載異常。
8. JPA 如何實現事務管理?
在 JPA 中事務由 Spring 統一管理,我們通常使用 @Transactional
來聲明事務邊界。Spring 會為事務方法創建代理,在方法執行前開啟事務,方法正常結束提交事務,異常則自動回滾。默認情況下只對 RuntimeException 回滾,若需處理受檢異常需要手動指定。JPA 操作必須在事務中執行,否則會拋出 TransactionRequiredException
,我們也可以通過事務傳播機制控制方法嵌套的事務行為。
9. JPA 高性能分頁與大數據量優化策略
默認 JPA 分頁基于 LIMIT + OFFSET 實現,簡單易用,但底層使用的是跳過(skip scan),越往后性能越差。性能線性下降!當數據量大、頁碼高時性能嚴重下降。為了解決這一問題,我們使用覆蓋索引 + where id > ?
進行 游標分頁(Keyset Pagination)實現高效翻頁;此外還可通過投影查詢減少字段,或結合數據庫索引與分表策略優化分頁性能。實際項目中會根據場景綜合選擇分頁策略。
10. JPA + Redis 緩存實戰方案
JPA 默認開啟了一級緩存,基于 EntityManager 實例,在事務內緩存同一個實體對象,避免重復查詢數據庫。而二級緩存是 Hibernate 的擴展功能,用于不同 EntityManager 實例之間共享實體緩存,需要通過注解和配置文件顯式啟用。 常通過 EhCache、Redis 等作為緩存實現。但考慮到其復雜性和集群一致性問題,實際項目中建議優先使用 Spring Cache + Redis 替代原生方案。 為了實現分布式共享,我們通常結合 Redis 或 Caffeine 等緩存框架替代原生二級緩存,搭配 Spring Cache 更靈活。
實際項目中我們通常采用 Spring Cache 來處理接口層的緩存,比如常規的查詢接口、字典表、配置項等,這樣可以借助注解簡化代碼。而對于 Redis 的高級數據結構操作(如排行榜、限流、鎖等),則通過 RedisTemplate 實現。在我們項目中,我們進一步封裝了一個統一的緩存組件,底層既支持 Spring Cache,也支持 RedisTemplate,便于統一 TTL 策略、命名規則與緩存失效控制。
11. 為什么已經有了 JPA,還要用 MyBatis?
實際項目中我們既使用過 JPA,也使用過 MyBatis,一般來說業務模型結構清晰、查詢簡單的模塊我們使用 Spring Data JPA 實現,利用它的自動查詢機制提升開發效率;而對于多表關聯復雜、SQL 調優要求高的模塊,我們使用 MyBatis 或 MyBatis-Plus 來手動編寫 SQL,更加靈活可控。在一些大型項目中,兩者并存是非常常見的。
12. JPA Entity 生命周期
狀態 | 說明 | 觸發方式/操作 | 是否持久化到數據庫 |
瞬時態(Transient) | 實例剛創建,未關聯到持久化上下文 |
;未調用 | 否 |
托管態(Managed / Persistent) | 實體被 EntityManager 管理,處于持久化上下文中 | 調用 或 查出對象 | 是 |
游離態(Detached) | 實體曾被管理,但現在已脫離持久化上下文 | 調用 ,事務結束,EntityManager 關閉 | 否(但數據庫有記錄) |
刪除態(Removed) | 實體被標記為刪除,等待同步到數據庫 | 調用 | 是(刪除操作) |
當前狀態 | 操作 | 結果狀態 |
瞬時態 |
| 托管態 |
托管態 |
| 刪除態 |
托管態 |
| 游離態 |
游離態 |
| 托管態 |
刪除態 | 事務提交/刷新 | 數據庫中刪除 |
游離態/瞬時態 | 無 | 不受 EntityManager 管理 |
13. JPA 如何聯表查詢?
1. 基于實體關聯映射的查詢
JPA 支持通過實體間的關系(如 @OneToMany
、@ManyToOne
、@ManyToMany
、@OneToOne
)來自動聯表查詢。示例:假設 Order
和 Customer
關聯,Order
有個 customer
屬性:
@Entity
public class Order {@ManyToOne@JoinColumn(name = "customer_id")private Customer customer;// other fields...
}
查詢所有訂單及其客戶:
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o JOIN FETCH o.customer", Order.class).getResultList();
這里 JOIN FETCH
用于立即加載客戶,避免 N+1 查詢。
2. 使用 JPQL 顯式寫聯表查詢
你也可以寫類似 SQL 的 JPQL 聯表查詢:
SELECT o, c FROM Order o JOIN o.customer c WHERE c.status = :status
也可以寫普通 JOIN
或者 LEFT JOIN
。
14. 什么是全自動 ORM 與 半自動 ORM?
全自動 ORM 是指框架根據實體類和注解自動生成所有 SQL 語句,開發者無需手寫 SQL,適合簡單的 CRUD 和業務場景,代表技術如 JPA、Spring Data JPA。優點是開發效率高,但對復雜查詢和性能調優支持有限。
半自動 ORM 則是開發者需要手寫 SQL 或 XML 映射來完成復雜查詢,框架負責對象映射和部分 CRUD 操作,代表技術如 MyBatis。它靈活度高,適合復雜業務,但開發和維護成本較大。
總結來說,全自動 ORM 適合快速開發和簡單場景,半自動 ORM 更適合復雜查詢和性能優化需求。
15. JPQL 和 SQL 的區別
JPQL 是 JPA 定義的面向對象查詢語言,操作的是實體類和屬性,不直接操作數據庫表,語法類似 SQL,但用實體名和屬性名替代表名和字段名。JPQL 具有數據庫無關性,能方便地查詢實體間的關系和繼承結構。
而 SQL 是數據庫的標準查詢語言,直接操作數據庫表和字段,語法依賴具體數據庫方言,功能更強大,適合復雜查詢和性能調優。
總結來說,JPQL 更適合 ORM 場景的對象查詢,SQL 則適合原生復雜查詢。