Asentinel-orm是一个基于Spring JDBC构建的轻量级ORM工具,特别是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
能够自动生成查询的一部分,例如选择列的部分。通过SqlBuilder
类的方法,可以添加where子句、order by子句、其他条件和实际列。在本节的下一部分,将展示一个由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()
方法的两个参数版本被调用,它允许传递一个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()
方法生成查询的从表选择部分,而其余部分(where, order by)必须添加。查询选择部分可以通过传递EntityDescriptorNodeCallback
实例进行自定义(关于EntityDescriptorNodeCallback
的详细信息可能是未来文章的主题)。
为了让 ORM 知道计划是选择和映射运行时定义的列,需要传递一个 DynamicColumnsEntityNodeCallback
。同时,提供一个 AutoEagerLoader
,以便 ORM 理解要急切加载与制造商相关的 CarModel
s 列表。然而,这与运行时定义的属性无关,但它展示了如何急切加载子成员。
结论
虽然在关系数据库中存储数据时,可能还有其他处理运行时定义列的方法,但本文所提出的方法的优势在于使用标准数据库列,这些列通过 ORM 直接生成的标准 SQL 查询进行读写。
我们在“社区”中讨论 asentinel-orm 的机会并不罕见,讨论我们开发这种工具的原因。通常,开发者在面对定制的 ORM 时起初显得犹豫和保留,问为什么不使用 Hibernate 或其他 JPA 实现。
在我们的案例中,主要驱动因素是需要一种快速、灵活且易于处理的方式来处理有时相当多的运行时定义属性(列),这些属性属于业务领域的实体。对我们来说,这被证明是正确的方式。应用程序在生产环境中运行顺利,客户对速度和性能感到满意,开发者在直观的 API 下感到舒适和富有创造力。
项目现在是开源的,任何感兴趣的人都可以很容易地查看,形成客观的看法,并且,为什么不呢,可以进行分支,提交 PR,并做出贡献。
资源
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm