Runtime-Gedefinieerde Kolommen Met asentinel-orm

asentinel-orm is een lichtgewicht ORM-tool gebouwd bovenop Spring JDBC, met name JdbcTemplate. Het beschikt dus over de meeste functies die men zou verwachten van een basis ORM, zoals SQL-generatie, lazy loading, enz.

Door gebruik te maken van JdbcTemplate, betekent dit dat het deelname aan Spring-beheerde transacties mogelijk maakt, en het kan eenvoudig worden geïntegreerd in elk project dat al JdbcTemplate gebruikt als een middel om met de database te communiceren.

Sinds 2015 is asentinel-orm met succes gebruikt in verschillende applicaties en voortdurend verbeterd zoals vereist door zakelijke behoeften. In de zomer van 2024 werd het officieel een open-source project, waarvan wij geloven dat het de evolutie zal versnellen en het aantal bijdragers zal vergroten.

In dit artikel wordt een voorbeeldtoepassing gebouwd om verschillende belangrijke kenmerken van ORM te schetsen:

  • Simpele configuratie
  • Eenvoudige modellering van domeinen met behulp van aangepaste annotaties
  • Gemakkelijk schrijven en veilig uitvoeren van eenvoudige SQL-instructies
  • Automatische generatie van SQL-instructies
  • Dynamisch schema (entiteiten worden verrijkt met extra runtime-attributen, opgeslagen en gelezen zonder codewijzigingen)

Toepassing

Setup

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • H2-database

Configuratie

Om te communiceren met de asentinel-orm en gebruik te maken van zijn functionaliteiten, is een instantie van OrmOperations  vereist.

Zoals vermeld in de JavaDoc, is dit  de centrale interface voor het uitvoeren van ORM-operaties, en het is niet de bedoeling noch vereist om specifiek geïmplementeerd te worden in de clientcode.

De voorbeeldtoepassing bevat de configuratiecode om een bean van dit type te maken.

Java

 

OrmOperations  heeft twee superinterfaces:

  • SqlBuilderFactory – creëert SqlBuilder  instanties die verder gebruikt kunnen worden om SQL-query’s te maken. SqlBuilder kan delen van de query automatisch genereren, zoals bijvoorbeeld degene die de kolommen selecteert. De where-clausule, de order by-clausule, andere voorwaarden en de daadwerkelijke kolommen kunnen ook toegevoegd worden met behulp van methoden van de klasse SqlBuilder. In het volgende deel van dit gedeelte wordt een voorbeeld van een door SqlBuilder gegenereerde query getoond.
  • Updater – gebruikt voor het opslaan van entiteiten in hun respectievelijke databasetabellen. Het kan invoegingen of updates uitvoeren, afhankelijk van of de entiteit nieuw is aangemaakt of al bestaat. Er bestaat een strategie-interface genaamd NewEntityDetector , die wordt gebruikt om te bepalen of een entiteit nieuw is. Standaard wordt de SimpleNewEntityDetector  gebruikt.

Alle queries die door de ORM worden gegenereerd, worden uitgevoerd met behulp van een SqlQueryTemplate instantie, die verder een Spring JdbcOperations/JdbcTemplate nodig heeft om te functioneren. Uiteindelijk bereiken alle queries de goede oude JdbcTemplate waardoor ze worden uitgevoerd terwijl ze deelnemen aan Spring-transacties, net als elke JdbcTemplate directe uitvoering.

Database-specifieke SQL-constructies en logica worden geleverd via implementaties van de JdbcFlavor interface, die verder in de meeste van de hierboven genoemde beans worden geïnjecteerd. In dit artikel, aangezien een H2-database wordt gebruikt, is een H2JdbcFlavor implementatie geconfigureerd.

De complete configuratie van de ORM als onderdeel van de voorbeeldapplicatie is OrmConfig.

Implementatie

Het experimentele domeinmodel dat door de voorbeeldapplicatie wordt blootgesteld, is eenvoudig en bestaat uit twee entiteiten – autofabrikanten en automodellen. Het vertegenwoordigt precies wat hun namen aanduiden, de relatie tussen hen is duidelijk: één autofabrikant kan meerdere automodellen bezitten.

Naast zijn naam is de autofabrikant verrijkt met attributen (kolommen) die dynamisch door de applicatiegebruiker tijdens runtime worden ingevoerd. Het voorbeeldgebruik is rechttoe rechtaan:

  • De gebruiker wordt verzocht om de gewenste namen en types voor de dynamische attributen
  • Een paar autofabrikanten worden gemaakt met concrete waarden voor eerder toegevoegde dynamische attributen, en dan
  • De entiteiten worden weer geladen, beschreven door zowel de initiële als de runtime-gedefinieerde attributen

De initiële entiteiten worden gemapt met behulp van de onderstaande databasetabellen:

SQL

 

De bijbehorende domeinklassen zijn versierd met ORM-specifieke annotaties om de mappings naar de bovenstaande databasetabellen te configureren.

Java

 

Java

 

Een paar overwegingen:

  • @Table – koppelt (associeert) de klasse aan een databasetabel
  • @PkColumn – koppelt de id (unieke identificatie) aan de primaire sleutel van de tabel
  • @Column – koppelt een klasse lid aan een tabelkolom 
  • @Child – definieert de relatie met een andere entiteit
  • @Child geannoteerde leden – geconfigureerd om lui geladen te worden
  • type tabelkolom – gekoppeld aan een enum veld – CarType

Om de CarManufacturer klasse ondersteuning te bieden voor runtime-gedefinieerde attributen (gekoppeld aan runtime-gedefinieerde tabelkolommen), wordt een subklasse zoals hieronder gedefinieerd:

Java

 

Deze klasse bewaart de runtime-gedefinieerde attributen (velden) in een Map. De interactie tussen de runtime veldwaarden en de ORM wordt voldaan via de implementatie van de DynamicColumnEntity interface.

Java

 

  • setValue() – wordt gebruikt om de waarde van de runtime-gedefinieerde kolom in te stellen wanneer deze wordt gelezen uit de tabel
  • getValue() – wordt gebruikt om de waarde van een runtime-gedefinieerde kolom op te halen wanneer deze wordt opgeslagen in de tabel

De DynamicColumn maakt runtime-gedefinieerde attributen overeen met hun overeenkomstige kolommen op een vergelijkbare manier als de @Column annotatie compileert leden met bekende tijd.

Tijdens het uitvoeren van de toepassing wordt de CfRunner uitgevoerd. De gebruiker wordt gevraagd namen en typen in te voeren voor de gewenste dynamische aangepaste attributen die de CarManufacturer entiteit verrijken (voor eenvoud, alleen int en varchar typen worden ondersteund).

Voor elk naam-typen paar wordt een DML-opdracht uitgevoerd zodat de nieuwe kolommen aan de database-tabel CarManufacturer kunnen worden toegevoegd. De volgende methode (gedeclareerd in CarService) voert de bewerking uit.

Java

 

Elk invoerattribuut wordt geregistreerd als een DefaultDynamicColumn, een DynamicColumn referentie-implementatie.

Zodra alle attributen zijn gedefinieerd, worden twee autofabrikanten toegevoegd aan de database, omdat de gebruiker waarden opgeeft voor elk attribuut.

Java

 

De onderstaande methode (gedeclareerd in CarService) maakt daadwerkelijk de entiteit via de ORM.

Java

 

De 2-parameter versie van de OrmOperations update() methode wordt aangeroepen, waardoor het mogelijk is om een UpdateSettings instantie door te geven en bij uitvoering aan de ORM te communiceren dat er runtime-gedefinieerde waarden zijn die moeten worden opgeslagen.

Tenslotte worden twee automodellen aangemaakt, die overeenkomen met een van de eerder toegevoegde autofabrikanten.

Java

 

De onderstaande methode (gedeclareerd in CarService) maakt de entiteiten daadwerkelijk aan via de ORM, dit keer met behulp van de OrmOperations update() methode om entiteiten op te slaan zonder dynamische attributen. Voor het gemak worden meerdere entiteiten in één keer aangemaakt.

Java

 

Als laatste stap wordt een van de aangemaakte fabrikanten terug geladen op basis van de naam met behulp van een door de ORM gegenereerde query.

Java

 

Enkele verklaringen met betrekking tot de hierboven gedefinieerde methode zijn de moeite waard om te doen.

De OrmOperations newSqlBuilder() methode maakt een SqlBuilder instantie aan, en zoals de naam al aangeeft, kan dit worden gebruikt om SQL queries te genereren. De SqlBuilder select() methode genereert het select from table gedeelte van de query, terwijl de rest (where, order by) moet worden toegevoegd. Het select gedeelte van de query kan worden aangepast door EntityDescriptorNodeCallback instanties door te geven (details over EntityDescriptorNodeCallback kunnen onderwerp zijn van een toekomstig artikel).

Om de ORM op de hoogte te stellen dat het plan is om runtimegedefinieerde kolommen te selecteren en in kaart te brengen, moet een DynamicColumnsEntityNodeCallback worden doorgegeven. Samen daarmee wordt een AutoEagerLoader geleverd zodat de ORM begrijpt dat de lijst met CarModel’s die geassocieerd zijn met de fabrikant gretig geladen moet worden. Dit heeft echter niets te maken met de runtimegedefinieerde attributen, maar het laat wel zien hoe een kindlid gretig geladen kan worden.

Conclusie

Hoewel er waarschijnlijk andere manieren zijn om te werken met runtimegedefinieerde kolommen wanneer gegevens zijn opgeslagen in relationele databases, heeft de aanpak die in dit artikel wordt gepresenteerd het voordeel dat er standaard databasekolommen worden gebruikt die worden gelezen/schreven met standaard SQL-query’s die rechtstreeks worden gegenereerd door de ORM.

Het was niet zeldzaam dat we in “de gemeenschap” de asentinel-orm bespraken, de redenen die we hadden om zo’n tool te ontwikkelen. Meestal waren ontwikkelaars aanvankelijk terughoudend en gereserveerd als het ging om een op maat gemaakte ORM, en vroegen zich af waarom we geen Hibernate of andere JPA-implementaties gebruikten.

In ons geval was de belangrijkste drijfveer de behoefte aan een snelle, flexibele en eenvoudige manier om soms een behoorlijk aantal runtimegedefinieerde attributen (kolommen) voor entiteiten die deel uitmaken van het bedrijfsdomein te verwerken. Voor ons bleek dit de juiste weg te zijn. De applicaties draaien soepel in productie, de klanten zijn tevreden over de snelheid en de behaalde prestaties, en de ontwikkelaars voelen zich comfortabel en creatief met de intuïtieve API.

Nu het project open source is, is het heel gemakkelijk voor iedereen die geïnteresseerd is om een kijkje te nemen, een objectieve mening te vormen en, waarom niet, het te forken, een PR te openen en bij te dragen.

Resources

  • Het open-source ORM-project is hier.
  • De broncode van de voorbeeldtoepassing is hier.

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