Sannheten først, eller hvorfor systemet må designes basert på databasestrukturen

Hei Habr!

Vi fortsetter å forske på temaet Java и vår, inkludert på databasenivå. I dag inviterer vi deg til å lese om hvorfor, når du designer store applikasjoner, er det databasestrukturen, og ikke Java-koden, som skal være av avgjørende betydning, hvordan dette gjøres, og hvilke unntak det er fra denne regelen.

I denne ganske sene artikkelen vil jeg forklare hvorfor jeg tror at i nesten alle tilfeller bør datamodellen i en applikasjon være utformet "fra databasen" i stedet for "fra funksjonene til Java" (eller hvilket klientspråk du er jobber med). Ved å ta den andre tilnærmingen, setter du deg opp for en lang vei med smerte og lidelse når prosjektet ditt begynner å vokse.

Artikkelen er skrevet basert på Et spørsmål, gitt på Stack Overflow.

Interessante diskusjoner om reddit i seksjoner /r/java и /r/programmering.

Kodegenerering

Hvor overrasket jeg var over at det er et så lite segment av brukere som, etter å ha blitt kjent med jOOQ, er opprørt over det faktum at jOOQ seriøst er avhengig av generering av kildekode for å fungere. Ingen hindrer deg i å bruke jOOQ slik du synes, eller tvinger deg til å bruke kodegenerering. Men standard (som beskrevet i manualen) måten å jobbe med jOOQ på er at du starter med et (legacy) databaseskjema, reverserer det ved å bruke jOOQ-kodegeneratoren for derved å få et sett med klasser som representerer tabellene dine, og deretter skrive type -sikre spørringer til disse tabellene:

	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 utenfor sammenstillingen, eller manuelt ved hver sammenstilling. For eksempel kan slik regenerering følge umiddelbart etter Flyway-databasemigrering, som også kan gjøres manuelt eller automatisk.

Generering av kildekode

Det er ulike filosofier, fordeler og ulemper knyttet til disse tilnærmingene til kodegenerering – manuell og automatisk – som jeg ikke skal diskutere i detalj i denne artikkelen. Men generelt sett er hele poenget med den genererte koden at den lar oss reprodusere i Java den "sannheten" som vi tar for gitt, enten innenfor systemet vårt eller utenfor det. På en måte er dette hva kompilatorer gjør når de genererer bytekode, maskinkode eller annen form for kildekode - vi får en representasjon av vår "sannhet" på et annet språk, uavhengig av de spesifikke årsakene.

Det finnes mange slike kodegeneratorer. For eksempel, XJC kan generere Java-kode basert på XSD- eller WSDL-filer. Prinsippet er alltid det samme:

  • Det er en viss sannhet (intern eller ekstern) - for eksempel en spesifikasjon, en datamodell, etc.
  • Vi trenger en lokal representasjon av denne sannheten i vårt programmeringsspråk.

Dessuten er det nesten alltid tilrådelig å generere en slik representasjon for å unngå redundans.

Typeleverandører og kommentarbehandling

Merk: en annen, mer moderne og spesifikk tilnærming til å generere kode for jOOQ er å bruke typeleverandører, slik de er implementert i F#. I dette tilfellet genereres koden av kompilatoren, faktisk på kompileringsstadiet. I prinsippet eksisterer ikke slik kode i kildeform. Java har lignende, men ikke like elegante, verktøy - annotasjonsprosessorer, for eksempel, Lombok.

På en måte skjer de samme tingene her som i det første tilfellet, med unntak av:

  • Du ser ikke den genererte koden (kanskje denne situasjonen virker mindre frastøtende for noen?)
  • Du må sørge for at typer kan gis, det vil si at "sann" alltid må være tilgjengelig. Dette er enkelt i tilfellet med Lombok, som kommenterer "sannhet". Det er litt mer komplisert med databasemodeller som er avhengig av en konstant tilgjengelig direkteforbindelse.

Hva er problemet med kodegenerering?

I tillegg til det vanskelige spørsmålet om hvordan man best kjører kodegenerering – manuelt eller automatisk, må vi også nevne at det er folk som mener at kodegenerering ikke er nødvendig i det hele tatt. Begrunnelsen for dette synspunktet, som jeg kom over oftest, er at det da er vanskelig å sette opp en byggerørledning. Ja, det er veldig vanskelig. Ytterligere infrastrukturkostnader oppstår. Hvis du akkurat har begynt med et bestemt produkt (enten det er jOOQ, eller JAXB, eller Hibernate osv.), tar det tid å sette opp et produksjonsmiljø som du heller vil bruke på å lære selve API-en slik at du kan trekke ut verdi fra det .

Hvis kostnadene forbundet med å forstå strukturen til generatoren er for høye, så gjorde API-en en dårlig jobb med brukbarheten til kodegeneratoren (og senere viser det seg at brukertilpasning i den også er vanskelig). Brukervennlighet bør ha høyeste prioritet for et slikt API. Men dette er bare ett argument mot kodegenerering. Ellers er det helt manuelt å skrive en lokal representasjon av indre eller ytre sannhet.

Mange vil si at de ikke har tid til å gjøre alt dette. De går tom for tidsfrister for superproduktet sitt. En dag skal vi rydde opp i monteringstransportørene, vi får tid. Jeg vil svare dem:

Sannheten først, eller hvorfor systemet må designes basert på databasestrukturen
Original, Alan O'Rourke, Publikumsstabel

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

Egentlig. For Hibernate og dets brukere er dette både en velsignelse og en forbannelse. I Hibernate kan du ganske enkelt skrive et par enheter, som dette:

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

Og nesten alt er klart. Nå er det opp til Hibernate å generere de komplekse "detaljene" om hvordan nøyaktig denne enheten vil bli definert i DDL-en til 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 begynn å kjøre programmet. En veldig kul mulighet til å komme raskt i gang og prøve forskjellige ting.

Men tillat meg. Jeg løy.

  • Vil Hibernate faktisk håndheve definisjonen av denne navngitte primærnøkkelen?
  • Vil Hibernate opprette en indeks i TITLE? – Jeg vet med sikkerhet at vi kommer til å trenge det.
  • Vil Hibernate nøyaktig gjøre denne nøkkelidentifikasjonen i identitetsspesifikasjonen?

Sannsynligvis ikke. Hvis du utvikler prosjektet fra bunnen av, er det alltid praktisk å forkaste den gamle databasen og generere en ny så snart du legger til de nødvendige kommentarene. Dermed vil bokentiteten til slutt ha formen:

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

Kul. Regenerer. Igjen, i dette tilfellet vil det være veldig enkelt i starten.

Men du må betale for det senere

Før eller siden må du gå i produksjon. Det er da denne modellen slutter å fungere. Fordi:

I produksjon vil det ikke lenger være mulig, om nødvendig, å forkaste den gamle databasen og starte fra bunnen av. Databasen din vil bli gammel.

Fra nå av og for alltid må du skrive DDL-migreringsskript, for eksempel ved bruk av Flyway. Hva vil skje med enhetene dine i dette tilfellet? Du kan enten tilpasse dem manuelt (og dermed doble arbeidsmengden din), eller du kan be Hibernate om å regenerere dem for deg (hvor sannsynlig er det at de som genereres på denne måten oppfyller forventningene dine?) Uansett, du taper.

Så når du kommer i produksjon, trenger du varme oppdateringer. Og de må settes i produksjon veldig raskt. Siden du ikke forberedte og ikke organiserte en jevn pipeline av migreringene dine for produksjon, lapper du vilt på alt. Og da har du ikke lenger tid til å gjøre alt riktig. Og du kritiserer Hibernate, fordi det alltid er andres feil, bare ikke deg...

I stedet kunne ting vært gjort helt annerledes helt fra begynnelsen. Sett for eksempel runde hjul på en sykkel.

Database først

Den virkelige "sannheten" i databaseskjemaet ditt og "suvereniteten" over den ligger i databasen. Skjemaet er definert kun i selve databasen og ingen andre steder, og hver klient har en kopi av dette skjemaet, så det er fornuftig å håndheve samsvar med skjemaet og dets integritet, for å gjøre det riktig i databasen - der informasjonen er lagret.
Dette er gammel, ja, til og med slem visdom. Primære og unike nøkler er gode. Fremmednøkler er bra. Å sjekke restriksjoner er bra. Uttalelser - Fint.

Dessuten er det ikke alt. Hvis du for eksempel bruker Oracle, vil du sannsynligvis spesifisere:

  • Hvilken bordplass er bordet ditt i?
  • Hva er PCTFREE-verdien?
  • Hva er cache-størrelsen i sekvensen din (bak id-en)

Dette er kanskje ikke viktig i små systemer, men du trenger ikke å vente til du beveger deg inn i big data-området – du kan begynne å dra nytte av leverandørleverte lagringsoptimaliseringer som de nevnt ovenfor mye tidligere. Ingen av ORMene jeg har sett (inkludert jOOQ) gir tilgang til hele settet med DDL-alternativer du kanskje vil bruke i databasen din. ORM-er tilbyr noen verktøy som hjelper deg med å skrive DDL.

Men på slutten av dagen er en godt designet krets håndskrevet i DDL. Enhver generert DDL er bare en tilnærming av den.

Hva med klientmodellen?

Som nevnt ovenfor, på klienten trenger du en kopi av databaseskjemaet ditt, klientvisningen. Unødvendig å nevne må denne klientvisningen være synkronisert med den faktiske modellen. Hva er den beste måten å oppnå dette på? Ved hjelp av en kodegenerator.

Alle databaser gir sin metainformasjon via SQL. Slik henter du alle tabellene fra databasen på forskjellige 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 spørringene (eller lignende, avhengig av om du også må vurdere visninger, materialiserte visninger, funksjoner med tabellverdi) utføres også ved å ringe DatabaseMetaData.getTables() fra JDBC, eller ved å bruke jOOQ-metamodulen.

Fra resultatene av slike spørringer er det relativt enkelt å generere en hvilken som helst representasjon på klientsiden av databasemodellen din, uavhengig av hvilken teknologi du bruker på klienten.

  • Hvis du bruker JDBC eller Spring, kan du lage et sett med strengkonstanter
  • Hvis du bruker JPA, kan du generere enhetene selv
  • Hvis du bruker jOOQ, kan du generere jOOQ-metamodellen

Avhengig av hvor mye funksjonalitet som tilbys av klient-APIet ditt (f.eks. jOOQ eller JPA), kan den genererte metamodellen være virkelig rik og komplett. Ta for eksempel muligheten for implisitte sammenføyninger, introdusert i jOOQ 3.11, som er avhengig av generert metainformasjon om fremmednøkkelrelasjonene som eksisterer mellom tabellene dine.

Nå vil enhver databaseøkning automatisk oppdatere klientkoden. Tenk deg for eksempel:

ALTER TABLE book RENAME COLUMN title TO book_title;

Vil du virkelig gjøre denne jobben to ganger? Ikke i noe tilfelle. Bare bruk DDL, kjør den gjennom byggepipeline, og få den oppdaterte 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 oppdaterte jOOQ-klassen. De fleste DDL-endringer påvirker også semantikk, ikke bare syntaks. Derfor kan det være nyttig å se i den kompilerte koden for å se hvilken kode som vil (eller kan) bli påvirket av databasetilveksten.

Den eneste sannheten

Uansett hvilken teknologi du bruker, er det alltid én modell som er den eneste kilden til sannhet for et delsystem - eller i det minste bør vi strebe etter dette og unngå slik virksomhetsforvirring, der "sannhet" er overalt og ingensteds på en gang . Alt kunne vært mye enklere. Hvis du bare utveksler XML-filer med et annet system, bruk bare 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ått
  • XSD tokeniserer XML-innhold veldig godt og tillater validering på alle klientspråk
  • XSD er velversjonert og har avansert bakoverkompatibilitet
  • XSD kan oversettes til Java-kode ved hjelp av XJC

Det siste punktet er viktig. Når vi kommuniserer med et eksternt system ved hjelp av XML-meldinger, ønsker vi å være sikre på at meldingene våre er gyldige. Dette er veldig enkelt å oppnå ved å bruke JAXB, XJC og XSD. Det ville være ren galskap å tro at med en "Java first"-designtilnærming der vi lager budskapene våre som Java-objekter, at de på en eller annen måte kunne kartlegges sammenhengende til XML og sendes til et annet system for forbruk. XML generert på denne måten ville være av svært dårlig kvalitet, udokumentert og vanskelig å utvikle. Hvis det fantes en servicenivåavtale (SLA) for et slikt grensesnitt, ville vi umiddelbart skrudd opp.

Ærlig talt, dette er det som skjer hele tiden med JSON APIer, men det er en annen historie, jeg vil krangle neste gang...

Databaser: de er det samme

Når du arbeider med databaser, innser du at de alle i utgangspunktet er like. Basen eier sine data og skal administrere ordningen. Eventuelle endringer som gjøres i skjemaet må implementeres direkte i DDL slik at den eneste sannhetens kilde oppdateres.

Når en kildeoppdatering har funnet sted, må alle klienter også oppdatere sine kopier av modellen. Noen klienter kan skrives i Java ved å bruke jOOQ og Hibernate eller JDBC (eller begge deler). Andre klienter kan skrives i Perl (vi ønsker dem bare lykke til), mens andre kan skrives i C#. Det spiller ingen rolle. Hovedmodellen ligger i databasen. Modeller generert ved hjelp av ORM-er er vanligvis av dårlig kvalitet, dårlig dokumentert og vanskelig å utvikle.

Så ikke gjør feil. Ikke gjør feil helt fra begynnelsen. Arbeid fra databasen. Bygg en distribusjonspipeline som kan automatiseres. Aktiver kodegeneratorer for å gjøre det enkelt å kopiere databasemodellen og dumpe den på klienter. Og slutt å bekymre deg for kodegeneratorer. De er gode. Med dem vil du bli mer produktiv. Du trenger bare å bruke litt tid på å sette dem opp helt fra begynnelsen - og så venter år med økt produktivitet på deg, som vil utgjøre historien til prosjektet ditt.

Ikke takk meg ennå, senere.

Avklaring

For å være tydelig: Denne artikkelen forfekter på ingen måte at du trenger å bøye hele systemet (dvs. domene, forretningslogikk, etc., etc.) for å passe til databasemodellen din. Det jeg sier i denne artikkelen er at klientkoden som samhandler med databasen skal handle på grunnlag av databasemodellen, slik at den ikke selv reproduserer databasemodellen i en "førsteklasses" status. Denne logikken er vanligvis plassert ved datatilgangslaget på klienten din.

I to-nivå arkitekturer, som fortsatt er bevart noen steder, kan en slik systemmodell være den eneste mulige. Men i de fleste systemer virker datatilgangslaget for meg å være et "undersystem" som innkapsler databasemodellen.

unntakene

Det er unntak fra hver regel, og jeg har allerede sagt at tilnærmingen til å generere database først og kildekode noen ganger kan være upassende. Her er et par slike unntak (det finnes sikkert andre):

  • Når skjemaet er ukjent og må oppdages. Du er for eksempel leverandør av et verktøy som hjelper brukere med å navigere i ethvert diagram. Uff. Det er ingen kodegenerering her. Men likevel kommer databasen først.
  • Når en krets må genereres i farten for å løse et eller annet problem. Dette eksemplet virker som en litt fantasifull versjon av mønsteret enhetsattributtverdi, det vil si at du egentlig ikke har et klart definert opplegg. I dette tilfellet kan du ofte ikke engang være sikker på at en RDBMS vil passe deg.

Unntak er av natur eksepsjonelle. I de fleste tilfeller som involverer bruk av en RDBMS, er skjemaet kjent på forhånd, det ligger innenfor RDBMS og er den eneste kilden til "sannhet", og alle klienter må skaffe seg kopier avledet fra det. Ideelt sett må du bruke en kodegenerator.

Kilde: www.habr.com

Legg til en kommentar