Sandheden først, eller hvorfor systemet skal designes baseret på databasestrukturen

Hej Habr!

Vi fortsætter med at forske i emnet Java и Forår, herunder på databaseniveau. I dag inviterer vi dig til at læse om, hvorfor det ved design af store applikationer er databasestrukturen, og ikke Java-koden, der skal være af afgørende betydning, hvordan dette gøres, og hvilke undtagelser der er fra denne regel.

I denne ret sene artikel vil jeg forklare, hvorfor jeg mener, at datamodellen i en applikation i næsten alle tilfælde bør designes "fra databasen" snarere end "fra Javas muligheder" (eller hvilket klientsprog du nu er arbejder med). Ved at tage den anden tilgang, sætter du dig selv op til en lang vej med smerte og lidelse, når dit projekt begynder at vokse.

Artiklen er skrevet ud fra et spørgsmål, givet på Stack Overflow.

Interessante diskussioner om reddit i sektioner /r/java и /r/programmering.

Kodegenerering

Hvor var jeg overrasket over, at der er et så lille segment af brugere, som efter at have stiftet bekendtskab med jOOQ er forargede over, at jOOQ seriøst er afhængig af kildekodegenerering for at fungere. Ingen forhindrer dig i at bruge jOOQ, som du finder passende, eller tvinger dig til at bruge kodegenerering. Men standard (som beskrevet i manualen) måde at arbejde med jOOQ på er, at du starter med et (legacy) databaseskema, reverse engineer det ved hjælp af jOOQ kodegeneratoren for derved at få et sæt klasser, der repræsenterer dine tabeller, og derefter skrive type -sikre forespørgsler til disse tabeller:

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

Koden genereres enten manuelt uden for samlingen eller manuelt ved hver samling. For eksempel kan en sådan regenerering følge umiddelbart efter Flyway-databasemigrering, som også kan gøres manuelt eller automatisk.

Generering af kildekode

Der er forskellige filosofier, fordele og ulemper forbundet med disse tilgange til kodegenerering - manuel og automatisk - som jeg ikke vil diskutere i detaljer i denne artikel. Men generelt er hele pointen med den genererede kode, at den giver os mulighed for i Java at gengive den "sandhed", som vi tager for givet, enten i vores system eller uden for det. Det er i en vis forstand, hvad compilere gør, når de genererer bytekode, maskinkode eller en anden form for kildekode - vi får en repræsentation af vores "sandhed" på et andet sprog, uanset de specifikke årsager.

Der er mange sådanne kodegeneratorer. For eksempel, XJC kan generere Java-kode baseret på XSD- eller WSDL-filer. Princippet er altid det samme:

  • Der er en vis sandhed (intern eller ekstern) - for eksempel en specifikation, en datamodel osv.
  • Vi har brug for en lokal repræsentation af denne sandhed i vores programmeringssprog.

Desuden er det næsten altid tilrådeligt at generere en sådan repræsentation for at undgå redundans.

Typeudbydere og annotationsbehandling

Bemærk: en anden, mere moderne og specifik tilgang til generering af kode til jOOQ er at bruge typeudbydere, som de er implementeret i F#. I dette tilfælde genereres koden af ​​compileren, faktisk på kompileringsstadiet. I princippet findes en sådan kode ikke i kildeform. Java har lignende, men ikke lige så elegante værktøjer - annotationsprocessorer, f.eks. Lombok.

På en måde sker der de samme ting her som i det første tilfælde, med undtagelse af:

  • Du kan ikke se den genererede kode (måske virker denne situation mindre frastødende for nogen?)
  • Du skal sikre dig, at typer kan leveres, det vil sige, at "sand" altid skal være tilgængelig. Dette er nemt i tilfældet med Lombok, som annoterer "sandhed". Det er lidt mere kompliceret med databasemodeller, der er afhængige af en konstant tilgængelig liveforbindelse.

Hvad er problemet med kodegenerering?

Udover det tricky spørgsmål om, hvordan man bedst kører kodegenerering – manuelt eller automatisk, må vi også nævne, at der er folk, der mener, at kodegenerering slet ikke er nødvendig. Begrundelsen for dette synspunkt, som jeg er stødt på oftest, er, at det så er svært at etablere en byggepipeline. Ja, det er virkelig svært. Yderligere infrastrukturomkostninger opstår. Hvis du lige er begyndt med et bestemt produkt (uanset om det er jOOQ, eller JAXB, eller Hibernate osv.), tager opsætningen af ​​et produktionsmiljø tid, som du hellere vil bruge på at lære selve API'en, så du kan udvinde værdi fra den .

Hvis omkostningerne forbundet med at forstå strukturen af ​​generatoren er for høje, så gjorde API'en faktisk et dårligt stykke arbejde med brugbarheden af ​​kodegeneratoren (og senere viser det sig, at brugertilpasning i den også er kompleks). Brugervenlighed bør være den højeste prioritet for enhver sådan API. Men dette er kun et argument imod kodegenerering. Ellers er det helt manuelt at skrive en lokal repræsentation af indre eller ydre sandhed.

Mange vil sige, at de ikke har tid til at gøre alt dette. De er ved at løbe tør for deadlines for deres Super Produkt. En dag skal vi rydde op i samlebåndene, vi får tid. Jeg vil svare dem:

Sandheden først, eller hvorfor systemet skal designes baseret på databasestrukturen
Original, Alan O'Rourke, Publikumsstak

Men i Hibernate/JPA er det så nemt at skrive Java-kode.

Virkelig. For Hibernate og dets brugere er dette både en velsignelse og en forbandelse. I Hibernate kan du blot skrive et par entiteter, som dette:

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

Og næsten alt er klar. Nu er det op til Hibernate at generere de komplekse "detaljer" om, hvordan præcis denne enhed vil blive defineret i DDL'en af ​​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);

... og start med at køre programmet. En rigtig fed mulighed for at komme hurtigt i gang og prøve forskellige ting.

Tillad mig dog. Jeg løj.

  • Vil Hibernate rent faktisk håndhæve definitionen af ​​denne navngivne primærnøgle?
  • Vil Hibernate oprette et indeks i TITLE? – Jeg ved med sikkerhed, at vi får brug for det.
  • Vil Hibernate nøjagtigt gøre denne nøgleidentifikation i identitetsspecifikationen?

Sikkert ikke. Hvis du udvikler dit projekt fra bunden, er det altid praktisk blot at kassere den gamle database og generere en ny, så snart du tilføjer de nødvendige anmærkninger. Bogenheden vil således i sidste ende antage formen:

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

Fedt nok. Regenerer. Igen, i dette tilfælde vil det være meget nemt i starten.

Men du skal betale for det senere

Før eller siden bliver du nødt til at gå i produktion. Det er når denne model holder op med at virke. Fordi:

I produktionen vil det ikke længere være muligt om nødvendigt at kassere den gamle database og starte fra bunden. Din database vil blive arv.

Fra nu af og for evigt bliver du nødt til at skrive DDL-migreringsscripts, for eksempel ved hjælp af Flyway. Hvad vil der ske med dine enheder i dette tilfælde? Du kan enten tilpasse dem manuelt (og dermed fordoble din arbejdsbyrde), eller du kan bede Hibernate om at regenerere dem for dig (hvor sandsynligt er det, at de, der genereres på denne måde, opfylder dine forventninger?) Uanset hvad, så taber du.

Så når først du kommer i produktion, skal du bruge varme patches. Og de skal sættes i produktion meget hurtigt. Da du ikke forberedte og ikke organiserede en glat pipeline af dine migrationer til produktion, lapper du vildt alt. Og så har du ikke længere tid til at gøre alting korrekt. Og du kritiserer Hibernate, fordi det altid er en andens skyld, bare ikke dig...

I stedet kunne tingene have været gjort helt anderledes helt fra begyndelsen. Sæt for eksempel runde hjul på en cykel.

Database først

Den virkelige "sandhed" i dit databaseskema og "suveræniteten" over det ligger i databasen. Skemaet er kun defineret i selve databasen og ingen andre steder, og hver klient har en kopi af dette skema, så det giver perfekt mening at håndhæve overholdelse af skemaet og dets integritet, for at gøre det rigtigt i databasen - hvor informationen er gemt.
Dette er gammel, endda forhastet visdom. Primære og unikke nøgler er gode. Fremmednøgler er gode. Det er godt at tjekke restriktioner. Udtalelser - Bøde.

Desuden er det ikke alt. Hvis du f.eks. bruger Oracle, vil du sandsynligvis gerne specificere:

  • Hvilken tablespace er dit bord i?
  • Hvad er dens PCTFREE-værdi?
  • Hvad er cachestørrelsen i din sekvens (bag id'et)

Dette er måske ikke vigtigt i små systemer, men du behøver ikke vente, indtil du bevæger dig ind i big data-området – du kan begynde at drage fordel af leverandørleverede lageroptimeringer som dem, der er nævnt ovenfor, meget hurtigere. Ingen af ​​de ORM'er, jeg har set (inklusive jOOQ), giver adgang til det fulde sæt af DDL-indstillinger, du måske vil bruge i din database. ORM'er tilbyder nogle værktøjer, der hjælper dig med at skrive DDL.

Men i slutningen af ​​dagen er et veldesignet kredsløb håndskrevet i DDL. Enhver genereret DDL er kun en tilnærmelse af den.

Hvad med klientmodellen?

Som nævnt ovenfor skal du på klienten have en kopi af dit databaseskema, klientvisningen. Det er overflødigt at nævne, at denne klientvisning skal være synkroniseret med den faktiske model. Hvad er den bedste måde at opnå dette på? Brug af en kodegenerator.

Alle databaser giver deres metainformation via SQL. Sådan får du alle tabellerne fra din database i forskellige 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

Disse forespørgsler (eller lignende, afhængigt af om du også skal tage hensyn til views, materialiserede views, tabel-vurderede funktioner) udføres også ved at kalde DatabaseMetaData.getTables() fra JDBC, eller ved at bruge jOOQ-metamodulet.

Ud fra resultaterne af sådanne forespørgsler er det relativt nemt at generere enhver repræsentation på klientsiden af ​​din databasemodel, uanset hvilken teknologi du bruger på klienten.

  • Hvis du bruger JDBC eller Spring, kan du oprette et sæt strengkonstanter
  • Hvis du bruger JPA, kan du generere selve enhederne
  • Hvis du bruger jOOQ, kan du generere jOOQ-metamodellen

Afhængig af hvor meget funktionalitet der tilbydes af din klient API (f.eks. jOOQ eller JPA), kan den genererede metamodel være virkelig rig og komplet. Tag for eksempel muligheden for implicitte joinforbindelser, introduceret i jOOQ 3.11, som er afhængig af genereret metainformation om de udenlandske nøglerelationer, der eksisterer mellem dine tabeller.

Nu vil enhver databasestigning automatisk opdatere klientkoden. Forestil dig for eksempel:

ALTER TABLE book RENAME COLUMN title TO book_title;

Vil du virkelig gerne udføre dette job to gange? I intet tilfælde. Du skal blot begå DDL, køre den gennem din byggepipeline og få den opdaterede enhed:

@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 opdaterede jOOQ-klasse. De fleste DDL-ændringer påvirker også semantikken, ikke kun syntaks. Derfor kan det være nyttigt at kigge i den kompilerede kode for at se, hvilken kode der vil (eller kunne) blive påvirket af din databasetilvækst.

Den eneste sandhed

Uanset hvilken teknologi du bruger, er der altid én model, der er den eneste kilde til sandhed for et eller andet undersystem - eller i det mindste bør vi stræbe efter dette og undgå en sådan virksomhedsforvirring, hvor "sandheden" er overalt og ingen steder på én gang . Alt kunne være meget enklere. Hvis du bare udveksler XML-filer med et andet system, skal du bare bruge XSD. Se på INFORMATION_SCHEMA-metamodellen fra jOOQ i XML-form:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD er godt forstået
  • XSD tokeniserer XML-indhold meget godt og tillader validering på alle klientsprog
  • XSD er velversioneret og har avanceret bagudkompatibilitet
  • XSD kan oversættes til Java-kode ved hjælp af XJC

Det sidste punkt er vigtigt. Når vi kommunikerer med et eksternt system ved hjælp af XML-meddelelser, vil vi være sikre på, at vores meddelelser er gyldige. Dette er meget nemt at opnå ved at bruge JAXB, XJC og XSD. Det ville være ren vanvid at tro, at med en "Java first"-designtilgang, hvor vi laver vores budskaber som Java-objekter, at de på en eller anden måde kunne kortlægges sammenhængende til XML og sendes til et andet system til forbrug. XML genereret på denne måde ville være af meget dårlig kvalitet, udokumenteret og vanskelig at udvikle. Hvis der var en service level agreement (SLA) for sådan en grænseflade, ville vi straks skrue op for det.

Helt ærligt, det er, hvad der sker hele tiden med JSON API'er, men det er en anden historie, jeg vil skændes næste gang...

Databaser: det er det samme

Når du arbejder med databaser, indser du, at de alle grundlæggende ligner hinanden. Basen ejer sine data og skal administrere ordningen. Eventuelle ændringer i skemaet skal implementeres direkte i DDL, så den enkelte sandhedskilde opdateres.

Når en kildeopdatering er sket, skal alle klienter også opdatere deres kopier af modellen. Nogle klienter kan skrives i Java ved hjælp af jOOQ og Hibernate eller JDBC (eller begge). Andre klienter kan skrives i Perl (vi ønsker dem bare held og lykke), mens andre kan skrives i C#. Det er lige meget. Hovedmodellen er i databasen. Modeller genereret ved hjælp af ORM'er er normalt af dårlig kvalitet, dårligt dokumenterede og vanskelige at udvikle.

Så tag ikke fejl. Lav ikke fejl fra begyndelsen. Arbejd fra databasen. Byg en implementeringspipeline, der kan automatiseres. Aktiver kodegeneratorer for at gøre det nemt at kopiere din databasemodel og dumpe den på klienter. Og stop med at bekymre dig om kodegeneratorer. De er gode. Med dem bliver du mere produktiv. Du skal bare bruge lidt tid på at sætte dem op helt fra begyndelsen – og så venter der dig år med øget produktivitet, som vil udgøre historien om dit projekt.

Tak mig ikke endnu, senere.

Afklaring

For at være klar: Denne artikel fortaler på ingen måde, at du skal bøje hele systemet (dvs. domæne, forretningslogik osv. osv.) for at passe til din databasemodel. Det, jeg siger i denne artikel, er, at klientkoden, der interagerer med databasen, skal handle på basis af databasemodellen, så den ikke selv gengiver databasemodellen i en "førsteklasses" status. Denne logik er normalt placeret ved dataadgangslaget på din klient.

I to-niveau arkitekturer, som stadig er bevaret nogle steder, kan en sådan systemmodel være den eneste mulige. Men i de fleste systemer forekommer dataadgangslaget for mig at være et "undersystem", der indkapsler databasemodellen.

undtagelser

Der er undtagelser til enhver regel, og jeg har allerede sagt, at tilgangen til database-første og kildekodegenerering nogle gange kan være upassende. Her er et par sådanne undtagelser (der er sikkert andre):

  • Når skemaet er ukendt og skal opdages. For eksempel er du udbyder af et værktøj, der hjælper brugere med at navigere i ethvert diagram. Åh. Der er ingen kodegenerering her. Men alligevel kommer databasen først.
  • Når et kredsløb skal genereres på farten for at løse et eller andet problem. Dette eksempel virker som en lidt fantasifuld version af mønsteret enhedsattributværdi, dvs. du har ikke rigtig en klart defineret ordning. I dette tilfælde kan du ofte ikke engang være sikker på, at et RDBMS passer til dig.

Undtagelser er i sagens natur exceptionelle. I de fleste tilfælde, der involverer brugen af ​​et RDBMS, er skemaet kendt på forhånd, det ligger i RDBMS og er den eneste kilde til "sandhed", og alle klienter skal erhverve kopier afledt af det. Ideelt set skal du bruge en kodegenerator.

Kilde: www.habr.com

Tilføj en kommentar