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的配置代碼。
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
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
。
實現
範例應用程式所暴露的實驗性領域模型是簡單明瞭的,由兩個實體組成——汽車製造商和 汽車型號。 它們的名稱正好代表了其含義,兩者之間的關係顯而易見:一個汽車製造商可以擁有多個汽車型號。
除了其名稱外,汽車製造商還增強了在運行時由應用程式使用者動態輸入的屬性(列)。所示例的使用案例非常直接:
- 用戶被要求提供動態屬性的目標名稱和類型
- 創建幾個汽車製造商,並為之前添加的動態屬性提供具體值,然後
- 實體被重新加載,並由初始屬性和運行時定義的屬性描述
初始實體使用以下數據庫表進行映射:
CREATE TABLE CarManufacturers (
ID INT auto_increment PRIMARY KEY,
NAME VARCHAR(255)
);
CREATE TABLE CarModels(
ID INT auto_increment PRIMARY KEY,
CarManufacturer int,
NAME VARCHAR(255),
TYPE VARCHAR(15),
foreign key (CarManufacturer) references CarManufacturers(id)
);
相應的領域類使用 ORM 特定的註釋來配置與上述數據庫表的映射。
"CarManufacturers") (
public class CarManufacturer {
"id") (
private int id;
"name") (
private String name;
parentRelationType = RelationType.MANY_TO_ONE, (
fkName = CarModel.COL_CAR_MANUFACTURER,
fetchType = FetchType.LAZY)
private List<CarModel> models = Collections.emptyList();
...
}
"CarModels") (
public class CarModel {
public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
"id") (
private int id;
"name") (
private String name;
"type") (
private CarType type;
fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY) (
private CarManufacturer carManufacturer;
...
}
public enum CarType {
CAR, SUV, TRUCK
}
幾點考量:
@Table
– 將類映射(關聯)到數據庫表@PkColumn
– 將id
(唯一標識符)映射到表的主鍵@Column
– 將類成員映射到表列@Child
– 定義與另一個實體的關係@Child
註釋的成員 – 配置為延遲加載type
表列 – 映射到enum
字段 –CarType
為了使 CarManufacturer
類支持運行時定義的屬性(映射到運行時定義的表列),定義如下的子類:
public class CustomFieldsCarManufacturer extends CarManufacturer
implements DynamicColumnsEntity<DynamicColumn> {
private final Map<DynamicColumn, Object> customFields = new HashMap<>();
...
public void setValue(DynamicColumn column, Object value) {
customFields.put(column, value);
}
public Object getValue(DynamicColumn column) {
return customFields.get(column);
}
...
}
這個類 將運行時定義的屬性(字段)存儲在 Map
中。運行時字段值與 ORM 之間的交互通過實現 DynamicColumnEntity
介面來完成。
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– 用於在從表格讀取時設定運行時定義的列的值getValue()
– 用於在保存到表格時檢索運行時定義列的值
DynamicColumn
將運行時定義的屬性與其對應的列進行類似地映射,就像@Column
註釋將編譯時已知成員進行映射一樣。
執行應用程序時,將執行CfRunner
。要求用戶輸入所需的動態自定義屬性的名稱和類型,以豐富CarManufacturer
實體(為簡單起見,僅支持int
和varchar
類型)。
對於每個名稱-類型對,將執行一個DML命令,以便將新列添加到CarManufacturer
數據庫表中。以下方法(在CarService
中聲明)執行此操作。
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
每個輸入屬性都記錄為DefaultDynamicColumn
,一個DynamicColumn
參考實現。
一旦定義了所有屬性,將兩個汽車製造商添加到數據庫中,因為用戶為每個此類屬性提供值。
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
// read values for each dynamic attribute
...
}
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
carService.createManufacturer(mazda, dynamicColumns);
以下方法(在CarService
中聲明)實際上通過ORM創建實體。
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
OrmOperations
的 update()
方法的2参数版本被调用,允许传递一个UpdateSettings
实例,并在执行时与ORM通信,以表明存在将被持久化的运行时定义的值。
最后,创建了两个汽车型号,对应之前添加的一个汽车制造商。
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
下面的方法(在CarService
中声明)实际上通过ORM创建实体,这次使用OrmOperations
的update()方法来持久化没有动态属性的实体。为了方便起见,在一次调用中创建多个实体。
public void createModels(CarModel... models) {
orm.update(models);
}
作为最后一步,通过ORM生成的查询,根据其名称加载回创建的制造商之一。
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
readOnly = true) (
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
.select(
AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
new DynamicColumnsEntityNodeCallback<>(
new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
attributes
)
)
.where().column("name").eq(name)
.execForEntity();
}
值得对上面定义的方法进行一些解释。
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,並做出貢獻。
資源
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm