Az igazság mindennél fontosabb, vagyis miért kell a rendszert az adatbázis struktúrája alapján tervezni

Helló, Habr!

Folyamatosan kutatjuk a témát Java és Spring, beleértve az adatbázisok szintjét is. Ma arról olvashat, hogy miért kell nagy alkalmazások tervezésekor az adatbázis struktúrájának, nem pedig a Java kódnak a döntő szerepet játszania, hogyan történik ez, és milyen kivételek vannak ez alól a szabály alól.

Ebben a viszonylag késlekedő cikkben elmagyarázom, miért gondolom, hogy szinte minden esetben az alkalmazás adatmodelljét az "adatbázisból kiindulva" kell tervezni, nem pedig a "Java lehetőségeiből kiindulva" (vagy bármely más kliens nyelvből, amellyel dolgozik). Ha a második megközelítést választja, egy fájdalmas és szenvedéssel teli úton találja magát, ahogy a projektje növekszik.

A cikk az egy kérdés alapján készült, amelyet a Stack Overflow-n tettek fel.

Érdekes beszélgetések a reddit különböző szekcióiban /r/java és /r/programming.

Kódgenerálás

Hát mennyire meglepett, hogy van egy ilyen kis réteg felhasználó, akik a jOOQ-al való megismerkedés után felháborodnak azon a ténnyel, hogy a jOOQ használata során komolyan támaszkodnak a forráskód generálására. Senki sem akadályoz meg abban, hogy a jOOQ-ot úgy használja, ahogy jónak véli, és senki sem kényszeríti a kód generálására. De alapértelmezés szerint (ahogyan a kézikönyvben le van írva) a jOOQ-al való munka úgy történik: Ön kezd az (örökölt) adatbázis sémájával, visszatervezést végez a jOOQ kódgenerátor segítségével, hogy egy osztályhalmazt kapjon, amely a táblázatait képviseli, majd típusbiztonságos lekérdezéseket ír ezekhez a táblázatokhoz:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Az információk a generált kód alapján kerültek kiírásra, amelyre az alábbi
//   SELECT feltétel hivatkozik

       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ generált nevek
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

A kód vagy manuálisan, az építésen kívül, vagy manuálisan, minden építéskor generálódik. Például egy ilyen regenerálás követheti az Flyway adatbázis-migrációt, amely szintén elvégezhető manuálisan vagy automatikusan.

Forráskód generálás

A kódgenerálás ilyen megközelítései – manuális és automatikus – különböző filozófiákkal, előnyökkel és hátrányokkal kapcsolatosak, amelyeket nem tervezek részletesen megvitatni ebben a cikkben. De összességében a generált kód lényege, hogy lehetővé teszi számunkra, hogy reprodukáljuk Java-ban azt az "igazságot", amelyet adott magunknak, legyen szó a rendszerünk keretein belülről vagy kívülről. Bizonyos értelemben ezt teszik a fordítók is, amelyek bájt-kódot, gépi kódot vagy bármilyen más kódot generálnak a forráskód alapján – egy másik nyelven kapjuk meg az "igazságunk" reprezentációját, függetlenül a konkrét okoktól.

Számos ilyen kódgeneráló létezik. Például, az XJC Java kódot generálhat XSD vagy WSDL fájlok alapján. Az elv mindig ugyanaz:

  • Létezik egyfajta igazság (belső vagy külső) – például specifikáció, adatmodell, stb.
  • Szükségünk van a nyelvünkben való helyi reprezentációra.

És a hasonló reprezentáció generálása szinte mindig célszerű – hogy elkerüljük a redundanciát.

Típuskezdeményezők és annotációfeldolgozás

Érdemes megjegyezni: egy másik, modernebb és specifikusabb megközelítés a jOOQ kódgenerálásához a típuskészítők használatával kapcsolatos, ahogyan az az F#-ban van megvalósítva. Ebben az esetben a kódot a fordító generálja, lényegében a fordítási fázisban. Olyan formában, mint forráskód, az ilyen kód alapvetően nem létezik. A Java-ban léteznek hasonló, bár nem annyira elegáns eszközök – ezek az annotációfeldolgozók, például a Lombok.

. Bizonyos értelemben itt is ugyanazok a dolgok történnek, mint az első esetben, kivéve:

  • Nem látod a generált kódot (talán számít valakinek, hogy ez nem tűnik annyira taszítónak?)
  • Garantálnod kell, hogy a típusok rendelkezésre állnak, tehát az "igazságnak" mindig elérhetőnek kell lennie. Ez könnyű a Lombok esetében, amely annotálja az "igazságot". Egy kicsit bonyolultabb a szabályzatokkal, amelyeket a folyamatosan elérhető élő kapcsolat függvényében dolgozunk fel.

Mi a probléma a kódgenerálással?

A kódgenerálás elindításának legjobb módjáról szóló ügyes kérdés mellett említenünk kell azt is, hogy vannak, akik úgy vélik, hogy a kódgenerálásra egyáltalán nincs szükség. Ennek a véleménynek a leggyakoribb indoka az, hogy nehéz beállítani az építési folyamatot. Igen, valóban nehéz. További infrastruktúra-kiadások merülnek fel. Ha éppen csak most kezd dolgozni egy adott termékkel (legyen az jOOQ, JAXB vagy Hibernate stb.), az ideális munkakörnyezet beállítása időt igényel, amelyet jobban szeretne az API sajátosságaival való ismerkedésre fordítani, hogy később értéket nyerhessen le belőle.

Ha túl nagyok a költségek a generátor működésének megértése miatt, akkor valóban rosszul dolgoztak az API kódgeneráló használhatóságán (és később kiderül, hogy a felhasználói beállítások is bonyolultak). A használhatóság a legmagasabb prioritás kell hogy legyen bármilyen ilyen API esetében. De ez csupán egy érv a kódgenerálás ellen. A többi esetben teljes mértékben kézzel kell elkészíteni a helyi reprezentációt a belső vagy külső igazságról.

Sokan azt mondják, hogy nincs idejük mindezzel foglalkozni. Most éppen a Super-Projektjük határidejei tűzben állnak. Majd később rendbe rakjuk az építési folyamatokat, ha időnk engedi. Én pedig azt válaszolnám nekik:

Az igazság mindennél fontosabb, vagyis miért kell a rendszert az adatbázis struktúrája alapján tervezni
Eredeti, Alan O’Rourke, Audience Stack

De a Hibernate / JPA esetében annyira egyszerűt írni "Java-ra".

Valóban. A Hibernate és felhasználói számára ez egyszerre áldás és átok. A Hibernate-ben egyszerűen megírhat pár entitást, ahogy a következőképpen:

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

És majdnem minden kész is. Most a Hibernate feladata az, hogy generálja a bonyolult "részleteket", hogy ez az entitás hogyan lesz definiálva az SQL "dialektusának" DDL-jén:

	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);

… és elkezdjük futtatni az alkalmazást. Valóban nagyszerű lehetőség, hogy gyorsan munkához lássunk és különböző dolgokat próbáljunk ki.

De engedje meg, hogy pontosítsak. Csalót játszottam.

  • De a Hibernate valóban alkalmazni fogja ennek a névvel ellátott elsődleges kulcsnak a definícióját?
  • A Hibernate készít indexet a TITLE esetében? – biztos vagyok benne, hogy szükségünk lesz rá.
  • A Hibernate valóban ezt a kulcsot fogja az Identity Specification azonosítójaként létrehozni?

Valószínűleg nem. Ha a projektedet a nulláról fejleszted, mindig kényelmes egyszerűen kidobni a régi adatbázist, és újat generálni, amint hozzáadtad a szükséges annotációkat. Így a Book entitás végül a következőképpen fog kinézni:

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

Menő. Újragenerálni. Ismét mondom, hogy ilyen esetben az elején nagyon könnyű lesz.

De később ezért fizetni kell.

Előbb vagy utóbb el kell menned a termelésbe. Pont ekkor fog a modell megállni. Mert:

A termelésben már nem lehet szükség esetén kidobni a régi adatbázist, és mindent tiszta lappal kezdeni. Az adatbázisod örökölté válik.

Mostantól örökre írnod kell DDL migrációs szkripteket, például Flyway segítségével.. És mi fog történni a te entitásaiddal? Vagy kézzel kell adaptálnod őket (és ezzel megduplázod a munkádat), vagy megparancsolod a Hibernate-nek, hogy újra generálja őket számodra (mennyire valószínű, hogy az így generált megfelel az elvárásaidnak?) Mindkét esetben veszítesz.

Így hát, amint átérsz a termelésbe, szükséged lesz forró javításokra. Azokat nagyon gyorsan kell bevezetned a termelésbe. Mivel nem készültél fel, és nem szervezted meg simán a migrációidat a termeléshez, mindent brutálisan javítani fogsz. Aztán már nem lesz időd mindent jól csinálni. És hibáztatni fogod a Hibernate-et, mert mindig bárki más a hibás, csak te nem…

Ehelyett, már az elején mindent teljesen másképp lehetett volna csinálni. Például kerek kerekeket tenni a biciklidre.

Először az adatbázis

Az adatbázisod sémájának valódi "igazsága" és a felette való "szuverenitás" magában az adatbázisban rejlik. A séma csak magában az adatbázisban van meghatározva, és sehol máshol, és minden ügyfélnek van egy példánya erről a sémáról, ezért teljesen ésszerű a séma és annak épsége betartatásának kikényszerítése – ott, ahol az információ tárolva van.
Ez egy régi, sőt kopott bölcsesség. Az elsődleges és egyedi kulcsok jók. A külső kulcsok jók. A megszorítások ellenőrzése jó. Állítások – jók.

Ráadásul ez még nem minden. Például, ha Oracle-t használ, valószínűleg meg szeretné adni:

  • Melyik táblatérben található a táblája
  • Mi a PCTFREE értéke
  • Mi a mérete a sorozatának (azonosító után)

Lehet, hogy mindez nem fontos kis rendszerekben, de nem szükséges megvárni, amíg elérjük a 'nagy adatok' területét – már korábban is hasznot húzhatunk az adatkezelési optimalizációkból, amelyeket a szolgáltató nyújt, mint például a fent említettek. Egyetlen ORM sem biztosítja a teljes DDL opciók elérését, amelyeket esetleg szeretne használni az adatbázisában. Az ORM-ek néhány eszközt kínálnak, amelyek segítenek a DDL írásában.

De végső soron egy jól megtervezett séma csak DDL-ben van megírva. Bármely generált DDL csak egy megközelítés.

Mi a helyzet a kliensmodellel?

Ahogy fent említettem, a kliensnek szüksége van egy példányra az adatbázis sémájáról, kliensnézetre. Felesleges említeni, hogy ennek a kliensnézetnek szinkronban kell lennie a valós modellel. Hogyan lehet ezt a legjobban elérni? Kódgenerátorral.

Minden adatbázis SQL-en keresztül nyújtja a meta-információit. Így érhetjük el az adatbázisából az összes táblát különböző SQL dialektusokon:

	-- 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

Ezeket a lekérdezéseket (vagy hasonlókat, attól függően, hogy figyelembe kell-e venni a nézeteket, anyagilag megvalósított nézeteket, táblázatos értékű funkciókat) szintén végrehajtják a hívás segítségével DatabaseMetaData.getTables() JDBC-ből, vagy a jOOQ meta-modulján keresztül.

Ezekből a lekérdezésekből viszonylag könnyen generálhatunk bármilyen kliensszemléletet az adatbázis modelljéről, függetlenül attól, hogy milyen technológia van használatban a klienseken.

  • Ha JDBC-t vagy Springet használ, létrehozhat egy sor string konstansot
  • Ha JPA-t használ, a saját entitásait generálhatja
  • Ha jOOQ-ot használ, akkor a jOOQ meta-modelt generálhat.

Attól függően, hogy milyen mértékű lehetőségeket kínál a kliens API-ja (pl. jOOQ vagy JPA), a generált meta-modell valóban gazdag és teljes lehet. Vegyük például a nem explicit összekapcsolás lehetőségét, amely a jOOQ 3.11-ben jelent meg,, és amely a táblái közötti külső kulcsok kapcsolatairól származó meta-információra épít.

Mostantól bármilyen adatbázis-módosítás automatikusan frissíti a kliens kódot. Képzelje el például:

ALTER TABLE book RENAME COLUMN title TO book_title;

Valóban kétszer szeretné ezt a munkát elvégezni? Semmiképpen. Csak rögzítjük a DDL-t, átfuttatjuk a fejlesztési folyamatán, és megkapjuk a frissített entitást:

@Entity
@Table(name = "book", indexes = {
 
  // Gondolt már erre?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Vagy a frissített jOOQ osztály. A DDL legtöbb módosítása a szemantikában is tükröződik, nemcsak a szintaxisban. Ezért hasznos lehet megnézni a lefordított kódban, hogy melyik kódot érintheti az adatbázisának bővítése.

Az egyetlen igazság

Függetlenül attól, hogy milyen technológiát használ, mindig van egy modell, amely a bizonyos alrendszer egyetlen igaz forrása – vagy legalábbis ezt kellene céloznunk, és elkerülni azt az enterprise zűrzavart, ahol az 'igazság' egyszerre van mindenhol és sehol. Minden sokkal egyszerűbb lehet. Ha csak XML fájlokat cserélget más rendszerekkel, egyszerűen használja az XSD-t. Nézze meg a jOOQ INFORMATION_SCHEMA meta-modelljét XML formában:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • Az XSD jól érthető
  • Az XSD nagyon jól jelöli az XML tartalmát, és lehetővé teszi a validálást minden kliensnyelven
  • Az XSD jól verzionálható, és kiterjedt visszafelé kompatibilitással bír
  • Az XSD Java kódra is lefordítható az XJC segítségével

Az utolsó pont fontos. Amikor külső rendszerrel kommunikálunk XML üzenetek segítségével, biztosak szeretnénk lenni üzeneteink érvényességében. Ezt nagyon könnyen elérhetjük JAXB, XJC és XSD használatával. Teljes őrültség lenne azt gondolni, hogy a „Java első” tervezési megközelítés mellett, ahol üzeneteinket Java objektumok formájában készítjük, valahogy világosan át tudnánk térni XML-re és elküldeni egy másik rendszer számára. Az így generált XML nagyon gyenge minőségű lenne, nem dokumentált, és nehezen fejleszthető. Ha ilyen interfészen SLA (szolgáltatási szint megállapodás) létezne, azonnal elhibáznánk.

Őszintén szólva, pontosan ez történik folyamatosan a JSON API-kkal, de ez már egy másik történet, legközelebb majd megint panaszkodom…

Adatbázisok: ugyanez

Adatbázisokkal dolgozva megérti, hogy mindegyik alapvetően hasonló. Az adatbázis a saját adatai felett rendelkezik, és irányítania kell a sémát. A sémában végrehajtott minden módosítást közvetlenül a DDL-ben kell végrehajtani, hogy frissüljön az egyetlen igazságforrás.

Amikor az igazságforrás frissítése megtörtént, minden kliensnek is frissítenie kell a modelljeinek másolatát. Néhány kliens Java nyelven készült jOOQ és Hibernate vagy JDBC (vagy mindegyik) használatával. Más kliensek Perl nyelven készültek (sok szerencsét kívánok nekik), míg harmadik felek C# nyelven íródtak. Ez nem számít. A fő modell az adatbázisban található. Az ORM-el generált modellek általában gyenge minőségűek, rosszul dokumentáltak, és nehezen fejleszthetők.

Ezért ne kövessen el hibákat. Kezdettől fogva kerüljön el hibákat. Dolgozzon az adatbázisból. Építsen egy olyan telepítési folyamatot, amelyet automatizálni lehet. Tartalmazza a kódgenerátorokat, hogy kényelmesen tudja másolni az adatbázis modelljét és átadni a klienseknek. És fejezze be a kódgenerátorok miatti aggodalmaskodást. Jó dolgok. Ezekkel termelékenyebbé válik. Csak egy kis időt kell szánnia a kezdeti beállításra – és a továbbiakban éveken át fokozott termelékenység vár Önre, amelyből a projektje története áll majd össze.

Még ne köszönjön, később.

Magyarázat

A tisztázás kedvéért: ez a cikk semmiképpen sem népszerűsíti azt, hogy az adatbázis modellje köré kellene építeni az egész rendszert (azaz a tárgykört, az üzleti logikát stb.). Ebben a cikkben arról beszélek, hogy az adatbázissal interakcióban lévő klienskódnak az adatbázis modelljéből kell kiindulnia, úgy, hogy benne ne jelenjen meg az adatbázis modellje „első osztály” státuszban. Az ilyen logika általában a kliens adat-hozzáférési szintjén található.

A kétfokozatú architektúrákban, amelyek még mindig néhol fennmaradtak, az ilyen rendszer modellje lehet az egyetlen lehetséges. Azonban a legtöbb rendszerben az adat-hozzáférési szint nekem „alrendszernek” tűnik, amely beburkolja az adatbázis modellt.

Kivétel

Minden szabálynak vannak kivételei, és már mondtam, hogy a primátusok adatbázis megközelítése és a forráskód generálása néha nem megfelelő lehet. Íme pár ilyen kivétel (valószínűleg lesznek mások is):

  • Amikor a séma ismeretlen, és meg kell nyitni. Például, ha te vagy az eszközszolgáltató, aki segít a felhasználóknak eligibilizálni bármilyen séma szerint. Hűha. Itt már nem lehet generálni a kódot. De azért – az adatbázis mindenekelőtt.
  • Amikor a séma menet közben kell, hogy generálódjon, hogy megoldjon valamilyen feladatot. Ez a példa kissé túlságosan is opulent verziója a mintának entity attribute value, azaz valójában nincs pontosan meghatározott sémád. Ebben az esetben gyakran még azzal sem lehet biztosan élni, hogy a RDBMS neked megfelelő lesz.

A kivételek alapvetően kivételesek. A RDBMS használatával kapcsolatos legtöbb esetben a séma előre ismert, a RDBMS-en belül található, és az egyetlen „igazság” forrása, és minden kliensnek másolatot kell szereznie, amely ebből származik. Ideálisan ebben az esetben kódgenerátort kellene alkalmazni.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster