Sanningen först, eller varför systemet mÄste designas utifrÄn databasens struktur

Hej, Habr!

Vi fortsÀtter att utforska Àmnet Java och Spring, inklusive pÄ databasenivÄ. Idag erbjuder vi att lÀsa om varför databassstruktur, snarare Àn Java-kod, bör ha avgörande betydelse nÀr man designar stora applikationer, hur detta görs och vilka undantag som finns frÄn denna regel.

I den hÀr ganska sena artikeln kommer jag att förklara varför jag anser att data-modellen i en applikation i praktiskt taget alla fall bör designas "utifrÄn databasen", snarare Àn "utifrÄn Java-möjligheterna" (eller nÄgot annat klient-sprÄk som du arbetar med). VÀljer man den andra metoden, dÄ ger man sig in pÄ en lÄng vÀg av smÀrta och lidande, sÄ snart ditt projekt börjar vÀxa.

Artikeln Àr skriven med inspiration frÄn en frÄga, stÀlld pÄ Stack Overflow.

Intressanta diskussioner pÄ reddit i sektionerna /r/java och /r/programming.

Kodgenerering

Jag blev verkligen förvÄnad över att det finns en sÄ liten skara anvÀndare som, efter att ha bekantat sig med jOOQ, blir upprörda över det faktum att jOOQ allvarligt baseras pÄ kodgenerering. Ingen hindrar dig frÄn att anvÀnda jOOQ som du tycker Àr nödvÀndigt, och ingen tvingar dig att anvÀnda kodgenerering. Men som standard (som beskrivs i manualen) fungerar jOOQ sÄ hÀr: du börjar med (Àrvda) databasscheman, utför omvÀnd ingenjörskonst med hjÀlp av jOOQs kodgenerator för att pÄ sÄ sÀtt fÄ en uppsÀttning klasser som representerar dina tabeller, och sedan skriver du typ-sÀkra frÄgor till dessa tabeller:

	for (Record2 record : DSL.using(configuration) 
//   ^^^^^^^^^^^^^^^^^^^^^^^ Typinformation har hÀrletts frÄn 
//   den genererade koden vilket refereras av det 
//   följande SELECT-villkoret 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME) 
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ genererade namn
       .from(ACTOR) 
       .orderBy(1, 2)) { 
    // ...
}

Koden genereras antingen manuellt utanför sammansÀttningen, eller manuellt vid varje sammansÀttning. Till exempel kan sÄdan regenerering följa direkt efter Flyway-databasmigrationen, som ocksÄ kan utföras manuellt eller automatiskt..

Kodgenerering

Med sĂ„dana metoder för kodgenerering – manuella och automatiska – Ă€r knutna olika filosofier, fördelar och nackdelar som jag inte avser att diskutera i detalj i denna artikel. Men i allmĂ€nhet handlar hela essensen av genererad kod om att Ă„terge den 'sanning' vi accepterar som en given, antingen inom vĂ„rt system eller utanför det. PĂ„ ett sĂ€tt gör kompilatorer samma sak, som genererar bytekod, maskinkod eller nĂ„gon annan typ av kod baserat pĂ„ kĂ€llkod – vi fĂ„r en representation av vĂ„r 'sanning' pĂ„ ett annat sprĂ„k, oavsett specifika orsaker.

Det finns mÄnga sÄdana kodgeneratorer. Till exempel, XJC kan generera Java-kod baserat pÄ XSD- eller WSDL-filer.Principen Àr alltid densamma:

  • Det finns en viss sanning (intern eller extern) – till exempel specifikation, datamodell, osv.
  • Vi behöver en lokal representation av denna sanning pĂ„ vĂ„rt programmeringssprĂ„k.

Och det Ă€r nĂ€stan alltid förnuftigt att generera en sĂ„dan representation – för att undvika överflödighet.

Typleverantörer och bearbetning av annoteringar

Observera: en annan, modernare och mer specifik metod för kodgenerering för jOOQ Ă€r kopplad till anvĂ€ndningen av typleverantörer, i den form de implementeras i F#.I sĂ„dana fall genereras koden av kompilatorn, faktiskt vid kompileringstillfĂ€llet. I form av kĂ€llkod existerar sĂ„dan kod inte egentligen. I Java finns liknande, om Ă€n inte sĂ„dĂ€r eleganta verktyg – det Ă€r annoteringsprocessorer, till exempel, Lombok..

PÄ sÀtt och vis hÀnder samma saker hÀr som i det första fallet, med undantag för att:

  • Du ser inte den genererade koden (möjligen verkar denna situation inte sĂ„ avskrĂ€ckande för nĂ„gon?)
  • Du mĂ„ste garantera att typer kan erbjudas, vilket betyder att 'sanningen' alltid mĂ„ste vara tillgĂ€nglig. Detta Ă€r lĂ€tt i fallet med Lombok, som annoterar "sanningen". Lite mer komplicerat Ă€r det med databasmodeller, vars funktioner beror pĂ„ att en stĂ€ndig aktiv anslutning finns tillgĂ€nglig.

Vad Àr problemet med kodgenerering?

Förutom den knepiga frĂ„gan om hur man bĂ€st ska starta kodgenereringen – manuellt eller automatiskt – mĂ„ste man ocksĂ„ nĂ€mna att det finns personer som anser att kodgenerering överhuvudtaget inte behövs. Den mest frekventa motiveringen för denna synpunkt som jag har stött pĂ„ Ă€r att det dĂ„ Ă€r svĂ„rt att konfigurera byggpipeline. Ja, det Ă€r verkligen svĂ„rt. Det uppstĂ„r ytterligare infrastrukturkostnader. Om du just har börjat arbeta med en viss produkt (oavsett om det Ă€r jOOQ, JAXB, Hibernate, etc.), tar det tid att stĂ€lla in arbetsmiljön, en tid som du hellre skulle lĂ€gga pĂ„ att studera API:et och sĂ„ smĂ„ningom dra nytta av det.

Om kostnaderna för att förstĂ„ hur generatorn fungerar Ă€r för höga – dĂ„ har API:et verkligen inte arbetat tillrĂ€ckligt med anvĂ€ndarvĂ€nligheten för kodgeneratorn (och det visar sig senare att Ă€ven anvĂ€ndarinstĂ€llningar Ă€r svĂ„ra). AnvĂ€ndarvĂ€nlighet bör alltid vara högsta prioritet för ett sĂ„dant API. Men det hĂ€r Ă€r bara ett argument mot kodgenerering. I övrigt Ă€r det helt och hĂ„llet nödvĂ€ndigt att skriva en lokal representation av interna eller externa sanningar för hand.

MÄnga kommer att sÀga att de inte har tid för detta. De har strikta deadlines för sin Super-Produkt. Senare fÄr vi uppdatera byggpipelines, det finns tid. Jag responderar:

Sanningen först, eller varför systemet mÄste designas utifrÄn databasens struktur
Original, Alan O'Rourke, Audience Stack

Men i Hibernate / JPA Àr det sÄ enkelt att skriva kod "för Java".

Verkligen. För Hibernate och dess anvÀndare Àr detta bÄde en vÀlsignelse och en förbannelse. I Hibernate kan man enkelt skriva ett par entiteter, sÄ hÀr:

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

Och nĂ€stan allt Ă€r klart. Nu Ă€r det Hibernates uppgift – att generera komplexa "detaljer" om hur just denna entitet kommer att definieras i DDL för din "dialekt" 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);


 och vi börjar köra applikationen. Verkligen fantastisk möjlighet att snabbt komma igĂ„ng och testa olika saker.

Men vÀnta. Jag har överdrivit.

  • Kommer Hibernate verkligen att anvĂ€nda definitionen av denna namngivna primĂ€ra nyckel?
  • Kommer Hibernate att skapa ett index pĂ„ TITLE? – jag vet att vi kommer att behöva det.
  • Kommer Hibernate verkligen att göra denna nyckel identifierande i Identity Specification?

Troligen inte. Om du utvecklar ditt projekt frÄn grunden, Àr det alltid praktiskt att bara slÀppa den gamla databasen och generera en ny sÄ snart du har lagt till nödvÀndiga annoteringar. SÄ, entiteten Book kommer sÄ smÄningom att se ut sÄ hÀr:

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

Coolt. Generera om. Återigen, i sĂ„ fall kommer det att bli mycket enkelt i början.

Men senare kommer du att fÄ betala för det.

Förr eller senare mÄste du gÄ i produktion. Just dÄ kommer denna modell att sluta fungera. För att:

I produktion kan du inte lÀngre kasta bort den gamla databasen och börja om frÄn början. Din databas kommer att bli en arvtagare.

FrÄn och med nu och för alltid mÄste du skriva DDL-migreringsskript, till exempel med Flyway.. Och vad hÀnder i sÄ fall med dina entiteter? Du kan antingen anpassa dem manuellt (och pÄ sÄ sÀtt dubbla din arbetsbelastning), eller be Hibernate generera dem igen Ät dig (hur stora Àr chanserna att det som genereras pÄ detta sÀtt motsvarar dina förvÀntningar?) Du förlorar oavsett.

DĂ€rför, sĂ„ snart du gĂ„r i produktion, behöver du snabba patchar. Och de mĂ„ste slĂ€ppas i produktion mycket snabbt. Eftersom du inte var förberedd och inte organiserade en smidig process för dina migrationer till produktion, kommer du att patcha överallt. Och sedan hinner du inte göra allt korrekt. Och skyller pĂ„ Hibernate, eftersom alltid nĂ„gon annan Ă€r skyldig, utom du


IstÀllet kunde allt ha gjorts helt annorlunda frÄn början. Till exempel, sÀtta pÄ cykeln runda hjul.

Först databasen.

Den verkliga "sanningen" i strukturen för din databas och "suverĂ€niteten" över den finns inne i databasen. Strukturen definieras endast i sjĂ€lva databasen och ingen annanstans, och varje kund har en kopia av denna struktur, sĂ„ det Ă€r helt rimligt att föreskriva överensstĂ€mmelse med strukturen och dess integritet, göra det direkt i databasen – dĂ€r informationen lagras.
Detta Ă€r en gammal och till och med slitstark visdom. PrimĂ€ra och unika nycklar Ă€r bra. Utlandsnycklar – bra. Kontroller av begrĂ€nsningar – bra. PĂ„stĂ„enden – bra.

Dessutom Àr detta inte allt. Om du anvÀnder Oracle, vill du troligen specificera:

  • I vilket tablespace din tabell finns
  • Vad dess PCTFREE-vĂ€rde Ă€r
  • Vad storleken pĂ„ cachen i din sekvens (efter identifieraren) Ă€r

Detta kanske inte betyder sĂ„ mycket i mindre system, men du behöver inte vĂ€nta med att gĂ„ över till "big data"-omrĂ„det — du kan börja dra nytta av de lagringsoptimeringar som leverantören erbjuder mycket tidigare, som de ovan nĂ€mnda. Ingen av de ORM:er jag har sett (inklusive jOOQ) ger Ă„tkomst till hela uppsĂ€ttningen av DDL-alternativ som du kanske vill anvĂ€nda i din databas. ORM:er erbjuder vissa verktyg som hjĂ€lper till att skriva DDL.

Men slutligen Àr en vÀl utformad struktur skriven för hand i DDL. All genererad DDL Àr bara en approximation av den.

Vad hÀnder med klientmodellen?

Som tidigare nÀmnts kommer du att behöva en kopia av din databasstruktur pÄ klienten, ett klientvisning. Det Àr överflödigt att nÀmna att detta klientvisning mÄste synkroniseras med den faktiska modellen. Hur gör man detta pÄ bÀsta sÀtt? Genom en kodgenerator.

Alla databaser tillhandahÄller sin metadata genom SQL. HÀr Àr hur du fÄr alla tabeller frÄn din databas pÄ olika SQL-dialekter:

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

Dessa frÄgor (eller liknande, beroende pÄ om vyer, materialiserade vyer och fuktiga funktioner ocksÄ beaktas) genomförs ocksÄ med ett anrop DatabaseMetaData.getTables() frÄn JDBC, eller med hjÀlp av jOOQ:s meta-modul.

FrÄn resultaten av sÄdana frÄgor Àr det relativt enkelt att generera vilket klientvisning av din databasmodell som helst, oavsett vilken teknik du anvÀnder pÄ klienten.

  • Om du anvĂ€nder JDBC eller Spring, kan du skapa en uppsĂ€ttning strĂ€ngkonstanter
  • Om du anvĂ€nder JPA, kan du generera sjĂ€lva entiteterna
  • Om du anvĂ€nder jOOQ, kan du generera jOOQ:s meta-modell

Beroende pÄ vilken mÀngd funktioner som erbjuds av ditt klient-API (t.ex. jOOQ eller JPA), kan den genererade meta-modellen vara verkligt rik och komplett. LÄt oss ta en titt pÄ möjligheten för implicita sammanfogningar, som tillkom i jOOQ 3.11, vilken bygger pÄ den genererade metainformationen om relationerna mellan dina tabeller.

Nu kommer varje Àndring av databasen automatiskt att leda till en uppdatering av klientkoden. FörestÀll dig till exempel:

ALTER TABLE book RENAME COLUMN title TO book_title;

Vill du verkligen göra det hÀr arbetet tvÄ gÄnger? Absolut inte. Vi registrerar bara DDL, kör det genom din byggpipeline och fÄr den uppdaterade entiteten:

@Entity
@Table(name = "book", indexes = {
 
  // Har du tÀnkt pÄ detta?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Eller den uppdaterade jOOQ-klassen. De flesta DDL-förÀndringar Äterspeglas ocksÄ i semantiken snarare Àn bara i syntaxen. DÀrför kan det vara praktiskt att titta i den kompilerade koden för att se vilken kod som kommer (eller kan) beröras av Àndringen av din databas.

Den enda sanningen

Oavsett vilken teknologi du anvĂ€nder, finns det alltid en modell som Ă€r den enda sanningskĂ€llan för en viss subsystem – eller Ă„tminstone bör vi strĂ€va efter detta och undvika den dĂ€r företagsförvirringen, dĂ€r "sanningen" Ă€r bĂ„de överallt och ingenstans. Allt kan vara mycket enklare. Om du bara utbyter XML-filer med nĂ„got annat system, anvĂ€nd dĂ„ bara XSD. Titta pĂ„ meta-modellen INFORMATION_SCHEMA frĂ„n jOOQ i XML-form:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD Ă€r lĂ€tt att förstĂ„
  • XSD strukturerar XML-innehĂ„llet mycket vĂ€l och möjliggör validering pĂ„ alla klientsprĂ„k
  • XSD versioneras vĂ€l och har bra bakĂ„tkompatibilitet
  • XSD kan översĂ€ttas till Java-kod med hjĂ€lp av XJC

Den sista punkten Àr viktig. NÀr vi kommunicerar med ett externt system med hjÀlp av XML-meddelanden vill vi vara sÀkra pÄ att vÄra meddelanden Àr giltiga. Detta kan lÀtt uppnÄs med JAXB, XJC och XSD. Det skulle vara rent galet att tro att, med en "Java-först"-design dÀr vi skapar vÄra meddelanden som Java-objekt, kan dessa pÄ nÄgot sÀtt tydligt avbildas pÄ XML och skickas för konsumtion i ett annat system. XML som genereras pÄ detta sÀtt skulle vara av mycket dÄlig kvalitet, icke-dokumenterat och svÄrt att underhÄlla. Om det fanns ett avtal om servicenivÄer (SLA) för ett sÄdant grÀnssnitt skulle vi omedelbart ha krossat det.

Ärligt talat, det hĂ€r hĂ€nder stĂ€ndigt med JSON-API:er, men det Ă€r en annan historia, nĂ€sta gĂ„ng ska jag klaga


Databaser: det Àr samma sak

NÀr du arbetar med databaser förstÄr du att de alla, i princip, liknar varandra. En databas Àger sina egna data och ska styra schemat. Eventuella Àndringar som görs i schemat mÄste genomföras direkt pÄ DDL för att uppdatera den enda sanningskÀllan.

NÀr uppdateringen av kÀllan har skett mÄste alla klienter ocksÄ uppdatera sina kopior av modellen. Vissa klienter kan vara skrivna i Java med jOOQ och Hibernate eller JDBC (eller alla pÄ en gÄng). Andra klienter kan vara skrivna i Perl (jag önskar dem lycka till), tredje pÄ C#. Det spelar ingen roll. Huvudmodellen finns i databasen. Modeller som genereras med hjÀlp av ORM Àr vanligtvis av dÄlig kvalitet, dÄligt dokumenterade och svÄra att utveckla.

SĂ„ gör inga misstag. Gör inga misstag frĂ„n början. Arbeta utifrĂ„n databasen. Bygg en distributionspipeline som kan automatiseras. Inkludera kodgeneratorer för att enkelt kopiera modellen av din databas och överföra den till klienterna. Sluta oroa dig för kodgeneratorer. De Ă€r bra. Med dem kommer du att bli mer produktiv. Det krĂ€vs bara lite tid frĂ„n början för att konfigurera dem – och sedan vĂ€ntar Ă„r av ökad produktivitet som kommer att vĂ€va din projekthistoria.

Tacka mig inte Àn, senare.

Förklaring

För tydlighetens skull: Denna artikel propagerar pÄ inget sÀtt för att hela systemet (det vill sÀga ÀmnesomrÄdet, affÀrslogiken, etc.) ska anpassas efter din databasmodell. I denna artikel diskuterar jag hur klientkoden som interagerar med databasen bör fungera utifrÄn databasmodellen, sÄ att sjÀlva klientkoden inte reproducerar databasmodellen som "första klass". Denna logik placeras vanligtvis pÄ datÄtkomstnivÄn hos dig pÄ klienten.

I tvÄnivÄarkitekturer, som fortfarande finns kvar pÄ vissa stÀllen, kan en sÄdan systemmodell vara den enda möjliga. Men i de flesta system verkar datÄtkomstnivÄn för mig som en "undersystem" som inkapslar databasmodellen.

Undantag

Det finns undantag frÄn varje regel, och jag har redan nÀmnt att den primÀra databastillvÀgagÄngssÀttet och koden som genereras ibland kan vara olÀmpliga. HÀr Àr ett par sÄdana undantag (det troligen finns fler):

  • NĂ€r schemat Ă€r okĂ€nt och behöver öppnas. Till exempel, om du Ă€r en leverantör av verktyg som hjĂ€lper anvĂ€ndare att navigera i vilket schema som helst. Puh. HĂ€r behövs ingen kodgenerering. Men oavsett – databasen först.
  • NĂ€r schemat mĂ„ste genereras dynamiskt för att lösa ett specifikt problem. Detta exempel verkar vara en nĂ„got överdriven version av mönstret entity attribute value, det vill sĂ€ga, du har faktiskt ingen definierad modell. I sĂ„dana fall kan det ofta vara svĂ„rt att vara sĂ€ker pĂ„ att en RDBMS passar dina behov.

Undantag Àr per definition undantagsfall. I de flesta fall som involverar anvÀndningen av RDBMS Àr schemat kÀnt pÄ förhand, det finns inuti RDBMS och Àr den enda kÀllan till "sanning", och alla klienter mÄste ha kopior som Àr derivat av det. Idealiskt bör en kodgenerator anvÀndas.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster