La verità prima di tutto, ovvero il motivo per cui un sistema deve essere progettato in base alla progettazione del database

Ehi Habr!

Continuiamo la ricerca sull'argomento Java и Primavera, anche a livello di database. Oggi vi invitiamo a leggere perché, quando si progettano applicazioni di grandi dimensioni, è la struttura del database, e non il codice Java, ad avere un'importanza decisiva, come viene fatta e quali eccezioni ci sono a questa regola.

In questo articolo, piuttosto tardivo, spiegherò perché credo che in quasi tutti i casi, il modello dati in un'applicazione dovrebbe essere progettato "dal database" piuttosto che "dalle capacità di Java" (o qualunque sia il linguaggio client che utilizzi). lavorando con). Adottando il secondo approccio, ti stai preparando per un lungo percorso di dolore e sofferenza una volta che il tuo progetto inizierà a crescere.

L'articolo è stato scritto sulla base di una domanda, fornito su Stack Overflow.

Discussioni interessanti su Reddit nelle sezioni /r/java и /r/programmazione.

Generazione del codice

Quanto sono rimasto sorpreso dal fatto che ci sia un segmento così piccolo di utenti che, dopo aver conosciuto jOOQ, sono indignati dal fatto che jOOQ si affida seriamente alla generazione del codice sorgente per funzionare. Nessuno ti impedisce di utilizzare jOOQ come ritieni opportuno o ti obbliga a utilizzare la generazione di codice. Ma il modo predefinito (come descritto nel manuale) di lavorare con jOOQ è iniziare con uno schema di database (legacy), decodificarlo utilizzando il generatore di codice jOOQ per ottenere così un insieme di classi che rappresentano le tabelle, quindi scrivere il tipo -safe query a queste tabelle:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на 
//   основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ сгенерированные имена
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Il codice viene generato manualmente all'esterno dell'assembly o manualmente in ogni assembly. Ad esempio, tale rigenerazione potrebbe avvenire immediatamente dopo Migrazione del database Flyway, che può essere eseguita anche manualmente o automaticamente.

Generazione del codice sorgente

Esistono varie filosofie, vantaggi e svantaggi associati a questi approcci alla generazione del codice, manuali e automatici, di cui non parlerò in dettaglio in questo articolo. Ma, in generale, il punto centrale del codice generato è che ci permette di riprodurre in Java quella “verità” che diamo per scontata, sia all'interno che all'esterno del nostro sistema. In un certo senso, questo è ciò che fanno i compilatori quando generano bytecode, codice macchina o qualche altra forma di codice sorgente: otteniamo una rappresentazione della nostra "verità" in un'altra lingua, indipendentemente dalle ragioni specifiche.

Esistono molti generatori di codici di questo tipo. Per esempio, XJC può generare codice Java basato su file XSD o WSDL. Il principio è sempre lo stesso:

  • Esiste una parte di 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, è quasi sempre consigliabile generare tale rappresentanza per evitare ridondanze.

Provider di tipi ed elaborazione delle annotazioni

Nota: un altro approccio più moderno e specifico alla generazione di codice per jOOQ utilizza i provider di tipi, poiché sono implementati in F#. In questo caso il codice viene generato dal compilatore, effettivamente in fase di compilazione. In linea di principio, tale codice non esiste in forma sorgente. Java ha strumenti simili, anche se non così eleganti: i processori di annotazione, ad esempio, Chili.

In un certo senso qui accadono le stesse cose del primo caso, ad eccezione di:

  • Non vedi il codice generato (forse a qualcuno questa situazione sembra meno ripugnante?)
  • È necessario assicurarsi che sia possibile fornire i tipi, ovvero "true" deve essere sempre disponibile. Questo è facile nel caso di Lombok, che annota la “verità”. È un po' più complicato con i modelli di database che dipendono da una connessione live costantemente disponibile.

Qual è il problema con la generazione del codice?

Oltre alla delicata questione su come eseguire al meglio la generazione del codice, manualmente o automaticamente, dobbiamo anche menzionare che ci sono persone che credono che la generazione del codice non sia affatto necessaria. La giustificazione di questo punto di vista, in cui mi sono imbattuto più spesso, è che in tal caso è difficile impostare una pipeline di costruzione. Sì, è davvero difficile. Sorgono costi infrastrutturali aggiuntivi. Se hai appena iniziato con un particolare prodotto (che sia jOOQ, o JAXB, o Hibernate, ecc.), la configurazione di un ambiente di produzione richiede tempo che preferiresti dedicare all'apprendimento dell'API stessa in modo da poterne estrarre valore .

Se i costi associati alla comprensione della struttura del generatore sono troppo alti, allora, in effetti, l'API ha fatto un pessimo lavoro sull'usabilità del generatore di codice (e in seguito si scopre che anche la personalizzazione da parte dell'utente è difficile). L'usabilità dovrebbe essere la massima priorità per qualsiasi API di questo tipo. Ma questo è solo un argomento contro la generazione del codice. Altrimenti è assolutamente del tutto manuale scrivere una rappresentazione locale della verità interna o esterna.

Molti diranno che non hanno tempo per fare tutto questo. Stanno scadendo le scadenze per il loro Super Prodotto. Un giorno metteremo in ordine i trasportatori di assemblaggio, avremo tempo. Risponderò loro:

La verità prima di tutto, ovvero il motivo per cui un sistema deve essere progettato in base alla progettazione del database
originale, Alan O'Rourke, Stack del pubblico

Ma in Hibernate/JPA è semplicissimo scrivere codice Java.

Veramente. Per Hibernate e i suoi utenti, questa è sia una benedizione che 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 tocca a Hibernate generare i complessi "dettagli" di come esattamente questa entità verrà 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 inizia a eseguire l'applicazione. Un'opportunità davvero interessante per iniziare rapidamente e provare cose diverse.

Tuttavia, per favore, permettetemi. Mentivo.

  • Hibernate applicherà effettivamente la definizione di questa chiave primaria denominata?
  • Hibernate creerà un indice in TITLE? – So per certo che ne avremo bisogno.
  • Hibernate renderà esattamente questa chiave identificabile nella specifica dell'identità?

Probabilmente no. Se stai sviluppando il tuo progetto da zero, è sempre conveniente eliminare semplicemente il vecchio database e generarne uno nuovo non appena aggiungi le annotazioni necessarie. Pertanto, l’entità Libro 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;
}

Freddo. Rigenerare. Anche in questo caso all’inizio sarà molto semplice.

Ma dovrai pagarlo più tardi

Prima o poi dovrai entrare in produzione. Questo sarà il momento in cui questo modello smetterà di funzionare. Perché:

In produzione non sarà più possibile, se necessario, scartare il vecchio database e ripartire da zero. Il tuo database diventerà legacy.

D'ora in poi e per sempre dovrai scrivere Script di migrazione DDL, ad esempio, utilizzando Flyway. Cosa accadrà alle vostre entità in questo caso? Puoi adattarli manualmente (e quindi raddoppiare il tuo carico di lavoro), oppure puoi dire a Hibernate di rigenerarli per te (con quale probabilità quelli generati in questo modo soddisfano le tue aspettative?). In ogni caso, perdi.

Quindi, una volta entrato in produzione, avrai bisogno di hot patch. E devono essere messi in produzione molto rapidamente. Dal momento che non hai preparato e non hai organizzato una pipeline fluida delle tue migrazioni per la produzione, correggi tutto in modo selvaggio. E poi non hai più tempo per fare tutto correttamente. E critichi Hibernate, perché la colpa è sempre di qualcun altro, ma non tu...

Invece le cose avrebbero potuto essere fatte in modo completamente diverso fin dall’inizio. Ad esempio, metti le ruote rotonde su una bicicletta.

Prima la banca dati

La vera "verità" nello schema del database e la "sovranità" su di esso si trova all'interno del database. Lo schema è definito solo nel database stesso e da nessun'altra parte, e ogni client ha una copia di questo schema, quindi è perfettamente logico imporre la conformità allo schema e alla sua integrità, per farlo direttamente nel database, dove si trovano le informazioni. immagazzinato.
Questa è saggezza vecchia, persino banale. Le chiavi primarie e univoche sono buone. Le chiavi esterne sono buone. Controllare le restrizioni è utile. approvazione - Bene.

Inoltre, non è tutto. Ad esempio, utilizzando Oracle, probabilmente vorrai specificare:

  • In quale tablespace si trova il tuo tavolo?
  • Qual è il suo valore PCTFREE?
  • Qual è la dimensione della cache nella sequenza (dietro l'ID)

Questo potrebbe non essere importante nei sistemi di piccole dimensioni, ma non devi aspettare di passare al regno dei big data: puoi iniziare a trarre vantaggio dalle ottimizzazioni dello storage fornite dai fornitori come quelle menzionate sopra molto prima. Nessuno degli ORM che ho visto (incluso jOOQ) fornisce l'accesso al set completo di opzioni DDL che potresti voler utilizzare nel tuo database. Gli ORM offrono alcuni strumenti che ti aiutano a scrivere DDL.

Ma alla fine, un circuito ben progettato è scritto a mano in DDL. Qualsiasi DDL generato ne è solo un'approssimazione.

E il modello cliente?

Come accennato in precedenza, sul client avrai bisogno di una copia dello schema del tuo database, la vista client. Inutile menzionare che la visione del cliente deve essere sincronizzata con il modello reale. Qual è il modo migliore per raggiungere questo obiettivo? Utilizzando un generatore di codice.

Tutti i database forniscono le loro metainformazioni tramite SQL. Ecco come ottenere tutte le tabelle dal tuo database 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 che si debbano considerare anche viste, viste materializzate, funzioni con valori di tabella) vengono eseguite anche chiamando DatabaseMetaData.getTables() da JDBC o utilizzando il meta-modulo jOOQ.

Dai risultati di tali query, è relativamente semplice generare qualsiasi rappresentazione lato client del modello di database, indipendentemente dalla tecnologia utilizzata sul client.

  • Se stai utilizzando JDBC o Spring, puoi creare un set di costanti stringa
  • Se usi JPA, puoi generare le entità stesse
  • Se usi jOOQ, puoi generare il metamodello jOOQ

A seconda della funzionalità offerta dall'API del tuo client (ad esempio jOOQ o JPA), il metamodello generato può essere davvero ricco e completo. Prendiamo, ad esempio, la possibilità di join impliciti, introdotto in jOOQ 3.11, che si basa sulle metainformazioni generate sulle relazioni di chiave esterna esistenti tra le tabelle.

Ora qualsiasi incremento del database aggiornerà automaticamente il codice client. Immagina ad esempio:

ALTER TABLE book RENAME COLUMN title TO book_title;

Vorresti davvero fare questo lavoro due volte? In nessun caso. È sufficiente eseguire il commit del DDL, eseguirlo attraverso la pipeline di compilazione e ottenere l'entità aggiornata:

@Entity
@Table(name = "book", indexes = {
 
  // Вы об этом задумывались?
  @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 influiscono anche sulla semantica, non solo sulla sintassi. Pertanto, può essere utile esaminare il codice compilato per vedere quale codice sarà (o potrebbe) essere influenzato dall'incremento del database.

L'unica verità

Indipendentemente dalla tecnologia utilizzata, esiste sempre un modello che rappresenta l’unica fonte di verità per alcuni sottosistemi o, come minimo, dovremmo lottare per questo ed evitare tale confusione aziendale, in cui la “verità” è ovunque e da nessuna parte allo stesso tempo. . Tutto potrebbe essere molto più semplice. Se stai semplicemente scambiando file XML con qualche altro sistema, usa semplicemente XSD. Guarda il metamodello INFORMAZIONI_SCHEMA da jOOQ in formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD è ben compreso
  • XSD tokenizza molto bene il contenuto XML e consente la convalida in tutte le lingue client
  • XSD ha una buona versione e ha una compatibilità con le versioni precedenti avanzata
  • XSD può essere tradotto in codice Java utilizzando XJC

L'ultimo punto è importante. Quando comunichiamo con un sistema esterno utilizzando messaggi XML, vogliamo essere sicuri che i nostri messaggi siano validi. Questo è molto facile da ottenere utilizzando JAXB, XJC e XSD. Sarebbe pura follia pensare che, con un approccio progettuale "Java first" in cui realizziamo i nostri messaggi come oggetti Java, questi possano in qualche modo essere mappati in modo coerente su XML e inviati a un altro sistema per il consumo. L'XML generato in questo modo sarebbe di pessima qualità, non documentato e difficile da sviluppare. Se esistesse un accordo sul livello di servizio (SLA) per un'interfaccia di questo tipo, rovineremmo immediatamente tutto.

Sinceramente con le API JSON succede sempre questo, ma questa è un'altra storia, la prossima volta litigherò...

Database: sono la stessa cosa

Quando lavori con i database, ti rendi conto che sono tutti sostanzialmente simili. La base possiede i suoi dati e deve gestire lo schema. Eventuali modifiche apportate allo schema dovranno essere recepite direttamente nel DDL in modo che venga aggiornata l'unica fonte di verità.

Quando si è verificato un aggiornamento dell'origine, tutti i client devono aggiornare anche le proprie copie del modello. Alcuni client possono essere scritti in Java utilizzando jOOQ e Hibernate o JDBC (o entrambi). Altri client possono essere scritti in Perl (auguriamo loro solo buona fortuna), mentre altri possono essere scritti in C#. Non importa. Il modello principale è nel database. I modelli generati utilizzando gli ORM sono generalmente di scarsa qualità, scarsamente documentati e difficili da sviluppare.

Quindi non commettere errori. Non commettere errori fin dall'inizio. Lavora dal database. Costruisci una pipeline di distribuzione che possa essere automatizzata. Abilita i generatori di codice per semplificare la copia del modello di database e il dump sui client. E smettila di preoccuparti dei generatori di codici. Sono buone. Con loro diventerai più produttivo. Devi solo dedicare un po 'di tempo alla loro configurazione fin dall'inizio e poi ti aspettano anni di maggiore produttività, che costituiranno la storia del tuo progetto.

Non ringraziarmi ancora, più tardi.

Chiarificazione

Per essere chiari: questo articolo non sostiene in alcun modo la necessità di piegare l'intero sistema (ad esempio dominio, logica aziendale, ecc. Ecc.) per adattarlo al modello di database. Quello che sto dicendo in questo articolo è che il codice client che interagisce con il database dovrebbe agire sulla base del modello di database, in modo che esso stesso non riproduca il modello di database in uno stato di "prima classe". Questa logica si trova solitamente al livello di accesso ai dati sul tuo client.

Nelle architetture a due livelli, che in alcuni luoghi sono ancora conservate, un tale modello di sistema potrebbe essere l’unico possibile. Tuttavia, nella maggior parte dei sistemi il livello di accesso ai dati mi sembra essere un "sottosistema" che incapsula il modello di database.

eccezioni

Ci sono eccezioni a ogni regola e ho già detto che l'approccio basato sul database e sulla generazione del codice sorgente a volte può essere inappropriato. 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 uno strumento che aiuta gli utenti a navigare in qualsiasi diagramma. Uffa. Non c'è generazione di codice qui. Tuttavia, il database viene prima di tutto.
  • Quando un circuito deve essere generato al volo per risolvere qualche problema. Questo esempio sembra una versione leggermente fantasiosa del modello valore dell'attributo dell'entità, cioè non hai davvero uno schema chiaramente definito. In questo caso, spesso non puoi nemmeno essere sicuro che un RDBMS sia adatto a te.

Le eccezioni sono per natura eccezionali. Nella maggior parte dei casi che prevedono l'utilizzo di un RDBMS, lo schema è noto in anticipo, risiede all'interno dell'RDBMS ed è l'unica fonte di “verità” e tutti i client devono acquisirne le copie derivate. Idealmente, è necessario utilizzare un generatore di codice.

Fonte: habr.com

Aggiungi un commento