asentinel-ormは、Spring JDBC、特にJdbcTemplate
の上に構築された軽量ORMツールです。そのため、SQL生成、遅延読み込みなど、基本的なORMから期待されるほとんどの機能を備えています。
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操作を実行するための中心的なインタフェースであり、クライアントコードで特に実装する必要はありません。
サンプルアプリケーションには、このタイプのビーンを作成するための構成コードが含まれています。
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
には2つのスーパーインタフェースがあります:
- SqlBuilderFactory –
SqlBuilder
のインスタンスを作成し、さらにSQLクエリを作成するために使用できます。SqlBuilder
は、クエリの一部を自動生成することができます。たとえば、列を選択するクエリの一部を生成できます。where句、order by句、その他の条件、実際の列は、SqlBuilder
クラスのメソッドを使用して追加できます。このセクションの次の部分では、生成されたSqlBuilder
クエリの例を示します。 - Updater – エンティティをそれぞれのデータベーステーブルに保存するために使用されます。エンティティが新しく作成されたものか既に存在するものかに応じて、挿入または更新を実行できます。
NewEntityDetector
という戦略インタフェースが存在し、エンティティが新しいものかどうかを判定するために使用されます。デフォルトでは、SimpleNewEntityDetector
が使用されます。
ORMによって生成されたすべてのクエリは、SqlQueryTemplate
インスタンスを使用して実行され、さらにSpringのJdbcOperations
/JdbcTemplate
が必要です。最終的に、すべてのクエリは、Springトランザクションに参加しながら実行される古き良きJdbcTemplate
に到達します。これは、任意のJdbcTemplate
の直接実行と同様です。
データベース特有のSQL構文とロジックは、JdbcFlavor
インターフェースの実装を介して提供され、上記のほとんどのビーンに注入されます。本記事では、H2データベースが使用されるため、H2JdbcFlavor
実装が構成されています。
サンプルアプリケーションの一部としてのORMの完全な構成はOrmConfig
です。
実装
サンプルアプリケーションによって公開された実験的なドメインモデルは簡単明瞭で、2つのエンティティ – 自動車メーカーと 自動車モデルで構成されています。 名前が示す通りの関係は明白で、1つの自動車メーカーが複数の自動車モデルを所有する可能性があります。
自動車メーカーは、その名前に加えて、アプリケーションユーザーが実行時に動的に入力する属性(カラム)で強化されています。示されたユースケースは非常に単純です:
- ユーザーには、動的属性の目的の名前とタイプを提供するように依頼されています
- いくつかの自動車メーカーが、以前に追加された動的属性に具体的な値で作成され、その後
- エンティティは、初期属性とランタイムで定義された属性の両方によって記述された状態でロードされます
初期エンティティは、以下のデータベーステーブルを使用してマッピングされます:
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
タイプのみがサポートされています)。
名前とタイプのペアごとに、新しい列をCarManufacturer
データベーステーブルに追加するために、DML コマンドが実行されます。次のメソッド(CarService
で宣言)がこの操作を実行します。
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
各入力属性は、DefaultDynamicColumn
、DynamicColumn
のリファレンス実装として記録されます。
すべての属性が定義されると、ユーザーがそれぞれの属性に値を提供することで、2つのカーメーカーがデータベースに追加されます。
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に伝えます。
最後に、以前追加された自動車メーカーの1つに対応する2つの車モデルが作成されます。
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
以下のメソッド(CarService
で宣言された)は、実際にORMを通じてエンティティを作成し、この時は動的属性なしでエンティティを永続化するためにOrmOperations
のupdate()メソッドを使用します。便利さのために、1回の呼び出しで複数のエンティティが作成されます。
public void createModels(CarModel... models) {
orm.update(models);
}
最後のステップとして、作成されたメーカーの1つが、その名前を使用して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 のリストをイagerにロードすることを理解します。しかしながら、これは実行時に定義された属性とは関係がありませんが、子メンバーがどのようにイagerにロードされるかを示しています。
結論
データがリレーショナルデータベースに格納されている場合に実行時に定義された列を扱う他の方法があるかもしれませんが、この記事で提示されたアプローチは、ORMによって直接生成された標準SQLクエリを使用して読み書きされる標準データベース列を使用するという利点があります。
「コミュニティ」でasentinel-ormや、そのようなツールを開発する理由について議論する機会があったことは珍しくありませんでした。通常、一見したところ、開発者はカスタムメイドのORMに対して消極的で控えめな姿勢を示し、なぜHibernateや他のJPA実装を使わないのかと尋ねました。
私たちの場合、主な動機は、ビジネスドメインの一部であるエンティティのために、時にはかなりの数の実行時に定義された属性(列)を迅速かつ柔軟かつ簡単に扱う必要があったことです。私たちにとって、それは正しい方法であることが証明されました。アプリケーションは本番環境でスムーズに実行されており、顧客はスピードと達成されたパフォーマンスに満足しており、開発者は直感的なAPIを使って快適で創造的です。
プロジェクトがオープンソースになったため、興味のある誰でも簡単に見て、客観的な意見を形成し、フォークし、PRを開き、貢献することができます。
リソース
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm