Ciao, Habr!
Continuiamo a esplorare il tema e , inclusi, a livello di database. Oggi ti proponiamo di leggere perché nella progettazione di grandi applicazioni la struttura del database, e non il codice Java, dovrebbe avere un'importanza decisiva, come si fa, e quali eccezioni esistono a questa regola.
In questo articolo piuttosto tardivo spiegherò perché credo che praticamente in tutti i casi il modello di dati in un'applicazione debba essere progettato "partendo dal database", e non "partendo dalle possibilità di Java" (o di un altro linguaggio client con cui lavori). Scegliendo il secondo approccio, entri in un lungo percorso di dolore e sofferenza, non appena il tuo progetto comincia a crescere.
L'articolo è scritto in riferimento a , posta su Stack Overflow.
Discussioni interessanti su reddit nelle sezioni e .
Generazione del codice
Sono rimasto molto sorpreso dal fatto che esista un così piccolo strato di utenti che, incontrando jOOQ, si indignano per il fatto che nel lavoro con jOOQ ci si affida seriamente alla generazione del codice sorgente. Nessuno ti impedisce di usare jOOQ come credi sia giusto e non ti costringe a utilizzare la generazione del codice. Ma per impostazione predefinita (come descritto nella guida), il lavoro con jOOQ avviene così: inizi con uno schema del database (ereditato), esegui il reverse engineering con il generatore di codice jOOQ, per ottenere un insieme di classi che rappresentano le tue tabelle e poi scrivi query type-safe per queste tabelle:
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Le informazioni sui tipi sono state derivate da
// codice sorgente generato, a cui si riferisce la seguente
// condizione SELECT
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ nomi generati
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}Il codice viene generato o manualmente al di fuori della build, o manualmente ad ogni build. Ad esempio, una tale rigenerazione può seguire subito dopo .
Generazione del codice sorgente
Con tali approcci alla generazione di codice – manuali e automatici – sono collegate diverse filosofie, vantaggi e svantaggi, che non intendo discutere in dettaglio in questo articolo. Tuttavia, in generale, la sostanza del codice generato è che consente di riprodurre in Java quella "verità" che accettiamo come un dato di fatto, sia all'interno del nostro sistema che al di fuori di esso. In un certo senso, lo stesso fanno i compilatori, che generano bytecode, codice macchina o qualche altra forma di codice basata su sorgenti – otteniamo una rappresentazione della nostra "verità" in un'altra lingua, indipendentemente dalle ragioni specifiche.
Esistono molti di questi generatori di codice. Ad esempio, . Il principio è sempre lo stesso:
- Esiste una certa verità (interna o esterna) – ad esempio, una specifica, un modello di dati, ecc.
- Abbiamo bisogno di una rappresentazione locale di questa verità nel nostro linguaggio di programmazione.
Inoltre, generare tale rappresentazione è quasi sempre conveniente – per evitare ridondanze.
Fornitori di tipi e elaborazione delle annotazioni
Nota: un altro approccio, più moderno e specifico, alla generazione di codice per jOOQ è legato all'uso dei fornitori di tipi, . In tal caso, il codice viene generato dal compilatore, precisamente durante la fase di compilazione. In forma di sorgenti, tale codice non esiste fondamentalmente. In Java esistono strumenti simili, anche se non così eleganti: sono i processori di annotazioni, ad esempio, .
In un certo senso, qui si verificano le stesse dinamiche che nel primo caso, eccetto per il fatto che:
- Non vedi il codice generato (forse a qualcuno questa situazione non sembra così respingente?)
- Devi garantire che i tipi possano essere forniti, cioè, la "verità" deve sempre essere accessibile. Questo è facile nel caso di Lombok, che annota la "verità". Un po' più complicato con i modelli di database, il cui funzionamento dipende da una connessione live sempre disponibile.
Qual è il problema con la generazione del codice?
Oltre alla subdola domanda su come è meglio avviare la generazione del codice – manualmente o automaticamente, è necessario menzionare che ci sono persone che credono che la generazione del codice non sia affatto necessaria. La giustificazione di questo punto di vista, che mi è stata presentata più spesso, è che diventa difficile configurare la pipeline di compilazione. Sì, è davvero difficile. Ci sono costi infrastrutturali aggiuntivi. Se state solo iniziando a lavorare con un determinato prodotto (sia esso jOOQ, JAXB, Hibernate, ecc.), il tempo che dedicate alla configurazione dell'ambiente di lavoro è tempo che vorreste trascorrere a studiare l'API stessa, per poi estrarne valore.
Se i costi per capire come funziona il generatore sono troppo alti – allora, in effetti, l'API non ha lavorato bene sull'usabilità del generatore di codice (e si scopre che anche la personalizzazione da parte dell'utente è complessa). La facilità d'uso deve essere la massima priorità per qualsiasi API di questo tipo. Ma questo è solo un argomento contro la generazione del codice. Per il resto, è assolutamente necessario scrivere completamente a mano la rappresentazione locale della verità interna o esterna.
Molti diranno che non hanno tempo per occuparsi di tutto questo. Hanno scadenze da rispettare per il loro Super-Prodotto. Prima o poi sistemeremo le pipeline di compilazione, ce la faremo. A questo rispondo:

,
Ma in Hibernate / JPA è così semplice scrivere codice "per Java".
In effetti. Per Hibernate e i suoi utenti, è sia una benedizione sia una maledizione. In Hibernate puoi semplicemente scrivere un paio di entità, in questo modo:
@Entity
class Book {
@Id
int id;
String title;
}E quasi tutto è pronto. Ora il compito di Hibernate è generare i complessi "dettagli" su come esattamente questa entità sarà definita nel DDL del tuo "dialetto" SQL:
CREATE TABLE book (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title VARCHAR(50),
CONSTRAINT pk_book PRIMARY KEY (id)
);
CREATE INDEX i_book_title ON book (title);… e cominciamo a far girare l'applicazione. È davvero una grande possibilità per iniziare a lavorare rapidamente e provare diverse soluzioni.
Tuttavia, permettetemi. Ho esagerato.
- Hibernate applicherà davvero la definizione di questa chiave primaria nominata?
- Hibernate creerà un indice su TITLE? – so per certo che ne avremo bisogno.
- Hibernate renderà sicuramente questa chiave identificativa nella Identity Specification?
Probabilmente no. Se stai sviluppando il tuo progetto da zero, è sempre comodo semplicemente scartare il vecchio database e generarne uno nuovo, appena aggiungi le annotazioni necessarie. Così, l'entità Book alla fine assumerà la forma:
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
Fantastico. Rigenerare. Ancora, in tal caso sarà molto semplice all'inizio.
Ma in seguito dovrai pagarne il prezzo.
Prima o poi dovrai andare in produzione. È proprio allora che un modello simile smetterà di funzionare. Perché:
In produzione non sarà più possibile scartare il vecchio database e ricominciare tutto da un foglio bianco. Il tuo database diventerà ereditato.
D'ora in poi e per sempre dovrai scrivere E cosa succederà, in tal caso, con le tue entità? Potrai o adattarle manualmente (e raddoppiare così il carico di lavoro), oppure ordinare a Hibernate di rigenerarle per te (quali sono le probabilità che quelle generate in questo modo corrispondano alle tue aspettative?) Comunque tu perdi.
Quindi, non appena passerai in produzione, avrai bisogno di patch urgenti. E queste devono essere portate in produzione molto rapidamente. Poiché non ti sei preparato e non hai organizzato una fluida catena di distribuzione delle tue migrazioni, tutto sarà patchato in modo disordinato. Alla fine non riuscirai a fare tutto correttamente. E darai la colpa a Hibernate, perché è sempre colpa di qualcun altro, mai tua...
Invece, avresti potuto fare tutto in modo completamente diverso fin dall'inizio. Ad esempio, mettere le ruote tonde sulla bicicletta.
Prima di tutto il database.
La vera 'verità' nel modello del tuo database e la 'sovranità' su di esso risiede all'interno del database stesso. Lo schema è definito solo nel database e in nessun altro posto, e ogni cliente ha una copia di questo schema, quindi è assolutamente sensato imporre il rispetto dello schema e della sua integrità direttamente nel database – là dove è conservata l'informazione.
Questo è un vecchio e persino trito proverbio. Le chiavi primarie e uniche sono buone. Le chiavi esterne sono buone. La verifica dei vincoli è buona. sono buone.
Inoltre, questo non è tutto. Ad esempio, utilizzando Oracle, probabilmente vorrete specificare:
- In quale tablespace si trova la vostra tabella
- Qual è il valore di PCTFREE
- Qual è la dimensione della cache nella vostra sequenza (dopo l'identificatore)
Probabilmente tutto ciò non è importante nei piccoli sistemi, ma non è necessario attendere il passaggio all'area dei "big data" — si può iniziare a trarre vantaggio dalle ottimizzazioni fornite dal fornitore molto prima, come quelle menzionate sopra. Nessun ORM che ho visto (incluso jOOQ) offre accesso a tutte le opzioni DDL che si potrebbe desiderare di utilizzare nel proprio database. Gli ORM offrono alcuni strumenti che aiutano a scrivere DDL.
Ma, in fin dei conti, uno schema ben progettato è scritto a mano in DDL. Qualsiasi DDL generato è solo una sua approssimazione.
E per quanto riguarda il modello client?
Come accennato sopra, sul client avrete bisogno di una copia dello schema del vostro database, una vista client. È superfluo sottolineare che questa vista client deve essere sincronizzata con il modello reale. Qual è il modo migliore per farlo? Tramite un generatore di codice.
Tutti i database forniscono la loro metainformazione tramite SQL. Ecco come ottenere da vostro database tutte le tabelle in diversi dialetti SQL:
-- H2, HSQLDB, MySQL, PostgreSQL, SQL Server
SELECT table_schema, table_name
FROM information_schema.tables
-- DB2
SELECT tabschema, tabname
FROM syscat.tables
-- Oracle
SELECT owner, table_name
FROM all_tables
-- SQLite
SELECT name
FROM sqlite_master
-- Teradata
SELECT databasename, tablename
FROM dbc.tables
Queste query (o simili, a seconda se devono essere considerate anche le viste, le viste materializzate, le funzioni con valore tabellare) vengono eseguite anche tramite chiamata da JDBC, oppure tramite il modulo di metadati jOOQ.
Dai risultati di tali query è relativamente facile generare qualsiasi vista client del modello del vostro database, indipendentemente dalla tecnologia utilizzata sul client.
- Se utilizzate JDBC o Spring, potete creare un insieme di costanti stringa
- Se utilizzate JPA, potete generare voi stessi le entità
- Se utilizzate jOOQ, potete generare il metamodelo jOOQ
A seconda del volume di funzionalità offerto dalla vostra API client (es. jOOQ o JPA), il modello di meta-dati generato può essere realmente ricco e completo. Prendiamo, ad esempio, la funzione delle join implicite, , che si basa sulle informazioni meta generate riguardanti le relazioni delle chiavi esterne esistenti tra le vostre tabelle.
Ora ogni incremento del database porterà automaticamente all'aggiornamento del codice client. Immaginate, ad esempio:
ALTER TABLE book RENAME COLUMN title TO book_title;Vorreste davvero fare questo lavoro due volte? Assolutamente no. Registriamo semplicemente il DDL, lo facciamo passare attraverso il vostro pipeline di build e otteniamo l'entità aggiornata:
@Entity
@Table(name = "book", indexes = {
\/\/ Ci avete pensato?
@Index(name = "i_book_title", columnList = "book_title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
@Column("book_title")
String bookTitle;
}Oppure la classe jOOQ aggiornata. La maggior parte delle modifiche DDL si rifletta anche sulla semantica, non solo sulla sintassi. Perciò è utile dare un'occhiata al codice compilato per vedere quale codice sarà (o potrebbe essere) interessato dall'incremento del vostro database.
La verità unica
Indipendentemente dalla tecnologia che usate, c'è sempre un modello che è l'unica fonte di verità per qualche sottosistema – o, almeno, dovremmo tendere a questo e evitare una confusione aziendale del tipo dove "verità" è immediatamente ovunque e in nessun posto. Può essere molto più semplice. Se semplicemente scambiate file XML con qualche altro sistema, usate semplicemente XSD. Date un'occhiata al modello meta INFORMATION_SCHEMA di jOOQ in formato XML:
- XSD è ben comprensibile
- XSD etichetta molto bene il contenuto XML e permette di eseguire la validazione in tutti i linguaggi client
- XSD è ben versionata e ha avanzata retrocompatibilità
- XSD può essere traspormato in codice Java utilizzando XJC
L'ultimo punto è importante. Durante la comunicazione con un sistema esterno tramite messaggi XML, vogliamo assicurarci della validità dei nostri messaggi. Questo è molto facile da ottenere grazie a JAXB, XJC e XSD. Sarebbe un completo assurdismo aspettarsi che, con un approccio di progettazione ‘Java per primo’, in cui creiamo i nostri messaggi come oggetti Java, questi possano essere in qualche modo convertiti in XML e inviati per l'uso in un altro sistema. L'XML generato in questo modo sarebbe di scarsa qualità, non documentato e difficile da sviluppare. Se esistesse un accordo sul livello di qualità del servizio (SLA) a riguardo, lo avremmo già compromesso.
A dire il vero, è esattamente quello che accade costantemente con le API JSON, ma questa è un'altra storia, ne parlerò la prossima volta...
Basi di dati: è la stessa cosa
Lavorando con i database, si comprende che sono tutti, fondamentalmente, simili. Un database possiede i propri dati e deve gestire lo schema. Qualsiasi modifica apportata allo schema deve essere eseguita direttamente su DDL, affinché venga aggiornato l'unico sorgente di verità.
Quando l'aggiornamento della sorgente avviene, tutti i clienti devono aggiornare le proprie copie del modello. Alcuni clienti possono essere scritti in Java utilizzando jOOQ e Hibernate o JDBC (o tutti e tre). Altri clienti possono essere scritti in Perl (e auguro loro buona fortuna), altri ancora in C#. Non importa. Il modello principale si trova nel database. I modelli generati tramite ORM sono generalmente di scarsa qualità, poco documentati e difficili da sviluppare.
Quindi non commettete errori. Sin dall'inizio non commettete errori. Lavorate partendo dal database. Costruite una pipeline di distribuzione che possa essere automatizzata. Includete generatori di codice per copiare facilmente il modello del vostro database e integrarlo nei clienti. E smettete di preoccuparvi dei generatori di codice. Sono buoni. Con loro diventerete più produttivi. È solo necessario investire un po' di tempo nella loro configurazione fin dall'inizio – e poi vi aspettano anni di produttività aumentata, dai quali si svilupperà la storia del vostro progetto.
Non ringraziate ancora, dopo.
Spiegazione
Per chiarezza: Questo articolo non sostiene in alcun modo che l'intero sistema debba adattarsi al modello del tuo database (cioè, dominio dell'oggetto, logica aziendale, ecc.). In questo articolo parlo del fatto che il codice client, che interagisce con il database, deve funzionare in base al modello del database, in modo che esso stesso non riproduca il modello del database come "primo classe". Questa logica si trova di solito a livello di accesso ai dati sul tuo client.
Nelle architetture a due livelli, che in alcune aree sono ancora presenti, un tale modello di sistema può essere l'unico possibile. Tuttavia, nella maggior parte dei sistemi, il livello di accesso ai dati mi sembra una "sottosistema" che incapsula il modello del database.
Eccezioni
Da ogni regola ci sono eccezioni, e ho già detto che l'approccio con la primarietà del database e la generazione del codice sorgente può talvolta rivelarsi inadeguato. Ecco un paio di tali eccezioni (probabilmente ce ne sono altre):
- Quando lo schema è sconosciuto e deve essere scoperto. Ad esempio, sei un fornitore di strumenti che aiuta gli utenti a orientarsi in qualsiasi schema. Uff. Qui senza generazione del codice. Ma comunque, il database è prima di tutto.
- Quando lo schema deve essere generato al volo per risolvere un certo compito. Questo esempio sembra una versione leggermente ricercata del pattern , cioè, non hai realmente uno schema chiaramente definito. In questo caso, spesso non si può neanche essere certi che un RDBMS sia adatto.
Le eccezioni per loro natura sono eccezionali. Nella maggior parte dei casi relativi all'utilizzo di un RDBMS, lo schema è noto in anticipo, si trova all'interno del RDBMS ed è l'unica fonte di "verità", e tutti i client devono ottenere copie derivate da esso. Idealmente, in questo caso bisognerebbe utilizzare un generatore di codice.
Fonte: habr.com
