Asentinel-orm ist ein leichtgewichtiges ORM-Tool, das auf Spring JDBC, insbesondere JdbcTemplate
, aufbaut. Somit verfügt es über die meisten Funktionen, die man von einem grundlegenden ORM erwarten würde, wie z. B. SQL-Generierung, Lazy Loading, etc.
Durch die Nutzung des JdbcTemplate
ermöglicht es die Teilnahme an von Spring verwalteten Transaktionen und kann leicht in jedes Projekt integriert werden, das bereits JdbcTemplate
zur Interaktion mit der Datenbank verwendet.
Seit 2015 wurde asentinel-orm erfolgreich in mehreren Anwendungen eingesetzt und kontinuierlich gemäß den geschäftlichen Anforderungen verbessert. Im Sommer 2024 wurde es offiziell zu einem Open-Source-Projekt, was wir erwarten, dass seine Entwicklung beschleunigen und die Anzahl der Beitragenden erhöhen wird.
In diesem Artikel wird eine Beispielanwendung erstellt, um mehrere wichtige ORM-Funktionen zu erläutern:
- Einfache Konfiguration
- Klare Modellierung von Domain-Entitäten mittels benutzerdefinierter Annotationen
- Einfaches Schreiben und sicheres Ausführen einfacher SQL-Anweisungen
- Automatische Generierung von SQL-Anweisungen
- Dynamisches Schema (Entitäten werden um zusätzliche Laufzeitattribute erweitert, persistiert und ohne Codeänderungen gelesen)
Anwendungs
Setup
- Java 21
- Spring Boot 3.4.0
- asentinel-orm 1.70.0
- H2-Datenbank
Konfiguration
Um mit dem asentinel-orm zu interagieren und seine Funktionen zu nutzen, wird eine Instanz von OrmOperations
benötigt.
Wie im JavaDoc angegeben, handelt es sich hierbei um die zentrale Schnittstelle für die Ausführung von ORM-Operationen, und es ist weder beabsichtigt noch erforderlich, dass sie speziell im Client-Code implementiert wird.
Die Beispielanwendung enthält den Konfigurationscode zum Erstellen eines Beans dieses Typs.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
hat zwei Super-Schnittstellen:
- SqlBuilderFactory – erstellt
SqlBuilder
-Instanzen, die weiterhin zur Erstellung von SQL-Abfragen verwendet werden können.SqlBuilder
kann Teile der Abfrage automatisch generieren, z.B. die, die die Spalten auswählt. Die Where-Klausel, die Order-By-Klausel, andere Bedingungen und die tatsächlichen Spalten können ebenfalls mit Methoden aus der KlasseSqlBuilder
hinzugefügt werden. Im nächsten Teil dieses Abschnitts wird ein Beispiel für eine mitSqlBuilder
generierte Abfrage gezeigt. - Updater – wird verwendet, um Entitäten in ihre jeweiligen Datenbanktabellen zu speichern. Es kann Einfügungen oder Aktualisierungen durchführen, je nachdem, ob die Entität neu erstellt wurde oder bereits vorhanden ist. Es existiert eine Strategie-Schnittstelle namens
NewEntityDetector
, die verwendet wird, um festzustellen, ob eine Entität neu ist. Standardmäßig wird derSimpleNewEntityDetector
verwendet.
Alle vom ORM generierten Abfragen werden mithilfe einer SqlQueryTemplate
Instanz ausgeführt, die zusätzlich eine Spring JdbcOperations
/JdbcTemplate
benötigt, um zu funktionieren. Letztendlich erreichen alle Abfragen das gute alte JdbcTemplate
, über das sie ausgeführt werden, während sie an Spring-Transaktionen teilnehmen, genau wie jede JdbcTemplate
direkte Ausführung.
Datenbankspezifische SQL-Konstrukte und Logik werden über Implementierungen des JdbcFlavor
-Interfaces bereitgestellt, die in die meisten der oben genannten Beans injiziert werden. In diesem Artikel wird eine H2JdbcFlavor
-Implementierung konfiguriert, da eine H2-Datenbank verwendet wird.
Die vollständige Konfiguration des ORM als Teil der Beispielanwendung ist OrmConfig
.
Implementierung
Das experimentelle Domänenmodell, das von der Beispielanwendung bereitgestellt wird, ist einfach und besteht aus zwei Entitäten – Automobilhersteller und Automodelle. Es repräsentiert genau das, was ihre Namen bedeuten; die Beziehung zwischen ihnen ist offensichtlich: Ein Automobilhersteller kann mehrere Automodelle besitzen.
Neben seinem Namen wird der Automobilhersteller mit Attributen (Spalten) angereichert, die vom Anwendungsbenutzer zur Laufzeit dynamisch eingegeben werden. Der veranschaulichte Anwendungsfall ist unkompliziert:
- Der Benutzer wird gebeten, die angestrebten Namen und Typen für die dynamischen Attribute bereitzustellen
- Einige Automobilhersteller werden mit konkreten Werten für zuvor hinzugefügte dynamische Attribute erstellt, und dann
- Die Entitäten werden unter Verwendung sowohl der ursprünglichen als auch der zur Laufzeit definierten Attribute wieder geladen
Die ursprünglichen Entitäten werden unter Verwendung der untenstehenden Datenbanktabellen gemappt:
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)
);
Die entsprechenden Domänenklassen sind mit ORM-spezifischen Annotationen dekoriert, um die Zuordnungen zu den oben genannten Datenbanktabellen zu konfigurieren.
"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
}
Einige Überlegungen:
@Table
– verknüpft (ordnet) die Klasse mit einer Datenbanktabelle@PkColumn
– verknüpft dieid
(eindeutiger Bezeichner) mit dem Primärschlüssel der Tabelle@Column
– verknüpft ein Klassenmitglied mit einer Spalte der Tabelle@Child
– definiert die Beziehung zu einer anderen Entität@Child
-annotierte Mitglieder – konfiguriert, um faul geladen zu werdentype
Spalte der Tabelle – zu einemenum
Feld zugeordnet –CarType
Damit die CarManufacturer
-Klasse zur Unterstützung von zur Laufzeit definierten Attributen (die auf zur Laufzeit definierten Tabellenspalten abgebildet werden) geeignet ist, wird eine Unterklasse wie die folgende definiert:
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);
}
...
}
Diese Klasse speichert die zur Laufzeit definierten Attribute (Felder) in einer Map
. Die Interaktion zwischen den Laufzeitfeldwerten und dem ORM erfolgt über die Implementierung des DynamicColumnEntity
Interfaces.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– wird verwendet, um den Wert der zur Laufzeit definierten Spalte festzulegen, wenn diese aus der Tabelle gelesen wird.getValue()
– wird verwendet, um den Wert einer zur Laufzeit definierten Spalte abzurufen, wenn diese in die Tabelle gespeichert wird
Der DynamicColumn
ordnet zur Laufzeit definierte Attribute ihren entsprechenden Spalten auf ähnliche Weise zu, wie die @Column
Annotation bekannte Klassenmitglieder zur Kompilierungszeit zuordnet.
Beim Ausführen der Anwendung wird der CfRunner
ausgeführt. Der Benutzer wird aufgefordert, Namen und Typen für die gewünschten dynamischen benutzerdefinierten Attribute einzugeben, die die CarManufacturer
Entität bereichern (zur Vereinfachung werden nur int
und varchar
Typen unterstützt).
Für jedes Namens-Typ-Paar wird ein DML-Befehl ausgeführt, damit die neuen Spalten der CarManufacturer
Datenbanktabelle hinzugefügt werden können. Die folgende Methode (deklariert in CarService
) führt die Operation aus.
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
Jedes Eingabeattribut wird als DefaultDynamicColumn
, eine Referenzimplementierung von DynamicColumn
aufgezeichnet.
Sobald alle Attribute definiert sind, werden zwei Automobilhersteller zur Datenbank hinzugefügt, da der Benutzer Werte für jedes solche Attribut angibt.
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);
Die untenstehende Methode (deklariert in CarService
) erstellt tatsächlich die Entität über das ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
Die 2-Parameter-Version der OrmOperations
update()
-Methode wird aufgerufen, die es ermöglicht, eine UpdateSettings
-Instanz zu übergeben und dem ORM bei der Ausführung mitzuteilen, dass es runtime-definierte Werte gibt, die persistiert werden sollen.
Zuletzt werden zwei Automodelle erstellt, die einem der zuvor hinzugefügten Automobilhersteller entsprechen.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
Die folgende Methode (deklariert in CarService
) erstellt tatsächlich die Entitäten über das ORM, diesmal unter Verwendung der OrmOperations
-Methode update() zum Persistieren von Entitäten ohne dynamische Attribute. Der Einfachheit halber werden mehrere Entitäten in einem Aufruf erstellt.
public void createModels(CarModel... models) {
orm.update(models);
}
Als letzter Schritt wird einer der erstellten Hersteller anhand seines Namens über eine vom ORM generierte Abfrage zurückgeladen.
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();
}
Einige Erklärungen zur oben definierten Methode sind es wert, gemacht zu werden.
Die OrmOperations
newSqlBuilder()
-Methode erstellt eine SqlBuilder
-Instanz, und wie der Name schon sagt, kann dies verwendet werden, um SQL-Abfragen zu generieren. Die SqlBuilder
select()
-Methode generiert den select from table-Teil der Abfrage, während der Rest (where, order by) hinzugefügt werden muss. Der Select-Teil der Abfrage kann angepasst werden, indem EntityDescriptorNodeCallback
-Instanzen übergeben werden (Details zu EntityDescriptorNodeCallback
könnten Gegenstand eines zukünftigen Artikels sein).
Um dem ORM mitzuteilen, dass der Plan darin besteht, zur Laufzeit definierte Spalten auszuwählen und zuzuordnen, muss ein DynamicColumnsEntityNodeCallback
übergeben werden. Zusammen mit ihm wird ein AutoEagerLoader
bereitgestellt, damit das ORM versteht, dass die Liste der CarModel
s , die mit dem Hersteller verbunden sind, frühzeitig geladen werden soll. Dennoch hat dies nichts mit den zur Laufzeit definierten Attributen zu tun, sondern zeigt, wie ein untergeordnetes Mitglied frühzeitig geladen werden kann.
Fazit
Während es wahrscheinlich andere Möglichkeiten gibt, mit zur Laufzeit definierten Spalten zu arbeiten, wenn Daten in relationalen Datenbanken gespeichert sind, hat der in diesem Artikel vorgestellte Ansatz den Vorteil, dass er Standarddatenbanksäulen verwendet, die mit standardmäßigen SQL-Abfragen gelesen/geschrieben werden, die direkt vom ORM generiert werden.
Es war nicht selten, dass wir die Gelegenheit hatten, in „der Gemeinschaft“ über das asentinel-orm zu diskutieren, über die Gründe, warum wir ein solches Tool entwickeln mussten. In der Regel waren die Entwickler auf den ersten Blick zögerlich und zurückhaltend, wenn es um ein maßgeschneidertes ORM ging, und fragten, warum nicht Hibernate oder andere JPA-Implementierungen verwendet werden sollten.
In unserem Fall war der Hauptantrieb die Notwendigkeit, eine schnelle, flexible und einfache Möglichkeit zu haben, mit manchmal recht vielen zur Laufzeit definierten Attributen (Spalten) für Entitäten zu arbeiten, die Teil des Geschäftsdomain sind. Für uns erwies sich dies als der richtige Weg. Die Anwendungen laufen reibungslos in der Produktion, die Kunden sind mit der Geschwindigkeit und der erreichten Leistung zufrieden, und die Entwickler fühlen sich wohl und kreativ mit der intuitiven API.
Da das Projekt jetzt Open Source ist, ist es für jeden Interessierten sehr einfach, einen Blick darauf zu werfen, sich eine objektive Meinung darüber zu bilden und, warum nicht, es zu forken, einen PR zu öffnen und beizutragen.
Ressourcen
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm