Tõde on peamine, või miks süsteemi tuleks projekteerida lähtudes andmebaasist

Tere, Habr!

Me jätkame teema uurimist Java ja Spring, sealhulgas andmebaaside tasandil. Täna pakume lugeda, miks suurte rakenduste projekteerimisel peaks andmebaasi struktuur, mitte Java kood, mängima määravat rolli, kuidas seda teha ja millised erandid sellest reeglist on.

Selles üsna hilises artiklis selgitan, miks arvan, et peaaegu kõigis juhtudel tuleks rakenduse andmemudelit projekteerida «andmebaasist lähtudes», mitte «Java võimalustest lähtudes» (või mõnest muust kliendi keelest, millega te töötate). Valides teise lähenemise, astud pika valude ja kannatuste teele, niipea kui su projekt hakkab kasvama.

Artikkel on kirjutatud ühe küsimuse, mille esitasid Stack Overflow's.

Huvitavad arutelud reddit'is osades /r/java ja /r/programming.

Koodigeneratsioon

Olin väga üllatunud, et on nii vähe kasutajaid, kes, tutvudes jOOQ-iga, kurdavad selle üle, et jOOQ puhul tuginedakse tõsiselt koodigeneratsioonile. Keegi ei takista sind kasutamast jOOQ-d nii, nagu sa arvad olevat vajalik, ja ei sunni kasutama koodigeneerimist. Kuid vaikimisi (nagu on kirjeldatud juhendis) toimub jOOQ kasutamine nii: alustad (pärandatud) andmebaasi skeemist, teed selle tagasiprojekteerimise jOOQ koodigeneraatori abil, et saada klasside kogum, mis esindab sinu tabeleid, ja siis kirjutad tüübikindlaid päringuid nende tabelite järgi:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Tüüpide teave on saadud
//   genereeritud koodi põhjal, millele viitab alljärgnev
//   SELECT tingimus
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ genereeritud nimed
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Kood genereeritakse kas käsitsi väljaspool kogumist või käsitsi iga kogumise käigus. Näiteks võib selline regeneerimine toimuda kohe pärast Flyway andmebaasi migreerimist, mida saab samuti teha käsitsi või automaatselt..

Koodigeneratsioon

Selliste koodigeneratsiooni lähenemiste – nii manuaalsete kui automaatsete – taga on erinevad filosoofiad, eelised ja puudused, millest ma ei kavatse selle artikli raames pikalt rääkida. Kuid üldiselt on genereeritud koodi tuum selles, et see võimaldab Java-s esindada seda «tõde», mida me peame iseenesestmõistetavaks, kas meie süsteemis või väljaspool seda. Teatud mõttes teevad sama ka kompilaatorid, kes genereerivad vahelood, masinkoodi või mõnd muud koodi vormi, põhinedes lähtekoodile – me saame esitluse meie «tõest» teises keeles, sõltumata konkreetsetest põhjustest.

Selliseid koodigeneraatoreid on palju. Näiteks XJC võib genereerida Java koodi XSD või WSDL failide põhjal.Printsiip on alati sama:

  • On olemas mingi tõde (sisemine või väline) – näiteks spetsifikatsioon, andmemudel jne.
  • Me vajame selle tõe kohalikke esitlusi meie programmeerimiskeeles.

Pealegi on sellise esitluse genereerimine peaaegu alati mõistlik – et vältida liigset koormust.

Tüüpide pakkujad ja annotatsioonide töötlemine

Märkuseks: teine, kaasaegsem ja spetsiifilisem lähenemine jOOQ koodigeneratsioonile on seotud tüübi pakkujate kasutamisega, nagu need on rakendatud F#-is.Sel juhul genereerib koodi kompilaator tegelikult kompileerimise käigus. Sellist koodi lähtekoodina ei eksisteeri. Java-s on sarnased, kuigi mitte nii elegantne tööriistad – need on annotatsiooniprotsessorid, näiteks Lombok..

Teatud mõttes toimub siin sama, mis esimeses juhul, erandiga:

  • Sa ei näe genereeritud koodi (võib-olla tundub see kellelegi mitte nii heidutav?)
  • Sa pead garanteerima, et tüübid on kättesaadavad, st «tõde» peab olema alati kergesti kättesaadav. See on lihtne juhul, kui Lombok, mis annotatsioonib «tõe». Veidi keerulisem on andmebaasi mudelitega, mille töö sõltub pidevalt kergesti kättesaadava elava ühenduse olemasolust.

Mis probleem on koodigeneratsiooniga?

Lisaks nutikale küsimusele selle kohta, kas koodi genereerimist peaks korraldama käsitsi või automaatselt, tuleb mainida ka, et on inimesi, kes usuvad, et koodi genereerimine ei ole vajalik. Selle vaate kõige sagedasem põhjendus, millega ma olen kokku puutunud, on, et siis on raske seadistada kogumise konveierit. Jah, tõepoolest, see on keeruline. Tekkivad täiendavad infrastruktuuri kulud. Kui alles hakkate teatud tootega (olgu see jOOQ, JAXB, või Hibernate jne) tööle, kulub aega, mille te tahaksite pühendada API õppimisele, et hiljem sellest väärtust välja võtta.

Kui kulud, mis on seotud genereerija töö arusaamisega, on liiga suured, siis tõepoolest, on API-s halvasti tööd tehtud koodi genereerimise kasutatavuse osas (ja hiljem selgub, et ka kasutaja seadistamine on keeruline). Kasutajasõbralikkus peaks olema igasuguste API-de kõrgeim prioriteet. Kuid see on vaid üks argument koodi genereerimise vastu. Ülejäänud osas on täielikult käsitsi kirjutada kohalik esitlus sisemisest või välimisest tõest.

Paljud ütlevad, et neil ei ole aega sellega tegeleda. Neil on tähtajad oma Super-Toote osas. Kunagi hiljem ajame konveiere korda, jõuab. Ma vastan neile:

Tõde on peamine, või miks süsteemi tuleks projekteerida lähtudes andmebaasist
Originaal, Alan O'Rourke, Audience Stack

Aga Hibernate'i / JPA ei ole nii lihtne kirjutada koodi "Java" jaoks.

Tõepoolest. Hibernate'i jaoks on see samal ajal õnn ja needus. Hibernate'is saab lihtsalt kirjutada paar entiteeti, nagu näiteks:

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

Ja peaaegu kõik on valmis. Nüüd on Hibernate'i ülesanne genereerida keerulised "detailid" selle kohta, kuidas see entiteet määratakse teie SQL "dialekti" DDL-is:

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

... ja hakkame rakendust käivitama. Tõeliselt lahe võimalus, et kiiresti tööle asuda ja erinevaid asju proovida.

Kuid oodake. Ma vale rääkisin.

  • Kas Hibernate rakendab tõepoolest selle nimetatud primaarvõtme määratlust?
  • Kas Hibernate loob indeksi TITLE-s? – ma tean kindlasti, et meil seda on vaja.
  • Kas Hibernate tõepoolest muudab selle võtme identifitseerivaks Identity Specificationis?

Tõenäoliselt mitte. Kui arendate oma projekti nullist, on alati mugav lihtsalt vana andmebaas kõrvale visata ja genereerida uus, niipea kui lisate vajalikud annotatsioonid. Seega muutub entiteet Book lõpuks nõnda:

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

Lahe. Generoida uuesti. Taaskord, sel juhul on alguses väga lihtne.

Kuid hiljem tuleb selle eest maksta.

Varem või hiljem peate minema tootmisse. Just siis lakkab see mudel toimimast. Sest:

Tootmises ei saa vanast andmebaasist vajadusel loobuda ja kõike puhtalt lehelt alustada. Teie andmebaas muutub pärandiks.

Nüüd ja igaveseks peate kirjutama DDL migratsiooniskeemid, näiteks kasutades Flyway. Mis siis juhtuma hakkab teie entiteetidega? Saate neid kas käsitsi kohandada (ja seeläbi kahekordistada oma töömahtu) või käskida Hibernate'il need uuesti genereerida (kui tõenäoliselt vastavad genereeritud entiteedid teie ootustele?) Igal juhul olete kaotaja.

Seega, niipea kui te lähete tootmisse, vajate kiireid plaastriteid. Ja neid tuleb tootmisse väga kiiresti anda. Kuna te ei ole ette valmistatud ja ei ole korraldanud oma migratsioonide sujuvat konveierimist, siis patšitakse kõike tohutult. Ja pärast te ei saa kõike õigesti teha. Ja süüdistate Hibernate'i, sest alati on süüdi keegi teine, ainult mitte teie...

Selle asemel oleks saanud alustada kõike täiesti teisiti. Näiteks võite paigaldada ümarad rattad jalgratta peale.

Esiteks andmebaas.

Tõeline "tõde" teie andmebaasi skeemis ja "suveräänsus" selle üle peitub andmebaasis. Skeem määratakse ainult andmebaasis ja mitte kusagil mujal, ja igal kliendil on selle skeemi koopia, seega on täiesti mõistlik rakendada skeemi ja selle terviklikkuse järgimist otse andmebaasis – seal, kus teave asub.
See on vana, isegi kulunud tarkus. Primaar- ja unikaalsed võtmed on head. Välised võtmed – head. Piirangute kontrollimine – hea. Adekvaatsed väited – on head.

Kuid see pole veel kõik. Näiteks, kui kasutate Oracle'i, tõenäoliselt soovite määrata:

  • Millises tabeliruumis asub teie tabel
  • Mis on selle PCTFREE väärtus
  • Mis on teie järjestuse (ID järgi) vahemälu suurus

Võib-olla ei ole see kõik väikeses süsteemis oluline, kuid ei pea ootama, kuni jõuate „suuremate andmete” valdkonda — saate varakult hakata lõikama kasu andmesalvestuse optimeerimistest, nagu eespool mainitud. Ükski ORM, mida olen näinud (sealhulgas jOOQ), ei paku juurdepääsu täielikule DDL valikule, mida võite oma andmebaasis kasutada. ORM-id pakuvad mõningaid tööriistu, mis aitavad kirjutada DDL-d.

Aga lõpuks on hästi kavandatud skeem käsitsi kirjutatud DDL-l. Iga genereeritud DDL on vaid selle ligikaudne versioon.

Ent mis teie kliendimudeli puhul?

Nagu eespool mainitud, vajate kliendis koopia oma andmebaasi skeemist, klientide vaadet. Üksikasjalikult öeldes, peab see klientide vaade olema sünkroniseeritud tegeliku mudeliga. Kuidas seda kõige paremini saavutada? Koode genereerimise abil.

Kõik andmebaasid pakuvad oma metaandmeid SQL-i kaudu. Siin on, kuidas saada oma andmebaasist kõik tabelid erinevates SQL-i dialektides:

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

Need päringud (või neile sarnased, sõltuvalt sellest, kas tuleb arvestada ka vaateid, materialiseeritud vaateid, tabeliväärtusega funktsioone) saab samuti teostada DatabaseMetaData.getTables() JDBC kaudu või jOOQ meta-moduli kaudu.

Selliste päringute tulemustest on suhteliselt lihtne genereerida mis tahes kliendi vaade teie andmebaasi mudelist, sõltumata sellest, millist tehnoloogiat kasutate oma kliendis.

  • Kui kasutate JDBC-d või Springi, saate luua stringi konstantide komplekti.
  • Kui kasutate JPA-d, saate genereerida ise entiteedid.
  • Kui kasutate jOOQ-d, saate genereerida jOOQ meta-mudeli.

Sõltuvalt teie kliendi API-st pakutavatest võimalustest (nt jOOQ või JPA) võib genereeritud meta-mudel olla tõeliselt rikas ja täielik. Võtame näiteks vaikimisi ühendused, mis tuli jOOQ 3.11-sse, mis toetub teie tabelite vahelise välisvõtme suhete genereeritud metaandmetele.

Nüüd toob iga andmebaasi valik automaatselt kaasa kliendi koodi värskendamise. Kujutage näiteks ette:

ALTER TABLE book RENAME COLUMN title TO book_title;

Kas te tõeliselt soovite seda tööd kaks korda teha? Absoluutselt mitte. Lihtsalt salvestame DDL-i, käivitame selle teie ehitustoru kaudu ja saame värskendatud entiteedi:

@Entity
@Table(name = "book", indexes = {

  // Olete sellele mõelnud?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;

  @Column("book_title")
  String bookTitle;
}

Või värskendatud jOOQ klass. Suurem osa DDL muudatustest kajastuvad ka semantikas, mitte ainult süntaksis. Seetõttu on mugav vaadata kompileeritud koodi, milline kood võib olla (või on) mõjutatud teie andmebaasi kasvust.

Ainus tõde

Ükskõik, millist tehnoloogiat te kasutate, on alati olemas üks mudel, mis on mingi alamsüsteemi ainus tõeallikas – või vähemalt peaksime sellele püüdlema ja vältima sellist ettevõtluse segadust, kus "tõde" on korraga igal pool ja mitte kusagil. Kõik võiks olla palju lihtsam. Kui te lihtsalt vahetate XML-faile mõne muu süsteemiga, kasutage lihtsalt XSD-d. Vaadake jOOQ meta-mudelit XML-vormingus:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD on hästi arusaadav
  • XSD tähistab XML-i sisu väga hästi ja võimaldab valideerimist kõigis kliendi keeltes
  • XSD-l on hea versioonimisel ja välja arendatud tagurpidi ühilduvus
  • XSD-d saab Java koodi teisendada XJC abil

Viimane punkt on oluline. Suheldes välise süsteemiga XML-sõnumite kaudu, tahame me olla kindlad, et meie sõnumid on kehtivad. Seda on väga lihtne saavutada, kasutades JAXB, XJC ja XSD. Oleks täielik hullumeelsus loota, et Java-põhise projekteerimise lähenemise korral, kus me loome meie sõnumid Java objektide kujul, oleks neid võimalik kuidagi õigesti XML-ile kaardistada ja saata teise süsteemi tarbimiseks. Selliselt genereeritud XML oleks väga madala kvaliteediga, dokumenteerimata ja seda oleks raske arendada. Kui sellistele liidesele oleks olemas teenuse kvaliteedi leping (SLA), siis rikuksime me selle kohe ära.

Ausalt öeldes juhtub see pidevalt JSON API-dega, aga see on juba teine lugu, järgmisel korral räägin…

Andmebaasid: see on kõik sama.

Töötades andmebaasidega, mõistate, et need on kõik põhimõtteliselt sarnased. Andmebaas omab oma andmeid ja peab haldama skeemi. Süsteemi muutmised peavad olema rakendatud otse DDL-le, et värskendada ainsat tõe allikat.

Kui allika uuendamine on toimunud, peavad kõik kliendid samuti värskendama oma versioone mudelist. Mõned kliendid võivad olla kirjutatud Java-s, kasutades jOOQ-d ja Hibernate-i või JDBC-d (või kõike koos). Teised kliendid võivad olla kirjutatud Perl-is (soovin neile edu), kolmandad – C#-is. See ei ole oluline. Peamine mudel asub andmebaasis. ORM-iga genereeritud mudelid on tavaliselt halva kvaliteediga, halvasti dokumenteeritud ja neid on raske arendada.

Seega ärge tehke vigu. Ärge tehke vigu algusest peale. Töödelge andmebaasist lähtuvalt. Looge selline juurutamisprotsess, mida on võimalik automatiseerida. Lisage koodigeneraatorid, et oleks mugav kopeerida oma andmebaasi mudelit ja edastada see klientidele. Ja lõpetage koodigeneraatorite pärast muretsemine. Need on head. Nende abil saate tõhusamaks. Tuleb lihtsalt alguses natuke aega nende seadistamiseks kulutada – ja teie ees ootavad aastaid suurenenud tootlikkust, millest hargneb teie projekti lugu.

Ärge tänage veel, hiljem.

Selgitus

Selguse huvides: see artikkel ei propageeri mingil juhul, et kogu süsteem tuleks üles ehitada andmebaasi mudeli kohaselt (st, aineala, äri loogika jne). Selle artikli mõte on, et kliendi kood, mis suhtleb andmebaasiga, peaks toimima andmebaasi mudelist lähtuvalt, nii et see ei peegeldaks andmebaasi mudelit 'esmaklassilisena'. Selline loogika asub tavaliselt andmebaasi juurde pääsemise tasemel teie kliendil.

Kaheetapilistes arhitektuurides, mis on veel mõnes kohas alles, võib selline süsteemi mudel olla ainus võimalik. Kuid enamikus süsteemides tundub, et andme juurde pääsemise tase on 'alam süsteem', mis kapseldab andmebaasi mudelit.

Erandid

Iga reegli kohta on erandeid, ja ma olen juba öelnud, et andmebaasi esmasuse ja lähtekoodi genereerimise lähenemine võib mõnikord osutuda sobimatuks. Siin on paar sellist erandit (võimalik, et leidub ka teisi):

  • Kui skeem on teadmata ja seda tuleb avada. Näiteks, kui olete tööriistade pakkuja, mis aitab kasutajatel igasugustes skeemides orienteeruda. Uff. Siin ilma koodigeneratsioonita läbi ei saa. Aga siiski – andmebaas on esikohal.
  • Kui skeem tuleb genereerida jooksvalt teatud ülesande täitmiseks. See näide tundub olema veidi keeruline versioon mustrist entity attribute value, st teil pole tegelikult selgelt määratletud skeemi. Sellisel juhul ei saa sageli olla kindel, kas RDBMS teile sobib.

Erandid on oma olemuselt erandlikud. Enamikus RDBMS-t seotud juhtumites on skeem eelnevalt teada, see on RDBMS-is ja on ainus 'tõe' allikas ning kõik kliendid peavad hankima oma versioonid, mis on sellest derivaadid. Ideaalis tuleks kasutada koodigeneraatorit.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster