Primer, la veritat, o per què s'ha de dissenyar un sistema basat en el disseny de la base de dades

Hola Habr!

Continuem investigant el tema Java и primavera, inclòs a nivell de base de dades. Avui us convidem a llegir per què, a l'hora de dissenyar aplicacions grans, és l'estructura de la base de dades, i no el codi Java, la que hauria de tenir una importància decisiva, com es fa i quines excepcions hi ha a aquesta regla.

En aquest article bastant tardà, explicaré per què crec que en gairebé tots els casos, el model de dades d'una aplicació s'hauria de dissenyar "des de la base de dades" en lloc de "des de les capacitats de Java" (o qualsevol llenguatge de client que tingueu). treballant amb). En adoptar el segon enfocament, t'estàs preparant per un llarg camí de dolor i patiment una vegada que el teu projecte comença a créixer.

L'article va ser escrit a partir de una pregunta, donat a Stack Overflow.

Debats interessants sobre reddit en seccions /r/java и /r/programació.

Generació de codi

Com em va sorprendre que hi hagi un segment tan reduït d'usuaris que, després d'haver-se familiaritzat amb jOOQ, estan indignats pel fet que jOOQ confiï seriosament en la generació de codi font per funcionar. Ningú t'impedeix utilitzar jOOQ com creguis adequat, ni t'obliga a utilitzar la generació de codi. Però la manera predeterminada (tal com es descriu al manual) de treballar amb jOOQ és que comenceu amb un esquema de base de dades (legat), feu-ne enginyeria inversa utilitzant el generador de codi jOOQ per obtenir així un conjunt de classes que representin les vostres taules i, a continuació, escriviu el tipus. -consultes segures a aquestes taules:

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

El codi es genera manualment fora del conjunt o manualment a cada conjunt. Per exemple, aquesta regeneració pot seguir immediatament després Migració de bases de dades Flyway, que també es pot fer manualment o automàticament.

Generació de codi font

Hi ha diverses filosofies, avantatges i desavantatges associats amb aquests enfocaments de generació de codi -manual i automàtica- que no parlaré en detall en aquest article. Però, en general, l'objectiu del codi generat és que ens permet reproduir en Java aquella “veritat” que donem per feta, ja sigui dins del nostre sistema o fora d'ell. En cert sentit, això és el que fan els compiladors quan generen bytecode, codi màquina o alguna altra forma de codi font: obtenim una representació de la nostra "veritat" en un altre llenguatge, independentment de les raons específiques.

Hi ha molts generadors de codi d'aquest tipus. Per exemple, XJC pot generar codi Java basat en fitxers XSD o WSDL. El principi és sempre el mateix:

  • Hi ha alguna veritat (interna o externa), per exemple, una especificació, un model de dades, etc.
  • Necessitem una representació local d'aquesta veritat en el nostre llenguatge de programació.

A més, gairebé sempre és aconsellable generar aquesta representació per evitar la redundància.

Proveïdors de tipus i processament d'anotacions

Nota: un altre enfocament més modern i específic per generar codi per a jOOQ és utilitzar proveïdors de tipus, ja que estan implementats en F#. En aquest cas, el codi el genera el compilador, de fet en l'etapa de compilació. En principi, aquest codi no existeix en forma font. Java té eines similars, encara que no tan elegants: processadors d'anotacions, per exemple, Lombok.

En cert sentit, aquí succeeixen les mateixes coses que en el primer cas, amb l'excepció de:

  • No veus el codi generat (potser aquesta situació sembla menys repulsiva per a algú?)
  • Heu d'assegurar-vos que es poden proporcionar tipus, és a dir, "true" ha d'estar sempre disponible. Això és fàcil en el cas de Lombok, que anota "veritat". És una mica més complicat amb els models de bases de dades que depenen d'una connexió en directe disponible constantment.

Quin és el problema amb la generació de codi?

A més de la delicada pregunta de la millor manera d'executar la generació de codi, manualment o automàticament, també hem d'esmentar que hi ha gent que creu que la generació de codi no és necessària. La justificació d'aquest punt de vista, amb el qual em vaig trobar més sovint, és que aleshores és difícil configurar un gasoducte de construcció. Sí, és realment difícil. Es produeixen costos addicionals d'infraestructura. Si acabeu de començar amb un producte en concret (ja sigui jOOQ, o JAXB, o Hibernate, etc.), la configuració d'un entorn de producció requereix un temps que preferiu dedicar-vos a aprendre la pròpia API perquè pugueu extreure'n valor. .

Si els costos associats a la comprensió de l'estructura del generador són massa alts, aleshores, de fet, l'API va fer un mal treball en la usabilitat del generador de codi (i més tard resulta que la personalització de l'usuari també és difícil). La usabilitat hauria de ser la màxima prioritat per a qualsevol API d'aquest tipus. Però aquest és només un argument en contra de la generació de codi. En cas contrari, és totalment manual escriure una representació local de la veritat interna o externa.

Molts diran que no tenen temps per fer tot això. S'estan acabant els terminis per al seu Super Producte. Algun dia endreçarem els transportadors de muntatge, tindrem temps. Els respondré:

Primer, la veritat, o per què s'ha de dissenyar un sistema basat en el disseny de la base de dades
Original, Alan O'Rourke, Audience Stack

Però a Hibernate/JPA és molt fàcil escriure codi Java.

Realment. Per a Hibernate i els seus usuaris, això és alhora una benedicció i una maledicció. A Hibernate podeu escriure simplement un parell d'entitats, com aquesta:

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

I gairebé tot està llest. Ara li toca a Hibernate generar els complexos "detalls" de com es definirà exactament aquesta entitat al DDL del vostre "dialecte" 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 comenceu a executar l'aplicació. Una oportunitat realment genial per començar ràpidament i provar coses diferents.

Tanmateix, si us plau, permeteu-me. Estava mentint.

  • Hibernate imposarà realment la definició d'aquesta clau primària anomenada?
  • Hibernate crearà un índex a TITLE? —Sé del cert que ho necessitarem.
  • Hibernate farà que aquesta clau s'identifiqui exactament a l'especificació d'identitat?

Probablement no. Si esteu desenvolupant el vostre projecte des de zero, sempre és convenient descartar la base de dades antiga i generar-ne una de nova tan bon punt afegiu les anotacions necessàries. Així, l'entitat Llibre adoptarà finalment la forma:

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

Guai. Regenerar. De nou, en aquest cas serà molt fàcil al principi.

Però l'hauràs de pagar més tard

Tard o d'hora hauràs d'entrar en producció. És llavors quan aquest model deixarà de funcionar. Perquè:

En producció, ja no serà possible, si cal, descartar la base de dades antiga i començar des de zero. La vostra base de dades es convertirà en llegat.

A partir d'ara i per sempre hauràs d'escriure Scripts de migració DDL, per exemple, utilitzant Flyway. Què passarà amb les vostres entitats en aquest cas? Podeu adaptar-los manualment (i, per tant, duplicar la vostra càrrega de treball), o bé podeu dir-li a Hibernate que els regeneri (quina probabilitat és que els generats d'aquesta manera compleixin les vostres expectatives?) De qualsevol manera, perds.

Per tant, un cop entri en producció, necessitareu pegats calents. I s'han de posar en producció molt ràpidament. Com que no us vau preparar i no vau organitzar un pipeline sense problemes de les vostres migracions per a la producció, ho feu pedaços tot. I llavors ja no tens temps de fer-ho tot correctament. I tu critiques Hibernate, perquè sempre és culpa d'algú, però no tu...

En canvi, les coses es podrien haver fet d'una manera completament diferent des del principi. Per exemple, poseu rodes rodones a una bicicleta.

Primer la base de dades

La "veritat" real de l'esquema de la vostra base de dades i la "sobirania" sobre ella es troben dins de la base de dades. L'esquema es defineix només a la base de dades en si i enlloc, i cada client té una còpia d'aquest esquema, per la qual cosa té molt sentit fer complir l'esquema i la seva integritat, per fer-ho bé a la base de dades, on es troba la informació. emmagatzemat.
Això és vella, fins i tot saviesa triturada. Les claus primàries i úniques són bones. Les claus externes són bones. Comprovar les restriccions és bo. Afirmacions - Bé.

A més, això no és tot. Per exemple, amb Oracle, probablement voldreu especificar:

  • En quin espai de taula està la teva taula?
  • Quin és el seu valor PCTFREE?
  • Quina és la mida de la memòria cau a la vostra seqüència (darrere de l'identificador)

Pot ser que això no sigui important en sistemes petits, però no cal que espereu fins que us moveu a l'àmbit de les grans dades; podeu començar a beneficiar-vos de les optimitzacions d'emmagatzematge proporcionades pel proveïdor com les esmentades anteriorment molt abans. Cap dels ORM que he vist (inclòs jOOQ) ofereix accés al conjunt complet d'opcions DDL que potser voldreu utilitzar a la vostra base de dades. Els ORM ofereixen algunes eines que us ajuden a escriure DDL.

Però al final del dia, un circuit ben dissenyat està escrit a mà en DDL. Qualsevol DDL generat és només una aproximació.

Què passa amb el model de client?

Com s'ha esmentat anteriorment, al client necessitareu una còpia de l'esquema de la vostra base de dades, la vista del client. No cal esmentar que aquesta vista del client ha d'estar sincronitzada amb el model real. Quina és la millor manera d'aconseguir-ho? Utilitzant un generador de codi.

Totes les bases de dades proporcionen la seva metainformació mitjançant SQL. A continuació s'explica com obtenir totes les taules de la vostra base de dades en diferents dialectes 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

Aquestes consultes (o altres similars, depenent de si també cal tenir en compte vistes, vistes materialitzades, funcions amb valors de taula) també s'executen cridant DatabaseMetaData.getTables() des de JDBC o utilitzant el metamòdul jOOQ.

A partir dels resultats d'aquestes consultes, és relativament fàcil generar qualsevol representació del client del vostre model de base de dades, independentment de quina tecnologia utilitzeu al client.

  • Si utilitzeu JDBC o Spring, podeu crear un conjunt de constants de cadena
  • Si utilitzeu JPA, podeu generar les entitats elles mateixes
  • Si utilitzeu jOOQ, podeu generar el metamodel jOOQ

Depenent de la quantitat de funcionalitats que ofereix l'API del vostre client (per exemple, jOOQ o JPA), el metamodel generat pot ser molt ric i complet. Preneu, per exemple, la possibilitat d'unions implícites, introduït a jOOQ 3.11, que es basa en la metainformació generada sobre les relacions de clau estrangera que hi ha entre les taules.

Ara qualsevol increment de la base de dades actualitzarà automàticament el codi del client. Imagineu per exemple:

ALTER TABLE book RENAME COLUMN title TO book_title;

De veritat t'agradaria fer aquesta feina dues vegades? En cap cas. Només heu de confirmar el DDL, executar-lo a través del vostre pipeline de compilació i obtenir l'entitat actualitzada:

@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;
}

O la classe jOOQ actualitzada. La majoria dels canvis de DDL també afecten la semàntica, no només la sintaxi. Per tant, pot ser útil mirar el codi compilat per veure quin codi es veurà (o podria) afectar per l'increment de la vostra base de dades.

L'única veritat

Independentment de la tecnologia que utilitzeu, sempre hi ha un model que és l'única font de veritat per a algun subsistema, o, com a mínim, hauríem d'esforçar-nos per aconseguir-ho i evitar aquesta confusió empresarial, on la "veritat" està a tot arreu i enlloc alhora. . Tot podria ser molt més senzill. Si només esteu intercanviant fitxers XML amb algun altre sistema, feu servir XSD. Mireu el metamodel INFORMATION_SCHEMA de jOOQ en format XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD s'entén bé
  • XSD tokenitza molt bé el contingut XML i permet la validació en tots els idiomes del client
  • XSD està ben versionat i té una compatibilitat avançada enrere
  • XSD es pot traduir al codi Java mitjançant XJC

L'últim punt és important. Quan ens comuniquem amb un sistema extern mitjançant missatges XML, volem estar segurs que els nostres missatges són vàlids. Això és molt fàcil d'aconseguir amb JAXB, XJC i XSD. Seria una bogeria pensar que, amb un enfocament de disseny "Java primer" on fem els nostres missatges com a objectes Java, d'alguna manera es podrien mapejar de manera coherent a XML i enviar-los a un altre sistema per al seu consum. L'XML generat d'aquesta manera seria de molt mala qualitat, no documentat i difícil de desenvolupar. Si hi hagués un acord de nivell de servei (SLA) per a una interfície d'aquest tipus, ho faríem malament immediatament.

Sincerament, això és el que passa tot el temps amb les API JSON, però això és una altra història, em barallaré la propera vegada...

Bases de dades: són el mateix

Quan es treballa amb bases de dades, t'adones que totes són bàsicament semblants. La base és propietària de les seves dades i ha de gestionar l'esquema. Qualsevol modificació feta a l'esquema s'ha d'implementar directament al DDL perquè s'actualitzi la font única de la veritat.

Quan s'ha produït una actualització d'origen, tots els clients també han d'actualitzar les seves còpies del model. Alguns clients es poden escriure en Java utilitzant jOOQ i Hibernate o JDBC (o tots dos). Altres clients es poden escriure en Perl (només els desitgem molta sort), mentre que altres es poden escriure en C#. No importa. El model principal es troba a la base de dades. Els models generats mitjançant ORM solen ser de mala qualitat, poc documentats i difícils de desenvolupar.

Així que no us equivoqueu. No cometre errors des del principi. Treballar des de la base de dades. Creeu una canalització de desplegament que es pugui automatitzar. Habiliteu els generadors de codi per facilitar la còpia del vostre model de base de dades i abocar-lo als clients. I deixeu de preocupar-vos pels generadors de codi. Estan bé. Amb ells seràs més productiu. Només heu de dedicar una mica de temps a configurar-los des del principi, i després us esperen anys d'augment de la productivitat, que marcaran la història del vostre projecte.

No m'ho agraïs encara, més tard.

Explicació

Per ser clar: aquest article de cap manera defensa que necessiteu doblegar tot el sistema (és a dir, domini, lògica empresarial, etc., etc.) per adaptar-se al vostre model de base de dades. El que dic en aquest article és que el codi de client que interactua amb la base de dades hauria d'actuar sobre la base del model de base de dades, de manera que ell mateix no reprodueixi el model de base de dades en un estat de "primera classe". Aquesta lògica normalment es troba a la capa d'accés a les dades del vostre client.

En arquitectures de dos nivells, que encara es conserven en alguns llocs, aquest model de sistema pot ser l'únic possible. Tanmateix, a la majoria de sistemes, la capa d'accés a les dades em sembla un "subsistema" que encapsula el model de base de dades.

Excepcions

Hi ha excepcions a totes les regles, i ja he dit que l'enfocament de la base de dades i la generació de codi font de vegades poden ser inadequats. Aquí hi ha un parell d'excepcions (probablement n'hi ha d'altres):

  • Quan l'esquema és desconegut i cal descobrir-lo. Per exemple, sou proveïdor d'una eina que ajuda els usuaris a navegar per qualsevol diagrama. Uf. No hi ha cap generació de codi aquí. Tot i així, la base de dades és el primer.
  • Quan s'ha de generar un circuit sobre la marxa per resoldre algun problema. Aquest exemple sembla una versió una mica fantasiosa del patró valor de l'atribut de l'entitat, és a dir, realment no teniu un esquema clarament definit. En aquest cas, sovint ni tan sols podeu estar segur que un RDBMS us convingui.

Les excepcions són excepcionals per naturalesa. En la majoria dels casos que impliquen l'ús d'un RDBMS, l'esquema es coneix per endavant, resideix dins del RDBMS i és l'única font de "veritat", i tots els clients han d'adquirir còpies derivades d'aquest. L'ideal és que utilitzeu un generador de codi.

Font: www.habr.com

Afegeix comentari