Asentinel-orm è un leggero strumento ORM costruito sopra Spring JDBC, in particolare JdbcTemplate
. Pertanto, possiede la maggior parte delle caratteristiche che ci si aspetterebbe da un ORM di base, come la generazione di SQL, il lazy loading, ecc.
Sfruttando il JdbcTemplate
, consente di partecipare alle transazioni gestite da Spring e può essere facilmente integrato in qualsiasi progetto che già utilizzi JdbcTemplate
come metodo per interagire con il database.
Dal 2015, asentinel-orm è stato utilizzato con successo in diverse applicazioni e continuamente migliorato secondo le esigenze aziendali. Nell’estate del 2024, è diventato un progetto open-source, il che riteniamo accelererà la sua evoluzione e aumenterà il numero di contributori.
In questo articolo, viene costruita un’applicazione di esempio per delineare diverse funzionalità chiave dell’ORM:
- Configurazione semplice
- Modellazione diretta dell’entità di dominio tramite annotazioni personalizzate
- Scrittura facile ed esecuzione sicura di istruzioni SQL semplici
- Generazione automatica di istruzioni SQL
- Schema dinamico (le entità sono arricchite con attributi aggiuntivi in fase di esecuzione, persistiti e letti senza modifiche al codice)
Applicazione
Configurazione
- Java 21
- Spring Boot 3.4.0
- asentinel-orm 1.70.0
- Database H2
Configurazione
Per interagire con il asentinel-orm e sfruttarne le funzionalità, è necessaria un’istanza di OrmOperations
.
Come indicato nel JavaDoc, questa è l’interfaccia centrale per eseguire operazioni ORM e non è né prevista né richiesta la sua implementazione specifica nel codice client.
L’applicazione di esempio include il codice di configurazione per creare un bean di questo tipo.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
ha due superinterfacce:
- SqlBuilderFactory – crea istanze di
SqlBuilder
che possono essere ulteriormente utilizzate per creare query SQL.SqlBuilder
è in grado di generare automaticamente parti della query, ad esempio quella che seleziona le colonne. La clausola where, la clausola order by, altre condizioni e le colonne effettive possono essere aggiunte anche utilizzando metodi della classeSqlBuilder
. Nella prossima parte di questa sezione viene mostrato un esempio di query generata daSqlBuilder
. - Updater – utilizzato per salvare le entità nelle rispettive tabelle del database. Può eseguire inserimenti o aggiornamenti a seconda che l’entità sia stata appena creata o esista già. Esiste un’interfaccia di strategia chiamata
NewEntityDetector
, che viene utilizzata per determinare se un’entità è nuova. Per impostazione predefinita viene utilizzato ilSimpleNewEntityDetector
.
Tutte le query generate dall’ORM vengono eseguite utilizzando un’istanza di SqlQueryTemplate
che necessita ulteriormente di un JdbcOperations
/JdbcTemplate
per funzionare. Alla fine, tutte le query raggiungono il buon vecchio JdbcTemplate
attraverso il quale vengono eseguite partecipando alle transazioni di Spring, proprio come qualsiasi JdbcTemplate
esecuzione diretta.
Le costruzioni e la logica SQL specifiche del database sono fornite tramite implementazioni dell’interfaccia JdbcFlavor
, ulteriormente iniettate nella maggior parte dei bean menzionati sopra. In questo articolo, poiché viene utilizzato un database H2, è configurata un’implementazione di H2JdbcFlavor
.
La configurazione completa dell’ORM come parte dell’applicazione di esempio è OrmConfig
.
Implementazione
Il modello di dominio sperimentale esposto dall’applicazione di esempio è semplice e consiste di due entità: produttori di automobili e modelli di automobili. Rappresentando esattamente ciò che i loro nomi denotano, la relazione tra di loro è ovvia: un produttore di automobili può possedere più modelli di automobili.
Oltre al suo nome, il produttore di automobili è arricchito con attributi (colonne) che vengono inseriti dinamicamente dall’utente dell’applicazione durante l’esecuzione. Il caso d’uso esemplificato è diretto:
- All’utente è richiesto di fornire i nomi e i tipi desiderati per gli attributi dinamici
- Un paio di produttori di auto sono creati con valori concreti per gli attributi dinamici aggiunti in precedenza, e poi
- Le entità vengono ricaricate descritte sia dagli attributi iniziali che da quelli definiti durante l’esecuzione
Le entità iniziali sono mappate utilizzando le tabelle del database di seguito:
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)
);
Le classi di dominio corrispondenti sono decorate con annotazioni specifiche dell’ORM per configurare i mapping alle tabelle del database sopra.
"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
}
Alcune considerazioni:
@Table
– mappa (associa) la classe a una tabella del database@PkColumn
– mappa l’id
(identificatore univoco) alla chiave primaria della tabella@Column
– mappa un membro della classe a una colonna della tabella@Child
– definisce la relazione con un’altra entità- Membri annotati con
@Child
– configurati per essere caricati in modo pigro - La colonna della tabella
type
– mappata a un campoenum
–CarType
Per consentire alla classe CarManufacturer
di supportare attributi definiti durante l’esecuzione (mappati a colonne di tabella definite durante l’esecuzione), viene definita una sottoclasse come quella di seguito:
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);
}
...
}
Questa classe memorizza gli attributi (campi) definiti durante l’esecuzione in una Map
. L’interazione tra i valori dei campi definiti durante l’esecuzione e l’ORM è soddisfatta tramite l’implementazione dell’interfaccia DynamicColumnEntity
.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– viene utilizzato per impostare il valore della colonna definita a tempo di esecuzione quando viene letto dalla tabellagetValue()
– viene utilizzato per recuperare il valore di una colonna definita a tempo di esecuzione quando viene salvato nella tabella
Il DynamicColumn
mappa attributi definiti a tempo di esecuzione alle rispettive colonne in modo simile all’annotazione @Column
che mappa membri noti a tempo di compilazione.
Durante l’esecuzione dell’applicazione, viene eseguito il CfRunner
. All’utente viene chiesto di inserire nomi e tipi per gli attributi personalizzati dinamici desiderati che arricchiscono l’entità CarManufacturer
. (Per semplicità, sono supportati solo i tipi int
e varchar
.)
Per ciascuna coppia nome-tipo, viene eseguito un comando DML in modo che le nuove colonne possano essere aggiunte alla tabella del database CarManufacturer
. Il seguente metodo (dichiarato in CarService
) esegue l’operazione.
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
Ogni attributo di input viene registrato come un DefaultDynamicColumn
, un’implementazione di riferimento DynamicColumn
.
Una volta definiti tutti gli attributi, due produttori di auto vengono aggiunti al database, poiché l’utente fornisce valori per ciascun attributo.
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);
Il metodo sottostante (dichiarato in CarService
) crea effettivamente l’entità tramite l’ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
La versione a 2 parametri del metodo OrmOperations
update()
viene chiamata, che consente di passare un’istanza di UpdateSettings
e comunicare all’ORM durante l’esecuzione che ci sono valori definiti a runtime che devono essere persistiti.
Infine, vengono creati due modelli di auto, corrispondenti a uno dei produttori di auto precedentemente aggiunti.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
Il metodo sottostante (dichiarato in CarService
) crea effettivamente le entità tramite l’ORM, questa volta utilizzando il metodo OrmOperations
update() per persistere entità senza attributi dinamici. Per comodità, vengono create più entità in una sola chiamata.
public void createModels(CarModel... models) {
orm.update(models);
}
Come ultimo passo, uno dei produttori creati viene ricaricato per nome utilizzando una query generata dall’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();
}
Vale la pena fornire alcune spiegazioni riguardo al metodo definito sopra.
Il metodo OrmOperations
newSqlBuilder()
crea un’istanza di SqlBuilder
, e come suggerisce il nome, questo può essere utilizzato per generare query SQL. Il metodo SqlBuilder
select()
genera la parte select from table della query, mentre il resto (where, order by) deve essere aggiunto. La parte select della query può essere personalizzata passando istanze di EntityDescriptorNodeCallback
(i dettagli su EntityDescriptorNodeCallback
potrebbero essere oggetto di un futuro articolo).
Per far sapere all’ORM che l’intento è selezionare e mappare colonne definite a runtime, è necessario passare un DynamicColumnsEntityNodeCallback
. Insieme ad esso, viene fornito un AutoEagerLoader
in modo che l’ORM comprenda di caricare in modo anticipato l’elenco dei CarModel
s associati al produttore. Tuttavia, questo non ha nulla a che fare con gli attributi definiti a runtime, ma dimostra come un membro figlio possa essere caricato in modo anticipato.
Conclusione
Sebbene ci siano probabilmente altri modi di lavorare con colonne definite a runtime quando i dati sono memorizzati in database relazionali, l’approccio presentato in questo articolo ha il vantaggio di utilizzare colonne di database standard che vengono lette/scritte utilizzando query SQL standard generate direttamente dall’ORM.
Non è stato raro quando abbiamo avuto l’opportunità di discutere nella “comunità” del asentinel-orm, le ragioni per cui abbiamo dovuto sviluppare uno strumento del genere. Di solito, a prima vista, gli sviluppatori si sono dimostrati riluttanti e riservati quando si trattava di un ORM su misura, chiedendo perché non utilizzare Hibernate o altre implementazioni JPA.
Nel nostro caso, il principale motore era la necessità di un modo veloce, flessibile e semplice di lavorare con un numero a volte piuttosto elevato di attributi (colonne) definiti a runtime per entità che fanno parte del dominio aziendale. Per noi, si è rivelato essere il modo giusto. Le applicazioni funzionano senza intoppi in produzione, i clienti sono soddisfatti della velocità e delle prestazioni raggiunte, e gli sviluppatori si sentono a loro agio e creativi con l’API intuitiva.
Poiché il progetto è ora open-source, è molto facile per chiunque interessato dare un’occhiata, formare un’opinione obiettiva al riguardo e, perché no, fare un fork, aprire una PR e contribuire.
Risorse
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm