Adevărul înainte de toate, sau de ce trebuie să proiectăm sistemul pe baza dispozitivului bazei de date

Salut, Habr!

Continuăm să explorăm subiectul Java și Spring, inclusiv la nivelul bazelor de date. Astăzi, vă propunem să citiți despre motivul pentru care, în proiectarea aplicațiilor mari, structura bazei de date, și nu codul Java, ar trebui să aibă o importanță decisivă, cum se realizează acest lucru și ce excepții există de la această regulă.

În acest articol destul de întârziat, voi explica de ce consider că, în aproape toate cazurile, modelul de date al unei aplicații ar trebui să fie proiectat «în baza bazei de date», mai degrabă decât «în funcție de capabilitățile Java» (sau alt limbaj client cu care lucrați). Alegând a doua abordare, vă angajați pe un drum lung de durere și suferință, de îndată ce proiectul dumneavoastră începe să crească.

Articolul este scris pe baza unei întrebări, adresate pe Stack Overflow.

Discuții interesante pe reddit în secțiunile /r/java și /r/programming.

Generarea codului

Cât de surprins am fost să descopăr că există o comunitate atât de mică de utilizatori care, după ce s-au familiarizat cu jOOQ, se revoltă împotriva faptului că, atunci când lucrați cu jOOQ, se bazează serios pe generarea de cod sursă. Nimeni nu vă oprește să utilizați jOOQ așa cum considerați necesar și nu vă obligă să folosiți generarea de cod. Dar în mod implicit (așa cum este descris în documentație), lucrul cu jOOQ se desfășoară astfel: începeți cu o schemă (moștenită) a bazei de date, efectuați inversarea proiectării folosind generatorul de cod jOOQ, pentru a obține un set de clase care reprezintă tabelele dumneavoastră, iar apoi scrieți interogări tipizate pentru aceste tabele:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Informațiile despre tipuri sunt deduse pe baza codului generat la care se face referire în condiția SELECT de mai jos

       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ nume generate
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Codul este generat fie manual în afara compilării, fie manual la fiecare compilare. De exemplu, o astfel de regenerare poate avea loc imediat după migrările bazei de date Flyway, care pot fi, de asemenea, efectuate manual sau automat.

Generarea de cod sursă

Cu astfel de abordări în generarea codului – manuale și automate – sunt asociate diverse filozofii, avantaje și dezavantaje pe care nu intenționez să le discut detaliat în această articol. Dar, în general, esența codului generat este că permite reproducerea pe Java a „adevărului” pe care îl acceptăm ca un dat, fie în cadrul sistemului nostru, fie în afara acestuia. Într-un anumit sens, același lucru este realizat de compilatoare, care generează bytecode, cod mașină sau alt tip de cod pe baza surselor – obținem o reprezentare a „adevărului” nostru într-o altă limbă, indiferent de motivele specifice.

Există numeroase astfel de generatoare de cod. De exemplu, XJC poate genera cod Java pe baza fișierelor XSD sau WSDL.Principiul este întotdeauna același:

  • Există o anumită adevăr (intern sau extern) – de exemplu, specificația, modelul de date etc.
  • Avem nevoie de o reprezentare locală a acestui adevăr în limbajul nostru de programare.

Și, în general, este întotdeauna rezonabil să generăm o astfel de reprezentare – pentru a evita redundanța.

Furnizori de tipuri și procesarea anotărilor

De remarcat: o altă abordare mai modernă și specifică pentru generarea codului pentru jOOQ este asociată cu utilizarea furnizorilor de tipuri, în așa fel cum sunt implementați în F#.În acest caz, codul este generat de compilator, efectiv în etapa de compilare. În formă de sursă, acest cod nu există în principiu. În Java există instrumente similare, deși nu atât de elegante – acestea sunt procesoarele de anotare, de exemplu, Lombok..

Într-un anumit sens, aici au loc aceleași lucruri ca în prima situație, cu excepția:

  • Nu vedeți codul generat (poate că această situație nu pare atât de respingătoare pentru cineva?)
  • Trebuie să garantați că tipurile pot fi furnizate, adică „adevărul” trebuie să fie întotdeauna accesibil. Acest lucru este ușor în cazul Lombok, care anotează „adevărul”. Este puțin mai complicat cu modelele de baze de date, a căror funcționare depinde de o conexiune activă și constant disponibilă.

Care este problema cu generarea codului?

Pe lângă întrebarea complexă despre cum este mai bine să pornești generarea de cod - manual sau automat, trebuie să menționăm și faptul că există oameni care consideră că generarea de cod este complet inutilă. Justificarea acestui punct de vedere, pe care l-am întâlnit cel mai des, este că este dificil să configurezi o linie de asamblare. Da, cu adevărat este complicat. Apar costuri de infrastructură suplimentare. Dacă abia începi să lucrezi cu un anumit produs (fie că este vorba de jOOQ, JAXB, Hibernate, etc.), timpul necesar pentru a configura mediul de lucru este timp pe care ai dori să-l dedici studiului API-ului pentru a extrage valoare din acesta.

Dacă costurile asociate cu înțelegerea funcționării generatorului sunt prea mari, atunci, într-adevăr, API-ul a avut o utilizabilitate slabă a generatorului de cod (iar mai târziu se dovedește că și personalizarea utilizatorului este complicată). Ușurința de utilizare ar trebui să fie o prioritate maximă pentru orice astfel de API. Dar aceasta este doar un argument împotriva generării de cod. În rest, este complet necesar să scrii manual o reprezentare locală a unei realități interne sau externe.

Mulți vor spune că nu au timp să se ocupe de toate acestea. Au termene limită strânse pentru Super-Produsul lor. Odată, ulterior, ne vom ocupa de liniile de asamblare, mai avem timp. Le voi răspunde:

Adevărul înainte de toate, sau de ce trebuie să proiectăm sistemul pe baza dispozitivului bazei de date
Original, Alan O’Rourke, Audience Stack

Dar în Hibernate / JPA este atât de simplu să scrii cod "pentru Java".

Așa este. Pentru Hibernate și utilizatorii săi, aceasta este simultan o binecuvântare și un blestem. În Hibernate poți reda cu ușurință câteva entități, astfel:

	@Entity
class Book {
  @Id
  int id;
  String title;
}

Și aproape totul este gata. Acum, responsabilitatea Hibernate este de a genera detaliile „complexe” despre cum exact această entitate va fi definită în DDL pentru dialectul tău 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);

… și începem să rulăm aplicația. O oportunitate cu adevărat grozavă pentru a începe rapid și a testa diverse lucruri.

Cu toate acestea, permiteți-mi. Am fost puțin mincinos.

  • Dar va aplica Hibernate cu adevărat definiția acestei chei primare denumite?
  • Va crea Hibernate un index în TITLE? – știu sigur că ne va fi necesar.
  • Va face Hibernate cu adevărat această cheie identificatoare conform specificației Identity?

Probabil că nu. Dacă dezvoltați proiectul vostru de la zero, este întotdeauna convenabil să aruncați vechea bază de date și să generați una nouă, odată ce ați adăugat anotările necesare. Astfel, entitatea Book va arăta în cele din urmă așa:

	@Entity
@Table(name = "book", indexes = {
  @Index(name = "i_book_title", columnList = "title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
  String title;
}

Super. Generează din nou. Din nou, în acest caz, la început va fi foarte ușor.

Dar mai târziu va trebui să plătiți pentru asta.

Mai devreme sau mai târziu va trebui să ieșiți în producție. Atunci modelul respectiv nu va mai funcționa. Deoarece:

În producție nu va mai fi posibil să aruncați vechea bază de date și să începeți totul de la zero. Baza dvs. de date va deveni moștenită.

De acum înainte, va trebui să scrieți scripturi de migrare DDL, de exemplu, cu ajutorul Flyway.. Ce se va întâmpla cu entitățile voastre în acest caz? Puteți fie să le adaptați manual (și astfel să vă dublați volumul de muncă), fie să cereți Hibernate să le genereze din nou pentru voi (cât de mari sunt șansele ca ceea ce va fi generat astfel să corespundă așteptărilor voastre?) În orice caz, aveți de pierdut.

Astfel, imediat ce treceți în producție, veți avea nevoie de patch-uri rapide. Iar acestea trebuie implementate în producție foarte rapid. Deoarece nu v-ați pregătit și nu ați organizat o livrare lină a migrațiilor voastre pentru producție, veți face totul în întârziere. Apoi nu veți mai avea timp să faceți totul corect. Și veți da vina pe Hibernate, deoarece întotdeauna e vinovat oricine, doar nu voi…

În schimb, de la bun început, totul putea fi făcut complet diferit. De exemplu, să pui roți circulare pe bicicletă.

Mai întâi baza de date.

Adevărata „adevărată” în schema bazei voastre de date și „suveranitatea” asupra acesteia se află în interiorul bazei de date. Schema este definită doar în baza de date și nicăieri altundeva, iar fiecare client are o copie a acestei scheme, așa că este complet logic să impuneți respectarea schemei și integritatea acesteia, făcând acest lucru direct în baza de date – acolo unde este stocată informația.
Este o înțelepciune veche, chiar bătută. Cheile primare și unice sunt bune. Cheile externe – sunt bune. Verificarea constrângerilor – este bună. Afirmările – sunt bune.

Și aceasta nu este tot. De exemplu, folosind Oracle, probabil că doriți să specificați:

  • În ce spațiu de tabele se află tabela dumneavoastră
  • Ce valoare are PCTFREE
  • Care este dimensiunea cache-ului din secvența dumneavoastră (după identificator)

Poate că toate acestea nu sunt importante în sistemele mici, dar nu trebuie să așteptați trecerea în domeniul «big data» — puteți începe să profitați de optimizările de stocare oferite de furnizor chiar mai devreme, cum ar fi cele menționate mai sus. Niciun ORM pe care l-am văzut (inclusiv jOOQ) nu oferă acces la întregul set de opțiuni DDL pe care ați putea dori să le utilizați în baza de date. ORM-urile oferă unele instrumente care ajută la scrierea DDL.

Dar, până la urmă, o schemă bine proiectată este scrisă manual în DDL. Orice DDL generat este doar o aproximație a acesteia.

Ce ziceți de modelul clientului?

Așa cum s-a menționat mai sus, pe client veți avea nevoie de o copie a schemei bazei de date, o reprezentare client. Este de la sine înțeles că această reprezentare client trebuie să fie sincronizată cu modelul real. Cum este cel mai bine să realizăm acest lucru? Prin intermediul unui generator de cod.

Toate bazele de date oferă metainformațiile lor prin SQL. Iată cum să obțineți toate tabelele din baza dumneavoastră de date în diferite dialecte 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

Aceste interogări (sau similare, în funcție de dacă trebuie să luați în considerare și vizualizările, viziuni materializate, funcții cu valori de tabel) sunt, de asemenea, executate prin apelul DatabaseMetaData.getTables() din JDBC, sau prin intermediul modulului meta jOOQ.

Din rezultatele acestor interogări, este relativ ușor să generați orice reprezentare client a modelului bazei de date, indiferent de tehnologia utilizată pe client.

  • Dacă utilizați JDBC sau Spring, puteți crea un set de constante de tip șir
  • Dacă folosiți JPA, puteți genera singuri entitățile
  • Dacă folosiți jOOQ, puteți genera meta-modelul jOOQ

În funcție de volumul de funcționalități oferite de API-ul clientului dumneavoastră (de exemplu, jOOQ sau JPA), meta-modelul generat poate fi cu adevărat bogat și complet. Să luăm, de exemplu, posibilitatea unirilor implicite, apărută în jOOQ 3.11, care se bazează pe informațiile meta generate despre relațiile dintre cheile externe care acționează între tabelele dumneavoastră.

Acum, orice modificare a bazei de date va duce automat la actualizarea codului clientului. Imaginați-vă, de exemplu:

ALTER TABLE book RENAME COLUMN title TO book_title;

Chiar ați vrea să faceți această muncă de două ori? Nici vorbă. Pur și simplu înregistrăm DDL-ul, îl rulăm prin canalul dumneavoastră de compilare și obținem entitatea actualizată:

@Entity
@Table(name = "book", indexes = {
 
  // V-ați gândit la asta?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Sau clasa jOOQ actualizată. Majoritatea modificărilor DDL se reflectă și în semantica, nu doar în sintaxă. De aceea, este adesea util să consultați codul compilat pentru a vedea ce cod va (sau poate fi) afectat de modificările bazei dumneavoastră de date.

Adevărul unic

Indiferent de tehnologia pe care o utilizați, întotdeauna există un model care constituie sursa unică a adevărului pentru o anumită subsistemă – sau, cel puțin, ar trebui să ne străduim să facem astfel și să evităm confuzia enterprise, în care „adevărul” este simultan peste tot și nicăieri. Totul poate fi mult mai simplu. Dacă pur și simplu schimbați fișiere XML cu un alt sistem, folosiți doar XSD. Consultați meta-modelul INFORMATION_SCHEMA din jOOQ în format XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD este ușor de înțeles
  • XSD marchează foarte bine conținutul XML și permite validarea pe toate limbile clientului
  • XSD este bine versiunezi și are o compatibilitate înapoi dezvoltată
  • XSD poate fi transformat în cod Java cu ajutorul XJC

Ultimul punct este important. Când comunicăm cu un sistem extern prin mesaje XML, vrem să ne asigurăm de validitatea mesajelor noastre. Acest lucru este foarte ușor de realizat folosind JAXB, XJC și XSD. Ar fi o nebunie să ne așteptăm ca, abordând designul "Java mai întâi", unde facem mesajele noastre sub formă de obiecte Java, acestea să poată fi reprezentate într-un mod clar în XML și trimise pentru a fi consumate de un alt sistem. XML-ul generat astfel ar fi de foarte slabă calitate, ne-documentat și dificil de dezvoltat. Dacă ar exista un acord de nivel de serviciu (SLA) pentru o astfel de interfață, l-am strica imediat.

Adevărul este că exact asta se întâmplă constant cu API-urile pe JSON, dar asta este o altă poveste, data viitoare o să mai vorbesc...

Baze de date: este același lucru

Când lucrați cu baze de date, înțelegeți că toate sunt, în principiu, asemănătoare. O bază de date deține datele sale și ar trebui să conducă schema. Orice modificare adusă schemei trebuie implementată direct pe DDL, astfel încât să se actualizeze sursa unică a adevărului.

Când actualizarea sursei a avut loc, toți clienții ar trebui să-și actualizeze și copiile modelului. Unii clienți pot fi scriși în Java folosind jOOQ și Hibernate sau JDBC (sau toate la un loc). Alți clienți pot fi scriși în Perl (le doresc mult noroc), iar alții – în C#. Nu contează. Principalul model se află în baza de date. Modelele generate prin ORM sunt de obicei de slabă calitate, puțin documentate și greu de dezvoltat.

De aceea, nu faceți greșeli. De la început, nu faceți greșeli. Lucrați, bazându-vă pe baza de date. Construiește un asemenea pipeline de desfășurare care poate fi automatizat. Include generatoare de cod pentru a copia cu ușurință modelul bazei de date și a-l descărca pe clienți. Și încetați să vă faceți griji în legătură cu generatoarele de cod. Ele sunt bune. Cu acestea veți deveni mai productiv. Trebuie doar să investiți puțin timp la început pentru a le configura – și pe parcurs, veți avea ani de productivitate crescută, din care se va țese povestea proiectului vostru.

Nu mulțumiți încă, mai târziu.

Explicație

Pentru claritate: Această articol nu promovează în niciun caz ideea că modelul bazei de date ar trebui să definească întreaga sistemă (adică, domeniul, logica de afaceri etc.). În acest articol, discut despre faptul că codul clientului care interacționează cu baza de date ar trebui să funcționeze având ca bază modelul bazei de date, astfel încât modelul să nu fie redat ca un entitate de clasă întâi în sine. Această logică se află, de obicei, la nivelul accesului la date pe clientul dumneavoastră.

În arhitecturile cu două niveluri, care încă mai există în unele locuri, un astfel de model de sistem poate fi singura variantă posibilă. Totuși, în majoritatea sistemelor, nivelul accesului la date mi se pare a fi o «sub-sistemă», care incapsulează modelul bazei de date.

Excepții

Din orice regulă există excepții, și am menționat deja că abordarea centrată pe baza de date și generarea codului sursă poate fi uneori inadecvată. Iată câteva astfel de excepții (probabil că mai există și altele):

  • Când schema este necunoscută și trebuie să fie deschisă. De exemplu, sunteți un furnizor de instrumente care ajută utilizatorii să se orienteze în orice schemă. Uff. Aici este nevoie de generarea de cod. Dar totuși – baza de date trebuie să fie prioritară.
  • Când schema trebuie generată în timp real pentru a rezolva o anumită problemă. Acest exemplu pare să fie o variantă ușor extravagantă a pattern-ului entity attribute value, deci, în realitate, nu aveți o schemă clar definită. În acest caz, adesea nu se poate fi sigur că un RDBMS va fi adecvat.

Excepțiile sunt, prin natura lor, excepționale. În majoritatea cazurilor care implică utilizarea unui RDBMS, schema este cunoscută din timp, se află în interiorul RDBMS și reprezintă singura sursă de "adevăr", iar toți clienții sunt nevoiți să obțină copii derivate din aceasta. În ideal, ar trebui implicat un generator de cod.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster