Sanningen först, eller varför ett system behöver designas utifrån databasdesignen

Hej Habr!

Vi fortsätter att forska i ämnet java и Vår, inklusive på databasnivå. Idag inbjuder vi dig att läsa om varför, när du designar stora applikationer, det är databasstrukturen, och inte Java-koden, som ska vara av avgörande betydelse, hur detta görs och vilka undantag det finns från denna regel.

I den här ganska sena artikeln kommer jag att förklara varför jag tror att i nästan alla fall bör datamodellen i en applikation utformas "från databasen" snarare än "från Javas kapacitet" (eller vilket klientspråk du än är arbetar med). Genom att ta det andra tillvägagångssättet, ställer du upp för en lång väg av smärta och lidande när ditt projekt börjar växa.

Artikeln skrevs utifrån en fråga, ges på Stack Overflow.

Intressanta diskussioner om reddit i avsnitt /r/java и / r / programmering.

Kodgenerering

Hur förvånad jag blev över att det finns ett så litet segment av användare som, efter att ha bekantat sig med jOOQ, är upprörda över det faktum att jOOQ på allvar förlitar sig på generering av källkod för att fungera. Ingen hindrar dig från att använda jOOQ som du tycker är lämplig, eller tvingar dig att använda kodgenerering. Men standardsättet (som beskrivs i manualen) att arbeta med jOOQ är att du börjar med ett (legacy) databasschema, bakåtkonstruerar det med hjälp av jOOQ-kodgeneratorn för att därigenom få en uppsättning klasser som representerar dina tabeller, och sedan skriver typ -säkra frågor till dessa tabeller:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на 
//   основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ сгенерированные имена
       .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 omedelbart efter Flyway-databasmigrering, som också kan göras manuellt eller automatiskt.

Generering av källkod

Det finns olika filosofier, fördelar och nackdelar förknippade med dessa metoder för kodgenerering - manuell och automatisk - som jag inte kommer att diskutera i detalj i den här artikeln. Men generellt sett är hela poängen med den genererade koden att den låter oss reproducera i Java den "sanning" som vi tar för given, antingen inom vårt system eller utanför det. På sätt och vis är detta vad kompilatorer gör när de genererar bytekod, maskinkod eller någon annan form av källkod - vi får en representation av vår "sanning" på ett annat språk, oavsett de specifika orsakerna.

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 en specifikation, en datamodell, etc.
  • Vi behöver en lokal representation av denna sanning i vårt programmeringsspråk.

Dessutom är det nästan alltid tillrådligt att generera en sådan representation för att undvika redundans.

Typleverantörer och anteckningsbearbetning

Notera: ett annat, mer modernt och specifikt tillvägagångssätt för att generera kod för jOOQ är att använda typleverantörer, eftersom de är implementerade i F#. I det här fallet genereras koden av kompilatorn, faktiskt i kompileringsstadiet. I princip finns inte sådan kod i källform. Java har liknande, men inte lika eleganta, verktyg - anteckningsprocessorer, till exempel, Lombok.

På sätt och vis händer samma saker här som i det första fallet, med undantag av:

  • Du ser inte den genererade koden (kanske den här situationen verkar mindre motbjudande för någon?)
  • Du måste se till att typer kan tillhandahållas, det vill säga "true" måste alltid finnas tillgängliga. Detta är lätt i fallet med Lombok, som kommenterar "sanning". Det är lite mer komplicerat med databasmodeller som är beroende av en ständigt tillgänglig liveuppkoppling.

Vad är problemet med kodgenerering?

Utöver den kluriga frågan om hur man bäst kör kodgenerering – manuellt eller automatiskt, måste vi också nämna att det finns personer som anser att kodgenerering inte alls behövs. Motiveringen till denna synpunkt, som jag stött på oftast, är att det då är svårt att sätta upp en byggledning. Ja, det är verkligen svårt. Ytterligare infrastrukturkostnader uppstår. Om du precis har börjat med en viss produkt (oavsett om det är jOOQ, eller JAXB, eller Hibernate, etc.), tar det tid att sätta upp en produktionsmiljö som du hellre lägger ner på att lära dig själva API:et så att du kan extrahera värde från det .

Om kostnaderna för att förstå generatorns struktur är för höga, så gjorde API:et ett dåligt jobb med användbarheten av kodgeneratorn (och senare visar det sig att användaranpassning i den också är svårt). Användbarhet bör vara högsta prioritet för alla sådana API. Men detta är bara ett argument mot kodgenerering. Annars är det helt manuellt att skriva en lokal representation av inre eller yttre sanning.

Många kommer att säga att de inte har tid att göra allt detta. De har slut på deadlines för sin superprodukt. Någon gång ska vi göra i ordning monteringstransportörerna, vi hinner. Jag ska svara på dem:

Sanningen först, eller varför ett system behöver designas utifrån databasdesignen
Original, Alan O'Rourke, Publikstack

Men i Hibernate/JPA är det så enkelt att skriva Java-kod.

Verkligen. För Hibernate och dess användare är detta både en välsignelse och en förbannelse. I Hibernate kan du helt enkelt skriva ett par enheter, så här:

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

Och nästan allt är klart. Nu är det upp till Hibernate att generera de komplexa "detaljerna" om hur exakt denna entitet kommer att definieras i DDL för din SQL "dialekt":

	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 börja köra programmet. Ett riktigt häftigt tillfälle att snabbt komma igång och prova olika saker.

Men tillåt mig. Jag ljög.

  • Kommer Hibernate verkligen att genomdriva definitionen av denna namngivna primärnyckel?
  • Kommer Hibernate att skapa ett index i TITLE? – Jag vet med säkerhet att vi kommer att behöva det.
  • Kommer Hibernate att göra denna nyckelidentifiering exakt i identitetsspecifikationen?

Antagligen inte. Om du utvecklar ditt projekt från grunden är det alltid bekvämt att helt enkelt kassera den gamla databasen och skapa en ny så fort du lägger till de nödvändiga kommentarerna. Således kommer bokentiteten i slutändan att ta formen:

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

Häftigt. Regenerera. Återigen, i det här fallet kommer det att vara väldigt enkelt i början.

Men du får betala för det senare

Förr eller senare måste du gå i produktion. Det är då denna modell kommer att sluta fungera. Därför att:

I produktionen kommer det inte längre att vara möjligt att vid behov kassera den gamla databasen och börja om från början. Din databas kommer att bli äldre.

Från och med nu och för alltid kommer du att behöva skriva DDL-migreringsskript, till exempel med Flyway. Vad kommer att hända med dina enheter i det här fallet? Du kan antingen anpassa dem manuellt (och därmed dubbla din arbetsbelastning), eller så kan du säga till Hibernate att återskapa dem åt dig (hur sannolikt är det att de som genereras på detta sätt uppfyller dina förväntningar?) Hur som helst, du förlorar.

Så när du väl kommer i produktion kommer du att behöva hot patches. Och de måste sättas i produktion väldigt snabbt. Eftersom du inte förberedde och inte organiserade en smidig pipeline av dina migrationer för produktion, lappar du vilt till allt. Och då hinner man inte längre göra allt korrekt. Och du kritiserar Hibernate, för det är alltid någon annans fel, bara inte du...

Istället kunde saker ha gjorts helt annorlunda från första början. Sätt till exempel runda hjul på en cykel.

Databas först

Den verkliga "sanningen" i ditt databasschema och "suveräniteten" över det ligger inom databasen. Schemat definieras endast i själva databasen och ingen annanstans, och varje klient har en kopia av detta schema, så det är helt vettigt att upprätthålla efterlevnad av schemat och dess integritet, att göra det rätt i databasen - där informationen finns lagrad.
Detta är gammal, till och med hackad visdom. Primära och unika nycklar är bra. Främmande nycklar är bra. Att kontrollera restriktioner är bra. Uttalanden - Bra.

Dessutom är det inte allt. Om du till exempel använder Oracle skulle du förmodligen vilja ange:

  • Vilken bordsyta är ditt bord i?
  • Vad är dess PCTFREE-värde?
  • Vad är cachestorleken i din sekvens (bakom id)

Detta kanske inte är viktigt i små system, men du behöver inte vänta tills du går in i big data-sfären – du kan börja dra nytta av leverantörsförsedda lagringsoptimeringar som de som nämns ovan mycket tidigare. Ingen av de ORM:er som jag har sett (inklusive jOOQ) ger tillgång till hela uppsättningen av DDL-alternativ som du kanske vill använda i din databas. ORM erbjuder några verktyg som hjälper dig att skriva DDL.

Men i slutet av dagen är en väldesignad krets handskriven i DDL. Alla genererade DDL är bara en approximation av den.

Hur är det med klientmodellen?

Som nämnts ovan behöver du på klienten en kopia av ditt databasschema, klientvyn. Onödigt att nämna måste denna klientvy vara synkroniserad med den faktiska modellen. Vad är det bästa sättet att uppnå detta? Använda en kodgenerator.

Alla databaser tillhandahåller sin metainformation via SQL. Så här får du alla tabeller från din databas i 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 du också måste ta hänsyn till vyer, materialiserade vyer, tabellvärderade funktioner) exekveras också genom att anropa DatabaseMetaData.getTables() från JDBC, eller med hjälp av jOOQ-metamodulen.

Utifrån resultaten av sådana frågor är det relativt enkelt att generera vilken representation som helst på klientsidan av din databasmodell, 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 skapa entiteterna själva
  • Om du använder jOOQ kan du skapa jOOQ-metamodellen

Beroende på hur mycket funktionalitet som erbjuds av ditt klient-API (t.ex. jOOQ eller JPA), kan den genererade metamodellen vara riktigt rik och komplett. Ta till exempel möjligheten till implicita kopplingar, introducerad i jOOQ 3.11, som förlitar sig på genererad metainformation om de främmande nyckelrelationerna som finns mellan dina tabeller.

Nu kommer varje databasökning automatiskt att uppdatera klientkoden. Föreställ dig till exempel:

ALTER TABLE book RENAME COLUMN title TO book_title;

Skulle du verkligen vilja göra det här jobbet två gånger? Inte i något fall. Beslut helt enkelt DDL, kör den genom din byggpipeline och få den uppdaterade enheten:

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

Eller den uppdaterade jOOQ-klassen. De flesta DDL-förändringar påverkar också semantiken, inte bara syntax. Därför kan det vara användbart att titta i den kompilerade koden för att se vilken kod som kommer (eller kan) påverkas av din databasökning.

Den enda sanningen

Oavsett vilken teknik du använder finns det alltid en modell som är den enda källan till sanning för något delsystem - eller åtminstone bör vi sträva efter detta och undvika sådan företagsförvirring, där "sanningen" finns överallt och ingenstans på en gång . Allt kunde vara mycket enklare. Om du bara byter XML-filer med något annat system, använd bara XSD. Titta på INFORMATION_SCHEMA-metamodellen från jOOQ i XML-form:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD är väl förstått
  • XSD tokeniserar XML-innehåll mycket väl och tillåter validering på alla klientspråk
  • XSD är välversionerad och har avancerad bakåtkompatibilitet
  • XSD kan översättas till Java-kod med 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 är mycket lätt att uppnå med JAXB, XJC och XSD. Det vore rent galenskap att tro att, med en "Java first"-designansats där vi gör våra meddelanden som Java-objekt, att de på något sätt skulle kunna mappas sammanhängande till XML och skickas till ett annat system för konsumtion. XML genererad på detta sätt skulle vara av mycket dålig kvalitet, odokumenterad och svår att utveckla. Om det fanns ett servicenivåavtal (SLA) för ett sådant gränssnitt skulle vi genast skruva ihop det.

Ärligt talat, detta är vad som händer hela tiden med JSON API:er, men det är en annan historia, jag kommer att gräla nästa gång...

Databaser: de är samma sak

När du arbetar med databaser inser du att de alla i grunden är lika. Basen äger sina data och måste hantera schemat. Alla ändringar som görs i schemat måste implementeras direkt i DDL så att den enda sanningskällan uppdateras.

När en källuppdatering har skett måste alla klienter också uppdatera sina kopior av modellen. Vissa klienter kan skrivas i Java med hjälp av jOOQ och Hibernate eller JDBC (eller båda). Andra klienter kan skrivas i Perl (vi önskar dem bara lycka till), medan andra kan skrivas i 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 inte misstag från första början. Arbeta från databasen. Bygg en distributionspipeline som kan automatiseras. Aktivera kodgeneratorer för att göra det enkelt att kopiera din databasmodell och dumpa den på klienter. Och sluta oroa dig för kodgeneratorer. De är bra. Med dem blir du mer produktiv. Du behöver bara lägga lite tid på att sätta upp dem från första början - och sedan väntar år av ökad produktivitet på dig, vilket kommer att utgöra historien om ditt projekt.

Tacka mig inte än, senare.

klargörande

För att vara tydlig: Den här artikeln förespråkar inte på något sätt att du behöver böja hela systemet (d.v.s. domän, affärslogik, etc., etc.) för att passa din databasmodell. Det jag säger i den här artikeln är att klientkoden som interagerar med databasen ska agera utifrån databasmodellen, så att den själv inte återger databasmodellen i en "förstklassig" status. Denna logik finns vanligtvis vid dataåtkomstlagret på din klient.

I tvånivåarkitekturer, som fortfarande finns bevarade på vissa ställen, kan en sådan systemmodell vara den enda möjliga. Men i de flesta system verkar dataåtkomstlagret för mig vara ett "undersystem" som kapslar in databasmodellen.

undantag

Det finns undantag från varje regel, och jag har redan sagt att tillvägagångssättet för databas-först och källkodsgenerering ibland kan vara olämpligt. Här är ett par sådana undantag (det finns förmodligen andra):

  • När schemat är okänt och måste upptäckas. Du är till exempel en leverantör av ett verktyg som hjälper användare att navigera i alla diagram. Usch. Det finns ingen kodgenerering här. Men ändå, databasen kommer först.
  • När en krets måste genereras i farten för att lösa något problem. Det här exemplet verkar vara en lite fantasifull version av mönstret enhetsattributvärde, d.v.s. du har inte riktigt ett klart definierat schema. I det här fallet kan du ofta inte ens vara säker på att ett RDBMS kommer att passa dig.

Undantag är till sin natur exceptionella. I de flesta fall som involverar användningen av en RDBMS är schemat känt i förväg, det finns inom RDBMS och är den enda källan till "sanning", och alla klienter måste skaffa kopior som härrör från det. Helst måste du använda en kodgenerator.

Källa: will.com

Lägg en kommentar