Spring Data JDBC 入門與實踐

Spring Data 家族最近多了一個新成員:Spring Data JDBC(目前最新正式版是 1.0.9,項目主頁是 https://spring.io/projects/spring-data-jdbc )。因為最近使用了此技術,所以便想寫文對其介紹一二。

本文的內容主要涉及 Spring Data JDBC 的由來、基本使用、與現有技術的異同,以及實踐中的經驗。

Spring Data JDBC,顧名思義,是一個基于 JDBC 的數據庫持久化框架。這一領域技術不少,常用的有 Hibernate、MyBatis,還有基于 Hibernate/JPA 的 Spring Data JPA,不太常用的有 JOOQ、QueryDSL 等。那為何 Spring 要另起爐灶,造個新輪子呢?在回答這個問題之前,先來簡單介紹一下 Spring Data JDBC 的用法。

一、基本使用

和 Spring Data JPA 及其它 Spring Data 技術類似,Spring Data JDBC 的基本使用只需三步:1. 增加 Maven/Gradle 依賴;2. 定義實體類;3. 定義 Repository 接口。依賴配置跳過不介紹了,我們直接來看代碼部分。

@Table("t_user")
class User {
  @Id
  private Long id;
  private String username;
  private String email;
  private UserStatus status;
    
  /** Getter/Setter **/
}

interface UserDao extends CrudRepository<User, Long>, UserRepositoryExtension {
  @Query("select * from t_user where username = :username")
  Optional<User> findByUsername(String username);
}

大體上看,Spring Data JDBC 的用法和 Spring Data JPA 類似,同樣也可以零實現獲得基本的增刪改查功能。并且,同 Spring Data JPA 一樣,insert 和 update 這兩個操作都能通過同 CrudRepository#save 方法實現。選擇方法是看實體中主鍵是否有值。如果沒有值,那就是 insert,有,則是 update。

和 Spring Data JPA 不同地方在于:

  1. 不需要 @Entity 注解,@Table@Id 也是 Spring Data 提供的,而不是 JPA 的;
  2. Spring Data JDBC 不支持直接通過方法名獲得基本的查詢功能,而是必須通過在 @Query 中定義 SQL 實現;
  3. Spring Data JDBC 支持自定義擴展,這個在后面會詳細介紹;

我個人覺得,不像 Spring Data JPA 和 Mongo 那樣,不支持通過方法名獲得基本查詢功能是 Spring Data JDBC 的一項缺點,但不嚴重。畢竟,對于簡單的查詢,原生 SQL 寫起來不麻煩。從項目長期發展角度看,這點工作量算不了什么。

那除去表面上的差異,Spring Data JDBC 和 Spring Data JPA 的不同之處又有哪些?

二、與 Spring Data JPA 的不同點

簡單

Spring Data JPA 基于 Hibernate,而 Hibernate 是一個讓人又愛又恨的技術。同原生 JDBC 相比,Hibernate 極大地簡化了開發工作量;但另一方面,因為 Dirty Check、延遲加載、各種如 ManyToOne 等映射規則,又讓 Hibernate 成為了一個復雜技術。而這些復雜性,平時很少直接用到,但是卻增加了 Hibernate 的開發和調試難度。

Spring Data JDBC 的一個意義就在于,讓開發人員享受類似于 Hibernate 所帶來的便捷的同時,避免被 Hibernate 高級特性的過度復雜所困擾。

與 MyBatis 集成

Hibernate 與 MyBatis 正像是硬幣的兩面.與 Hibernate 相比,MyBatis 簡單、易用、可靠,但是難免顯得羅嗦了一些。這種羅嗦在基本功能層面顯得更加明顯。雖然,在大型項目中,基本功能實現層面的羅嗦并不是大問題(這也是互聯網公司喜歡用 MyBatis 的原因),但更加簡單自然是何樂而不為呢。

Spring Data JDBC 與 MyBatis 結合,自然是能夠結合兩者有點,基本功能得到了簡化,復雜功能也能信手拈來。

Spring Data JDBC 與 MyBatis 整合有兩種方式:

  1. 官方的整合方法 https://docs.spring.io/spring-data/jdbc/docs/1.1.0.RC1/reference/html/#jdbc.mybatis
  2. 基于自定義 Repository 實現

相較而言,我更喜歡第二種方法,因為官方整合方法只是用 MyBatis 實現了基本功能,這反而是 MyBatis 所不擅長的,而基于自定義 Repository 實現的方式更能發揮兩者的優勢。下面看一下簡單示例:

首先定義擴展接口

interface UserRepositoryExtension {
  void update(User user);
}

實現上面的接口,用 MyBatis 實現具體功能。類命名必須為接口名 + Impl。

@Component
class UserRepositoryExtensionImpl implements UserRepositoryExtension {
  private final SqlSession sqlSession;

  public void update(User user) {
    return sqlSession.update("update", user);
  }
}

原有接口擴展上面的接口

interface UserDao extends CrudRepository<User, Long>, UserRepositoryExtension {
}

這樣就可以了,是不是很簡單。

領域驅動設計

在 Spring Data JDBC 文檔中,有一節提到了領域驅動設計 https://docs.spring.io/spring-data/jdbc/docs/1.1.0.RC1/reference/html/#jdbc.domain-driven-design 。文檔中說到 Spring Data 的很多設計都是受了 DDD 的啟發:

In the current implementation, entities referenced from an aggregate root are deleted and recreated by Spring Data JDBC.

具體 Spring Data JDBC 是如何實現 DDD 的?在介紹之前,先問大家一個問題,大家覺得數據庫每個表都需要有一個 Repository 或 DAO 類與之對應嗎?

答案是否。按照 DDD 的思想,只有 Aggregate Root(聚合根)才是持久化操作的唯一入口。舉個例子,Order(訂單)和 OrderItem(訂單條目)都是訂單域中的實體。但是,因為訂單是聚合根,所以只有訂單有對應的 Repository 類,而訂單條目則沒有。

那如何完成對訂單條目表的數據操作呢?這篇文章《Spring Data JDBC, References, and Aggregates》 (https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates) 對此做了比較詳細的介紹。

interface OrderRepository extends CrudRepository<PurchaseOrder, Long> {
  @Query("select count(*) from order_item")
  int countItems();
}

如上例所示,訂單類(有 PurchaseOrder 表示)對應的 Repository 包含了對 order_item 表的操作,因為訂單條目不是聚合根,沒有自己的 Repository。所以,對訂單條目的操作需要定義在訂單的 Repository 中。恐怕也是這個思想上的不同,導致 Spring Data JDBC 很可能不會具備像 Spring Data JPA 和 Mongo 那樣純聲明式的持久化功能了。

@Autowired OrderRepository repository;

@Test
public void createUpdateDeleteOrder() {
  PurchaseOrder order = new PurchaseOrder();
  order.addItem(4, "Captain Future Comet Lego set");
  order.addItem(2, "Cute blue angler fish plush toy");

  PurchaseOrder saved = repository.save(order);

  assertThat(repository.count()).isEqualTo(1);
  assertThat(repository.countItems()).isEqualTo(2);
  
  repository.delete(saved);

  assertThat(repository.count()).isEqualTo(0);
  assertThat(repository.countItems()).isEqualTo(0);
}

而對于訂單的 save 和 delete 操作,也會對訂單條目進行操作。

對于其它更多的關于 Spring Data JDBC 和 DDD 的內容,歡迎大家自己看文章,也歡迎和我討論。

三、最佳實踐

接下來討論一下我在使用 Spring Data JDBC 過程中總結的一些最佳實踐。

DAO or Repository

我更傾向于講使用了 Spring Data 技術的接口命名為 DAO,而不是 Repository。按照 DDD 的思想,Repository 是包含了領域知識的,需要保證聚合的數據一致性。而要在復雜業務中滿足這一點,僅靠擴展一個接口是不可能做到的。因此,在 Spring Data 接口之上,還需要自己實現真正的 Repository。因此,Repository 這個名字要保留下來。

如何使用 save 方法

CrudRepository 提供了 save 方法,能同時實現 insert 和 update 兩種功能。我的建議只把 save 當作是創建數據的工具,盡量不要在更新時使用它。原因在于在復雜的項目中,使用 save 進行數據更新,極容易造成數據被錯誤覆蓋。因為 Spring Data JDBC 同 JPA 技術一樣,都會根據實體類生成對全部字段更新的 update 語句,并且 Spring Data JDBC 目前沒有內建的樂觀鎖機制。

何時使用 MyBatis

前面提到 Spring Data JDBC 可以和 MyBatis 結合使用。那什么時候一個功能應該用 Spring Data JDBC 實現,什么時候應該用 MyBatis 實現。我個人意見是當持久化方法的基本類型入參大于3個時,使用 MyBatis 實現(并將參數抽取為參數對象)。 因為,當入參大于3時,意味著參數列表和 SQL 語句都會比較復雜。使用 Spring Data JDBC,一是目前不支持參數對象,而是過長的 SQL 在注解中定義不易閱讀。

如何使用枚舉字段

我發現大部分項目的數據庫設計喜歡使用 Int 類型表示注入狀態、類型這樣的枚舉字段。雖然這樣做對數據庫性能有好處,但是對開發人員寫代碼的性能可是大有壞處。因為選項一多,沒有幾個人能記得住 1、2、3、4 各自代表什么業務含義。而且更有甚者,不同項目中,甚至同一個項目,意義類似的字段的取值各有不同。這簡直是項目維護的黑洞,Bug 的源泉。

所以,在代碼層面,一定要使用枚舉類型表示數據庫中的 Int 所代表的枚舉類型。所以,在本文的第一個代碼示例中,用戶狀態是用一個枚舉類表示的

...
UserStatus status;
...

在 Spring Data JDBC 和 MyBatis 中,都有相應的機制解決枚舉和 Int 轉換的問題,但并不是開箱即用,而是需要寫一些代碼。因為篇幅問題,本文就不做具體介紹,算是挖個小坑,留到下一篇文章講解。

四、總結

個人觀點,Spring 就像是 Java 開源界的暴雪。“Spring 出品,必屬精品。” 這么說其實不算很過分。

個人覺得,一方面,Spring 出品的項目,都是易用且功能強大的。更重要的是,Spring 項目的影響更多體現的設計和思想層面,總能引領某種風潮,這恐怕是 Spring 項目這么長時間以來,一直深受歡迎的原因。

因此,對與 Spring Data JDBC 這個項目,大家應更多關注。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容