La verità prima di tutto, o perché è necessario progettare un sistema in base alla struttura del database

Ciao, Habr!

Continuiamo ad esplorare il tema Java e Spring, incluso a livello di database. Oggi vi proponiamo di leggere perché, nella progettazione di grandi applicazioni, la struttura del database, e non il codice Java, dovrebbe avere un ruolo cruciale, come si fa e quali eccezioni ci sono a questa regola.

In questo articolo piuttosto tardivo spiegherò perché ritengo che nel pratico la modelazione dei dati in un'applicazione debba essere progettata "partendo dal database", e non "partendo dalle capacità di Java" (o di un altro linguaggio client con cui stai lavorando). Scegliendo il secondo approccio, ti avventuri in un lungo cammino di sofferenza non appena il tuo progetto inizia a crescere.

L'articolo è scritto ispirandosi a una domanda, posta su Stack Overflow.

Discussioni interessanti su reddit nelle sezioni /r/java e /r/programming.

Generazione di codice

Sono rimasto sorpreso nel vedere che esiste una ristretta fascia di utenti che, dopo aver familiarizzato con jOOQ, si lamentano del fatto che, nell'utilizzo di jOOQ, ci si affida seriamente alla generazione del codice sorgente. Nessuno vi impedisce di utilizzare jOOQ come ritenete opportuno e non vi obbliga a utilizzare la generazione del codice. Tuttavia, di default (come descritto nella guida) l'operazione con jOOQ avviene in questo modo: iniziate con lo schema del database (eredidato), eseguite la sua ingegnerizzazione inversa utilizzando il generatore di codice jOOQ, per ottenere un insieme di classi che rappresentano le vostre tabelle, e poi scrivete query tipo-sicure su queste tabelle:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Le informazioni sui tipi sono derivate da 
//   il codice 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ò avvenire immediatamente dopo migrazione del database Flyway, che può essere eseguita anche manualmente o automaticamente.

Generazione del codice sorgente

Con questi approcci alla generazione del codice – manuali e automatici – sono collegate diverse filosofie, vantaggi e svantaggi, che non intendo approfondire in questo articolo. Ma, in generale, l'essenza del codice generato è che permette di riprodurre in Java quella "verità" che consideriamo un dato di fatto, sia all'interno del nostro sistema che all'esterno. In un certo senso, lo stesso fanno i compilatori, che generano bytecode, codice macchina o qualche altro tipo di codice sulla base dei sorgenti – otteniamo una rappresentazione della nostra "verità" in un'altra lingua, indipendentemente dalle cause specifiche.

Esistono molti di questi generatori di codice. Ad esempio, XJC può generare codice Java basato su file XSD o WSDL. 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.

In effetti, generare tale rappresentazione è quasi sempre consigliabile per evitare ridondanze.

Provider di tipi e elaborazione delle annotazioni

Nota: un altro approccio più moderno e specifico per la generazione di codice per jOOQ è legato all'uso di provider di tipi, così come sono implementati in F#. In questo caso, il codice viene generato dal compilatore, proprio nella fase di compilazione. In forma di sorgenti, tale codice in linea di principio non esiste. In Java esistono strumenti simili, sebbene non così raffinati: sono i processori di annotazioni, ad esempio, Lombok.

In un certo senso, qui avvengono le stesse cose di prima, a eccezione di:

  • Non vedete il codice generato (forse questa situazione non sembra così sgradevole a qualcuno?)
  • Devi garantire che i tipi possano essere forniti, cioè, "la verità" deve sempre essere disponibile. Questo è facile nel caso di Lombok, che annota "la verità". Un po’ più complesso con i modelli di database, la cui operatività dipende da una connessione attiva sempre disponibile.

Qual è il problema con la generazione di codice?

Oltre alla questione su quale sia il modo migliore per avviare la generazione del codice – manualmente o automaticamente, va anche detto che ci sono persone che ritengono che la generazione del codice non sia affatto necessaria. La giustificazione più comune che ho incontrato per questo punto di vista è che in tal caso risulta difficile configurare la catena di montaggio. È vero, è davvero difficile. Ci sono costi infrastrutturali aggiuntivi. Se stai appena iniziando a lavorare con un determinato prodotto (sia esso jOOQ, JAXB, Hibernate, ecc.), ci vuole tempo per configurare l'ambiente di lavoro, tempo che preferiresti investire nello studio dell'API stessa per poter poi estrarre valore da essa.

Se i cost di comprendere come funziona il generatore sono troppo elevati, allora si può dire che l'API non ha fatto un buon lavoro riguardo all'usabilità del generatore di codice (e in seguito si scopre che anche la personalizzazione dell'utente è complessa). La facilità d'uso dovrebbe essere la massima priorità per qualsiasi API di questo tipo. Ma questo è solo un argomento contro la generazione di codice. Altrimenti, scrivere manualmente una rappresentazione locale della verità interna o esterna è del tutto possibile.

Molti diranno che non hanno tempo per occuparsi di tutto ciò. Hanno scadenze serrate per il loro Super-Prodotto. Poi, in futuro, miglioreremo le linee di assemblaggio, ci sarà tempo. Io rispondo loro:

La verità prima di tutto, o perché è necessario progettare un sistema in base alla struttura del database
Originale, Alan O'Rourk, Audience Stack

Ma in Hibernate / JPA è così facile scrivere codice «per Java».

Davvero. Per Hibernate e i suoi utenti è 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 la responsabilità di Hibernate è generare le complessità dei «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 iniziamo a utilizzare l'applicazione. Davvero una possibilità fantastica per iniziare rapidamente e provare cose diverse.

Tuttavia, permettetemi di correggermi.

  • Hibernate applicherà davvero la definizione di questa chiave primaria denominata?
  • Hibernate creerà un indice su TITLE? – so per certo che ne avremo bisogno.
  • A Hibernate farà di questo chiave un identificatore nella Identity Specification?

Probabilmente no. Se stai sviluppando il tuo progetto da zero, è sempre comodo semplicemente eliminare il vecchio database e generarne uno nuovo quando aggiungi le annotazioni necessarie. Così, l'entità Book alla fine apparirà così:

	@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 una volta, in tal caso, sarà molto facile all'inizio.

Ma poi bisognerà fare dei pagamenti.

Prima o poi dovrà essere messo in produzione. È proprio allora che questo modello smetterà di funzionare. Perché:

In produzione non sarà più possibile, se necessario, scartare il vecchio database e ricominciare da zero. Il vostro database diventerà ereditato.

Da ora in poi e per sempre dovrete scrivere script di migrazione DDL, ad esempio utilizzando Flyway. E cosa accadrà alle vostre entità in questo caso? Potrete adattarle manualmente (e in questo modo raddoppierete il vostro carico di lavoro), oppure direte a Hibernate di rigenerarle per voi (quanto è probabile che la generazione corrisponda alle vostre aspettative?) In ogni caso, sarete in svantaggio.

Pertanto, non appena passerete in produzione, avrete bisogno di patch rapidi. E queste devono essere rilasciate in produzione molto rapidamente. Poiché non vi siete preparati e non avete organizzato una fluida conveyorizzazione delle vostre migrazioni per la produzione, farete patch di qualsiasi tipo. E poi non riuscirete a fare tutto correttamente in tempo. E incolperete Hibernate, perché è sempre colpa di qualcun altro, mai vostra...

Invece, dall'inizio si poteva fare tutto in modo completamente diverso. Ad esempio, mettere ruote rotonde su una bicicletta.

Inizialmente, il database

La vera "verità" nello schema del tuo database e la "sovranità" su di esso risiedono all'interno del database. Lo schema è definito solo nel database stesso e non altrove, e ciascun cliente ha una copia di questo schema, quindi è assolutamente sensato imporre il rispetto dello schema e della sua integrità direttamente nel database, dove risiede l'informazione.
Questa è una saggezza antica, infatti trita. Le chiavi primarie e uniche sono importanti. Le chiavi esterne sono utili. Il controllo delle restrizioni è positivo. Affermare – è positivo.

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

  • In quale tablespace si trova la tua tabella
  • Qual è il suo valore PCTFREE
  • Qual è la dimensione della cache nella tua sequenza (dietro l'identificatore)

For small systems, this might not matter much, but you don't have to wait until you enter the realm of "big data" — you can start benefiting from the optimizations offered by your provider earlier, such as those mentioned above. None of the ORMs I’ve encountered (including jOOQ) provide access to the full set of DDL options you might want to use in your database. ORMs offer some tools that help in writing DDL.

But ultimately, a well-designed schema is hand-written in DDL. Any generated DDL is merely an approximation of it.

What about the client model?

As mentioned earlier, on the client side, you will need a copy of your database schema, the client representation. It goes without saying that this client representation must be synchronized with the actual model. What is the best way to achieve this? Using a code generator.

All databases provide their metadata through SQL. Here’s how to retrieve all tables from your database in different SQL dialects:

	-- 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 le viste, le viste materializzate e le funzioni con valore tabellare) vengono anche eseguite tramite la chiamata DatabaseMetaData.getTables() da JDBC, oppure tramite il modulo meta di jOOQ.

Dai risultati di tali query, è relativamente facile generare qualsiasi vista client del modello del tuo database, indipendentemente dalla tecnologia utilizzata nel client.

  • Se utilizzi JDBC o Spring, puoi creare un insieme di costanti stringa
  • Se usi JPA, puoi generare tu stesso le entità
  • Se utilizzi jOOQ, puoi generare il meta-modello di jOOQ

A seconda di quali funzionalità offre la tua API client (ad es. jOOQ o JPA), il meta-modello generato può essere veramente ricco e completo. Prendiamo ad esempio le unioni implicite, introdotte in jOOQ 3.11, che si basa sulle meta informazioni generate sulle relazioni chiave esterne che agiscono tra le tue tabelle.

Ora qualsiasi incremento del tuo database comporterà automaticamente l'aggiornamento del codice client. Immagina, ad esempio:

ALTER TABLE book RENAME COLUMN title TO book_title;

Davvero vorresti fare questo lavoro due volte? Assolutamente no. Registriamo semplicemente il DDL, lo facciamo passare attraverso la tua pipeline di build e otteniamo l'entità aggiornata:

@Entity
@Table(name = "book", indexes = {
 
  // Ci hai 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 riflettono anche sulla semantica, non solo sulla sintassi. Pertanto, è utile dare un'occhiata al codice compilato per vedere quale codice sarà (o potrebbe essere) influenzato dall'incremento del tuo database.

L'unica verità

Indipendentemente dalla tecnologia che utilizzi, c'è sempre un modello che rappresenta l'unica fonte di verità per un dato sottosistema – o, quanto meno, dovremmo cercare di raggiungerlo e evitarci una confusione aziendale in cui la "verità" è ovunque e in nessun posto. Tutto può essere molto più semplice. Se stai semplicemente scambiando file XML con un'altra sistema, utilizza semplicemente l'XSD. Dai un'occhiata alla meta-modello INFORMATION_SCHEMA di jOOQ in formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • L'XSD è ben compreso
  • L'XSD contrassegna molto bene il contenuto XML e consente la validazione in tutti i linguaggi client
  • L'XSD ha una buona gestione delle versioni e offre una compatibilità retroattiva avanzata
  • L'XSD può essere tradotta in codice Java tramite XJC

L'ultimo punto è importante. Quando comunichiamo con un sistema esterno utilizzando messaggi XML, vogliamo assicurarci della validità dei nostri messaggi. Questo è molto facile da ottenere grazie a JAXB, XJC e XSD. Sarei completamente folle a pensare che, con un approccio di progettazione "Java first", in cui realizziamo i nostri messaggi come oggetti Java, essi possano essere in qualche modo mappati chiaramente su XML e inviati per essere consumati in un altro sistema. XML generato in questo modo sarebbe di scarsa qualità, non documentato e difficile da manutenere. Se ci fosse un accordo sul livello di servizio (SLA) per questa interfaccia, lo avremmo già compromesso.

A dire il vero, è proprio quello che succede continuamente con le API JSON, ma questa è un'altra storia, la prossima volta mi sfogherò...

Database: sono tutti uguali

Lavorando con i database, ci si rende conto che essi sono, in linea di principio, simili. Un database possiede i propri dati e deve gestire lo schema. Qualsiasi modifica apportata allo schema deve essere implementata direttamente su DDL per aggiornare l'unica fonte di verità.

Quando si verifica un aggiornamento della sorgente, 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 (buona fortuna a loro), e altri ancora in C#. Non è importante. Il modello principale risiede nel database. I modelli generati tramite ORM sono generalmente di scarsa qualità, poco documentati e difficili da sviluppare.

Quindi, non commettete errori. Fin dall'inizio, non fate errori. Lavorate basandovi sul database. Costruite una pipeline di distribuzione che possa essere automatizzata. Includete generatori di codice per copiare comodamente il modello del vostro database e distribuirlo ai clienti. E smettete di preoccuparvi dei generatori di codice. Sono utili. Con loro diventerete più produttivi. Basta dedicare un po' di tempo alla loro configurazione iniziale – e poi vi aspettano anni di maggiore produttività, che costituiranno la storia del vostro progetto.

Non ringraziate ancora, dopo.

Spiegazione

Per chiarezza: Questo articolo non sostiene in alcun modo che l'intero sistema (ovvero, il dominio, la logica aziendale, ecc.) debba essere piegato al modello del tuo database. In questo articolo, parlo del fatto che il codice client che interagisce con il database deve operare basandosi sul modello del database, in modo che la sua struttura non riproduca il modello del database come "prima classe". Questa logica si trova solitamente a livello di accesso ai dati sul client.

Nelle architetture a due livelli, che ancora in alcuni casi sono presenti, tale modello di sistema può essere l'unica opzione 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 qualsiasi regola ci sono eccezioni, e ho già detto che l'approccio con la priorità del database e la generazione di codice sorgente può a volte risultare inadeguato. Ecco un paio di tali eccezioni (probabilmente ce ne saranno altre):

  • Quando lo schema è sconosciuto e deve essere aperto. Ad esempio, sei un fornitore di strumenti che aiutano gli utenti a orientarsi in qualsiasi schema. Uff. Qui niente generazione di codice. Ma comunque, il database prima di tutto.
  • Quando lo schema deve essere generato al volo per risolvere un certo problema. Questo esempio sembra una versione leggermente sofisticata del pattern. entity attribute value, ovvero, non hai realmente uno schema definito. In questo caso, spesso non si può nemmeno essere certi che un RDBMS sia adatto.

Le eccezioni sono per loro natura eccezionali. Nella maggior parte dei casi legati all'uso di RDBMS, lo schema è già noto e si trova all'interno dell'RDBMS ed è l'unica fonte di 'verità', e tutti i clienti devono creare copie derivate. Idealmente, in questo caso, bisognerebbe coinvolgere un generatore di codice.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster