Become a member!

Chiavi primarie composte in Delphi MVC Framework ActiveRecord

🌐
Questo articolo è disponibile anche in altre lingue:
🇬🇧 English · 🇪🇸 Español · 🇩🇪 Deutsch · 🇧🇷 Português

Chiavi primarie composte in DMVCFramework ActiveRecord con Delphi

Per anni l'ActiveRecord di DMVCFramework ha tenuto le chiavi primarie volutamente semplici: una sola colonna, nessuna eccezione. La versione 3.5 rimuove questo limite.

Se hai progettato un database relazionale di una certa dimensione, ti sei imbattuto in questa tabella:

CREATE TABLE user_roles (
  user_id INTEGER NOT NULL,
  role_id INTEGER NOT NULL,
  PRIMARY KEY (user_id, role_id)
);

Una tabella di associazione. La sua identità non è una colonna, sono due. E per anni TMVCActiveRecord ha imposto una regola ferma: esattamente una colonna di chiave primaria per entità. Aggiungevi foPrimaryKey a un secondo campo e ti bloccava all’avvio con un errore.

La regola manteneva l’ORM semplice. Lasciava però fuori un tipo di tabella molto comune: user_roles(user_id, role_id), una riga d’ordine con chiave (order_id, line_no), un record per-tenant con chiave (tenant, code). Tabelle di questo tipo sono ovunque.

I workaround che tutti conoscevano

Va detto che una via d’uscita c’era sempre. Potevi non dichiarare alcuna chiave primaria, mappare due campi normali e leggerli con RQL e Where<T> come qualsiasi altra cosa. Rinunciavi solo a indirizzare una riga tramite la sua chiave: niente GetByPK, quindi ogni lookup passava per un filtro esplicito.

Oppure aggiungevi un id autoincrement surrogato come chiave primaria, con un vincolo UNIQUE sulle colonne naturali. ActiveRecord era contento, riavevi il CRUD per chiave, e il database garantiva comunque l’unicità della chiave reale. Poi quella colonna id passava il resto della sua vita senza che nessuno la interrogasse, esistendo solo per far smettere l’ORM di lamentarsi.

Entrambi funzionano. Moltissimi schemi validi girano esattamente così ancora oggi. Ma nessuno dei due ti permette di dire la cosa ovvia: che (user_id, role_id) è la chiave, e far sì che l’ORM la tratti come tale.

È anche il motivo per cui questa funzionalità ha aspettato così tanto. I workaround erano abbastanza buoni, e un workaround abbastanza buono è il nemico naturale della soluzione vera: finché la via d’uscita funziona, nessuno mette in cima alla lista la porta che manca. Per anni un id surrogato ha assorbito in silenzio la richiesta, e io l’ho lasciato fare. Stavolta mi sono stancato di spiegare il workaround, così mi sono messo lì e ho fatto in modo che ActiveRecord mappasse direttamente la chiave naturale.

A partire dalla 3.5, non devi più scegliere

Ecco l’intera modifica al tuo model:

[MVCTable('user_roles')]
TUserRole = class(TMVCActiveRecord)
private
  [MVCTableField('user_id', [foPrimaryKey])]
  fUserID: Integer;
  [MVCTableField('role_id', [foPrimaryKey])]
  fRoleID: Integer;
  // ...
end;

Nessun nuovo attributo da memorizzare. Marchi entrambe le colonne con foPrimaryKey, esattamente come già ne marchi una. Se sai mettere una foPrimaryKey, sai già mettere una chiave composta: la curva di apprendimento è un gradino alto zero. Da quel momento ActiveRecord tratta (user_id, role_id) come l’identità della riga, e ogni WHERE, INSERT, UPDATE e DELETE generato copre l’intera chiave.

E poiché un singolo valore non può più puntare a una riga, i metodi per chiave che conosci hanno una controparte plurale:

lRole := TMVCActiveRecord.GetByPKs<TUserRole>([1, 42]);

Quella chiamata, GetByPKs, è un metodo di classe, e suggerisce una cosa che vale la pena sapere: è tutto un unico engine, e non sei costretto allo stile Active Record statico. DMVCFramework offre già un repository pronto sulle stesse identiche entità, IMVCRepository<T>, nella unit MVCFramework.Repository. Il suo vero vantaggio rispetto ai metodi di classe è che è un’interfaccia: a differenza di una chiamata statica, puoi iniettarlo direttamente nei tuoi controller e servizi tramite il container di dependency injection. Lo registri una volta e lasci che [MVCInject] lo consegni a chi ne ha bisogno:

// Registra il repository una volta, nel .dpr, prima di avviare il server
Container.RegisterType(TMVCRepository<TUserRole>, IMVCRepository<TUserRole>,
  TRegistrationType.SingletonPerRequest);

// Poi iniettalo dove ti serve, un controller o un servizio
type
  [MVCPath('/user-roles')]
  TUserRolesController = class(TMVCController)
  private
    fRepo: IMVCRepository<TUserRole>;
  public
    [MVCInject]
    constructor Create(UserRolesRepository: IMVCRepository<TUserRole>); reintroduce;
  end;

Le entità sono le stesse e le chiavi composte funzionano ovviamente allo stesso modo, ma qui ci arrivi tramite una dipendenza: in un test puoi sostituirla con un’altra, cosa impossibile con una chiamata statica. Scriverti dei repository a mano resta comunque perfettamente valido, solo che raramente serve.

Quei metodi plurali ad array erano la parte ovvia. La domanda più difficile era come dare un nome a una chiave ora fatta di più valori. Così, accanto ai metodi ad array, c’è ora un modo diverso di indirizzare una riga: imposti ogni campo della chiave per nome di proprietà, nell’ordine che preferisci, e poi chiami Load, un metodo del tutto nuovo che legge la chiave direttamente dall’entità invece che da una lista di argomenti. Sulla carta sembra un cambiamento da poco. Sotto è un vero cambio di paradigma, perché per la prima volta è l’entità a possedere la propria chiave, invece di riceverla come argomento posizionale.

Questo è il succo, ed è anche dove finisce la parte facile. Marcare le due colonne richiede un minuto. Le domande nascoste dietro quel minuto sono il motivo per cui l’articolo completo è così lungo:

  • I metodi posizionali ad array (GetByPKs, LoadByPKs) e il nuovo Load per nome di proprietà esistono entrambi per un motivo: quale trappola evita l’uno, e quando dovresti comunque preferire l’altro?
  • Cosa fanno Load e Refresh quando la riga semplicemente non c’è, e perché ho scelto la risposta meno comoda?
  • Le colonne di una stessa chiave possono essere di tipi diversi? E quante di esse può riempire il database al posto tuo?
  • Con il controller auto-CRUD, come si scrive l’URL per indirizzare una riga la cui chiave ha due colonne? E come si rappresentano nell’URL le chiavi di tipo stringa o GUID?
  • Qual è l’unico breaking change dell’intera release, e quale tipo ristretto di codice se ne deve davvero preoccupare?

Trovi tutte le risposte a queste e ad altre domande nell’articolo completo su Patreon.

Un superpotere che conoscono in pochi

Colgo l’occasione per segnalare un pezzo di DMVCFramework che considero un vero superpotere: TMVCActiveRecordController. Chi lo usa non torna più indietro, eppure la maggior parte degli sviluppatori con cui parlo ne ignora perfino l’esistenza.

Quello che risolve è il codice più ripetitivo di qualsiasi API basata sui dati. Per ogni tabella, altrimenti, scriveresti a mano lo stesso controller: una GET per la lista, una GET per id, una POST per creare, una PUT per aggiornare, una DELETE, più paginazione, filtri e ordinamento, moltiplicato per ogni entità dello schema. È boilerplate che hai già scritto cento volte e che alla centunesima sbaglierai in un dettaglio, sempre nell’unico endpoint sfuggito alla code review.

Il controller sostituisce tutto questo con una riga:

FMVC.AddController(TMVCActiveRecordController, '/api/entities');
FMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create(CON_DEF_NAME));

Da quel momento ogni entità ActiveRecord registrata è una risorsa REST. Hai CRUD completo, query RQL direttamente dall’URL per filtrare, ordinare e paginare i risultati, e un descrittore Swagger generato per te. Nessun controller per entità, nessun DTO per entità, nessuna rotta per entità.

La cosa che lo rende usabile in un’applicazione vera è che non è un passacarte stupido che si limita a spostare righe dentro e fuori dal database alle spalle dell’ORM. Il controller fa passare le tue entità attraverso il loro normale ciclo di vita, quindi tutta la business logic che hai messo nell’entità continua a valere: la validazione, gli hook OnBeforeInsert e OnBeforeUpdate, i campi calcolati e di sola lettura, le regole di serializzazione. Un valore che l’entità rifiuta viene rifiutato via HTTP con la stessa fermezza con cui lo sarebbe da codice Delphi. Stai esponendo il tuo model, non aggirandolo.

Abbina tutto questo al nuovo generatore di entità e i conti diventano quasi assurdi. Punti il generatore su un database esistente e ti scrive le classi TMVCActiveRecord, tabella per tabella; registri il controller una volta, e uno schema con migliaia di tabelle diventa un’API RESTful nel tempo di fare un caffè. Il limite è chiaro: una tabella che è radice di un aggregato, con figli o altre relazioni la cui coerenza va mantenuta (pensa a una testata_fattura con le sue righe_fattura), non andrebbe esposta riga per riga in questo modo. Va servita attraverso il suo Aggregate Root, come insegna il Domain-Driven Design (il famoso libro di Eric Evans), così che sia la radice a far rispettare gli invarianti dell’aggregato. Per quei casi serve ancora un controller dedicato, scritto a mano. Ma la maggior parte degli schemi è fatta soprattutto di tabelle semplici e indipendenti, e per ognuna di quelle questo approccio low-code ti consegna un’API funzionante e validata con uno sforzo vicino allo zero.

Ed è esattamente qui che le chiavi composte dovevano guadagnarsi il posto. Un controller auto-CRUD è generale solo quanto le chiavi che sa indirizzare, e una funzionalità ferma alle chiavi a colonna singola avrebbe lasciato le tabelle di associazione fuori proprio dalla parte del framework nata per trattare ogni entità allo stesso modo.

Leggi l’articolo completo

Ho scritto tutto per esteso, con il codice, il ragionamento dietro ogni scelta e i test con cui viene rilasciato (SQLite, Firebird e PostgreSQL, Win32 e Win64), come articolo di approfondimento per gli abbonati a DelphiMVCFramework su Patreon.

Se costruisci backend Delphi con DMVCFramework, è lì che stanno i dettagli. L’abbonamento ti dà accesso anche a tutto il resto del materiale premium: articoli tecnici come questo, video, i libri di approfondimento e uno sconto sulla prossima edizione della guida ufficiale a DMVCFramework. Ed è anche ciò che tiene in vita lo sviluppo del framework.

👉 Chiavi primarie composte in DMVCFramework ActiveRecord: articolo completo su Patreon

Comments

comments powered by Disqus