使用 asentinel-orm 的運行時定義列

asentinel-orm 是一個輕量級的 ORM 工具,基於 Spring JDBC,特別是 JdbcTemplate。因此,它擁有基本 ORM 所期望的大多數功能,例如 SQL 生成、延遲加載等。

通過利用 JdbcTemplate,這意味著它允許參與 Spring 管理的事務,並且可以輕鬆地整合到任何已經使用 JdbcTemplate 與數據庫互動的項目中。

自 2015 年以來,asentinel-orm 已在幾個應用程序中成功使用,並根據業務需求不斷改進。2024 年夏季,它正式成為 一個開源項目,我們認為這將加速其演變並增加貢獻者的數量。

在本文中,構建了一個示例應用程序,以概述幾個 ORM 主要特徵:

  • 簡單配置
  • 通過自定義註釋進行直接的領域實體建模
  • 輕鬆編寫和安全執行純 SQL 語句
  • 自動生成 SQL 語句
  • 動態架構(實體在運行時增加額外屬性,持久化並在不改變代碼的情況下讀取)

應用程序

設置

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • H2 數據庫

配置

為了與asentinel-orm互動並利用其功能,需要一個OrmOperations 的實例。

如JavaDoc中所述,這 是執行ORM操作的中央接口,並且不打算也不需要在客戶端代碼中特定實現。

示例應用程序包括創建此類型bean的配置代碼。

Java

 

OrmOperations 有兩個超級接口:

  • SqlBuilderFactory – 創建SqlBuilder 實例,可以進一步用於創建SQL查詢。 SqlBuilder能夠自動生成查詢的部分,例如選擇列的部分。 where子句,order by子句,其他條件以及實際列也可以使用SqlBuilder類的方法添加。在本節的下一部分中,將展示一個SqlBuilder生成的查詢示例。
  • Updater – 用於將實體保存到它們各自的數據庫表中。它可以執行插入或更新,具體取決於實體是新創建的還是已經存在。存在一個名為NewEntityDetector 的策略接口,用於確定實體是否是新的。默認情況下,使用SimpleNewEntityDetector

所有由 ORM 生成的查詢都是使用 SqlQueryTemplate 實例執行的,這進一步需要一個 Spring JdbcOperations/JdbcTemplate 來運作。最終,所有查詢通過良好的舊 JdbcTemplate 被執行,同時參與 Spring 交易,就像任何 JdbcTemplate 的直接執行.

特定於資料庫的 SQL 結構和邏輯是通過 JdbcFlavor 接口的實現提供的,並進一步注入到上述大多數 bean 中。在本文中,由於使用 H2 資料庫,因此配置了一個 H2JdbcFlavor 實現。

作為範例應用程式的一部分,ORM 的完整配置是 OrmConfig

實現

範例應用程式所暴露的實驗性領域模型是簡單明瞭的,由兩個實體組成——汽車製造商和 汽車型號。 它們的名稱正好代表了其含義,兩者之間的關係顯而易見:一個汽車製造商可以擁有多個汽車型號。

除了其名稱外,汽車製造商還增強了在運行時由應用程式使用者動態輸入的屬性(列)。所示例的使用案例非常直接:

  • 用戶被要求提供動態屬性的目標名稱和類型
  • 創建幾個汽車製造商,並為之前添加的動態屬性提供具體值,然後
  • 實體被重新加載,並由初始屬性和運行時定義的屬性描述

初始實體使用以下數據庫表進行映射:

SQL

 

相應的領域類使用 ORM 特定的註釋來配置與上述數據庫表的映射。

Java

 

Java

 

幾點考量:

  • @Table – 將類映射(關聯)到數據庫表
  • @PkColumn – 將 id(唯一標識符)映射到表的主鍵
  • @Column – 將類成員映射到表列
  • @Child – 定義與另一個實體的關係
  • @Child 註釋的成員 – 配置為延遲加載
  • type 表列 – 映射到 enum 字段 – CarType

為了使 CarManufacturer 類支持運行時定義的屬性(映射到運行時定義的表列),定義如下的子類:

Java

 

這個類 將運行時定義的屬性(字段)存儲在 Map 中。運行時字段值與 ORM 之間的交互通過實現 DynamicColumnEntity 介面來完成。

Java

 

  • setValue() – 用於在從表格讀取時設定運行時定義的列的值
  • getValue() – 用於在保存到表格時檢索運行時定義列的值

DynamicColumn 將運行時定義的屬性與其對應的列進行類似地映射,就像@Column 註釋將編譯時已知成員進行映射一樣。

執行應用程序時,將執行CfRunner。要求用戶輸入所需的動態自定義屬性的名稱和類型,以豐富CarManufacturer 實體(為簡單起見,僅支持intvarchar 類型)。 

對於每個名稱-類型對,將執行一個DML命令,以便將新列添加到CarManufacturer 數據庫表中。以下方法(在CarService中聲明)執行此操作。

Java

 

每個輸入屬性都記錄為DefaultDynamicColumn,一個DynamicColumn 參考實現。

一旦定義了所有屬性,將兩個汽車製造商添加到數據庫中,因為用戶為每個此類屬性提供值。

Java

 

以下方法(在CarService中聲明)實際上通過ORM創建實體。

Java

 

OrmOperations update()方法的2参数版本被调用,允许传递一个UpdateSettings实例,并在执行时与ORM通信,以表明存在将被持久化的运行时定义的值。

最后,创建了两个汽车型号,对应之前添加的一个汽车制造商。

Java

 

下面的方法(在CarService中声明)实际上通过ORM创建实体,这次使用OrmOperations的update()方法来持久化没有动态属性的实体。为了方便起见,在一次调用中创建多个实体。

Java

 

作为最后一步,通过ORM生成的查询,根据其名称加载回创建的制造商之一。

Java

 

值得对上面定义的方法进行一些解释。

OrmOperations newSqlBuilder() 方法创建一个SqlBuilder实例,顾名思义,可用于生成SQL查询。SqlBuilder select() 方法生成查询的select from table部分,而其余部分(where, order by)必须添加。查询的select部分可以通过传递EntityDescriptorNodeCallback 实例来定制(关于EntityDescriptorNodeCallback的详细信息可能是未来文章的主题)。

為了讓 ORM 知道計劃是選擇並映射運行時定義的列,需要傳遞一個 DynamicColumnsEntityNodeCallback 。與此一起,提供了一個 AutoEagerLoader ,以便 ORM 明白要急切加載與製造商相關的 CarModel 列表。然而,這與運行時定義的屬性無關,但它展示了如何急切加載子成員。

結論

雖然在關係型資料庫中處理運行時定義的列可能還有其他方式,但本文所提出的方法的優勢在於使用標準的資料庫列,這些列是通過 ORM 直接生成的標準 SQL 查詢進行讀取/寫入的。

當我們有機會在「社群」中討論 asentinel-orm 及開發這種工具的原因時,這並不罕見。通常,乍一看,開發者在面對自定義 ORM 時顯得不情願和保留,詢問為何不使用 Hibernate 或其他 JPA 實現。

在我們的情況下,主要驅動因素是需要一種快速、靈活且容易的方式來處理業務領域中某些實體的運行時定義屬性(列)的數量,這些屬性有時相當多。對我們來說,這被證明是正確的方式。應用程序在生產環境中運行平穩,客戶對速度和實現的性能感到滿意,開發者則對直觀的 API 感到舒適和創造性。

由於該項目現在是開源的,任何感興趣的人都可以輕鬆查看,形成客觀意見,並且,為什麼不呢,fork 它,開啟一個 PR,並做出貢獻。

資源

  • 這個開源 ORM 項目在這裡
  • 範例應用程式的原始碼在這裡

Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm