E vërteta së pari, ose pse sistemi duhet të dizajnohet bazuar në strukturën e bazës së të dhënave

Hej Habr!

Ne vazhdojmë të shqyrtojmë temën Java и Pranverëduke përfshirë në nivelin e bazës së të dhënave. Sot ne ofrojmë të lexojmë se pse, kur dizajnoni aplikacione të mëdha, është struktura e bazës së të dhënave, dhe jo kodi Java, ajo që duhet të jetë me rëndësi vendimtare, si bëhet kjo dhe cilat janë përjashtimet nga ky rregull.

Në këtë artikull mjaft të vonuar, unë do të shpjegoj pse mendoj se pothuajse në të gjitha rastet, modeli i të dhënave në një aplikacion duhet të dizajnohet "nga baza e të dhënave" dhe jo "nga aftësitë e Java" (ose cilado gjuhë klienti që jeni duke punuar me). Duke zgjedhur qasjen e dytë, ju hyni në një rrugë të gjatë dhimbjeje dhe vuajtjeje sapo projekti juaj të fillojë të rritet.

Artikulli është shkruar në bazë të nje pyetje, dhënë në Stack Overflow.

Diskutime interesante mbi reddit në seksione /r/java и /r/programim.

Gjenerimi i kodit

Sa i befasuar jam që ka një shtresë kaq të vogël përdoruesish, të cilët, pasi janë njohur me jOOQ, ndien keqardhje për faktin që jOOQ mbështetet seriozisht në gjenerimin e kodit burimor për të ekzekutuar. Askush nuk ju ndalon të përdorni jOOQ ashtu siç e shihni të arsyeshme dhe askush nuk ju detyron të përdorni gjenerimin e kodit. Por si parazgjedhje (siç përshkruhet në manual), jOOQ funksionon si kjo: ju filloni me një skemë të bazës së të dhënave (të trashëguara), e ndërtoni atë me gjeneratorin e kodit jOOQ për të marrë një grup klasash që përfaqësojnë tabelat tuaja dhe më pas shkruani tip- pyetje të sigurta kundrejt këtyre tabelave:

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

Kodi gjenerohet ose manualisht jashtë ndërtimit, ose manualisht në çdo ndërtim. Për shembull, një rigjenerim i tillë mund të ndodhë menjëherë pas Migrimi i bazës së të dhënave Flyway, i cili gjithashtu mund të bëhet manualisht ose automatikisht.

Gjenerimi i kodit burimor

Ka filozofi të ndryshme, avantazhe dhe disavantazhe që lidhen me këto qasje të gjenerimit të kodit - manuale dhe automatike - të cilat nuk do t'i diskutoj në detaje në këtë artikull. Por, në përgjithësi, e gjithë pika e kodit të krijuar është se ai ju lejon të riprodhoni në Java "të vërtetën" që ne e marrim si të mirëqenë, qoftë brenda sistemit tonë ose jashtë tij. Në njëfarë kuptimi, përpiluesit që gjenerojnë bytekod, kodin e makinës ose ndonjë lloj tjetër kodi nga kodi burimor bëjnë të njëjtën gjë - ne marrim një paraqitje të "të vërtetës" tonë në një gjuhë tjetër, pavarësisht nga arsyet specifike.

Ka shumë gjeneratorë të tillë kodesh. Për shembull, XJC mund të gjenerojë kod Java bazuar në skedarët XSD ose WSDL. Parimi është gjithmonë i njëjtë:

  • Ka disa të vërteta (të brendshme ose të jashtme) - për shembull, një specifikim, një model të dhënash, etj.
  • Ne kemi nevojë për një përfaqësim lokal të kësaj të vërtete në gjuhën tonë të programimit.

Për më tepër, është pothuajse gjithmonë e këshillueshme që të gjenerohet një përfaqësim i tillë - në mënyrë që të shmanget teprica.

Ofruesit e llojit dhe përpunimi i shënimeve

Shënim: Një qasje tjetër, më moderne dhe specifike për gjenerimin e kodit për jOOQ përfshin përdorimin e ofruesve të tipit, siç janë zbatuar në F#. Në këtë rast, kodi gjenerohet nga përpiluesi, në fakt në fazën e kompilimit. Në parim, një kod i tillë nuk ekziston në formën e kodeve burimore. Në Java, ka mjete të ngjashme, megjithëse jo aq elegante - këto janë procesorë shënimesh, për shembull, Lombok.

Në një farë kuptimi, të njëjtat gjëra ndodhin këtu si në rastin e parë, përveç:

  • Ju nuk e shihni kodin e krijuar (ndoshta kjo situatë nuk i duket aq e neveritshme dikujt?)
  • Duhet të siguroheni që llojet mund të sigurohen, domethënë, "e vërtetë" duhet të jetë gjithmonë e disponueshme. Kjo është e lehtë në rastin e Lombok, i cili shënon "të vërtetën". Është pak më e vështirë me modelet e bazës së të dhënave që varen nga një lidhje e drejtpërdrejtë që është gjithmonë e disponueshme.

Cili është problemi me gjenerimin e kodit?

Përveç pyetjes së ndërlikuar se si është më mirë të fillohet gjenerimi i kodit - manualisht ose automatikisht, më duhet të përmend se ka njerëz që besojnë se gjenerimi i kodit nuk nevojitet fare. Arsyetimi për këtë këndvështrim, të cilin e kam hasur më shpesh, është se më pas është e vështirë të vendoset tubacioni i ndërtimit. Po, është vërtet e vështirë. Ka kosto shtesë për infrastrukturën. Nëse sapo po filloni me një produkt të caktuar (qoftë ai jOOQ, ose JAXB, ose Hibernate, etj.), duhet kohë për të ngritur një tavolinë pune që dëshironi të shpenzoni duke mësuar vetë API-në për të nxjerrë vlerë prej tij. .

Nëse kostot që lidhen me të kuptuarit e pajisjes së gjeneratorit janë shumë të larta, atëherë, me të vërtetë, API bëri një punë të dobët në përdorshmërinë e gjeneratorit të kodit (dhe në të ardhmen rezulton se personalizimi në të është gjithashtu i vështirë). Përdorshmëria duhet të jetë përparësia më e lartë për çdo API të tillë. Por ky është vetëm një argument kundër gjenerimit të kodit. Përndryshe, shkruani tërësisht me dorë paraqitjen lokale të së vërtetës së brendshme ose të jashtme.

Shumë do të thonë se nuk kanë kohë për t'i bërë të gjitha këto. Ata janë në afatin përfundimtar për Superproduktin e tyre. Një ditë më vonë do të krehim transportuesit e montimit, do të kemi kohë. Unë do t'u përgjigjem atyre:

E vërteta së pari, ose pse sistemi duhet të dizajnohet bazuar në strukturën e bazës së të dhënave
Origjinal, Alan O'Rourke, Audienca Stack

Por në Hibernate / JPA është kaq e lehtë të shkruash kodin "në Java".

Vërtet. Për Hibernate dhe përdoruesit e tij, ky është edhe një ndihmë edhe një mallkim. Në Hibernate, thjesht mund të shkruani disa entitete, si kjo:

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

Dhe pothuajse gjithçka është gati. Tani pjesa e Hibernate është të gjenerojë "detaje" komplekse se si do të përcaktohet saktësisht ky entitet në DDL të "dialektit" tuaj të 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);

... dhe filloni të ekzekutoni aplikacionin. Një veçori vërtet interesante për t'u ngritur dhe për të funksionuar shpejt dhe për të provuar gjëra të ndryshme.

Megjithatë, më lejoni. po gënjeja.

  • A do të zbatojë në të vërtetë Hibernate përkufizimin e këtij çelësi kryesor të emërtuar?
  • A do të krijojë Hibernate një indeks në TITLE? E di me siguri që na duhet.
  • A do ta bëjë Hibernate këtë çelës një çelës identiteti në Specifikimin e Identitetit?

Me siguri jo. Nëse po zhvilloni projektin tuaj nga e para, është gjithmonë e përshtatshme që thjesht të hidhni poshtë bazën e të dhënave të vjetër dhe të krijoni një të re sapo të shtoni shënimet e nevojshme. Pra, entiteti i Librit përfundimisht do të marrë formën:

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

I ftohtë. Rigjeneroni. Përsëri, në këtë rast, do të jetë shumë e lehtë në fillim.

Por ju do të duhet të paguani për të më vonë.

Herët a vonë do t'ju duhet të shkoni në prodhim. Në atë moment modeli pushon së punuari. Sepse:

Në prodhim, nuk do të jetë më e mundur, nëse është e nevojshme, të hidhni poshtë bazën e të dhënave të vjetër dhe të filloni gjithçka nga e para. Baza e të dhënave juaj do të kthehet në një të trashëguar.

Tani e tutje dhe përgjithmonë do t'ju duhet të shkruani Skriptet e migrimit DDL, p.sh. duke përdorur Flyway. Dhe çfarë do të ndodhë me subjektet tuaja në këtë rast? Mund t'i përshtatni ato me dorë (dhe të dyfishoni ngarkesën tuaj të punës) ose t'i rigjeneroni Hibernate për ju (sa ka gjasa që ai i krijuar në këtë mënyrë të përmbushë pritshmëritë tuaja?) Ju humbisni në çdo mënyrë.

Kështu, sapo të kaloni në prodhim, do t'ju nevojiten arna të nxehta. Dhe ato duhet të sillen në prodhim shumë shpejt. Meqenëse nuk keni përgatitur dhe organizuar një tubacion të qetë të migrimeve tuaja për prodhim, ju jeni duke u arnuar pa masë. Dhe atëherë nuk keni kohë për të bërë gjithçka siç duhet. Dhe ju qortoni Hibernate, sepse fajin e ka gjithmonë kushdo, por jo ju ...

Përkundrazi, që në fillim gjithçka mund të ishte bërë krejtësisht ndryshe. Për shembull, vendosni rrota të rrumbullakëta në një biçikletë.

Baza e të dhënave së pari

"E vërteta" e vërtetë në skemën tuaj të bazës së të dhënave dhe "sovraniteti" mbi të qëndron brenda bazës së të dhënave. Skema përcaktohet vetëm në vetë bazën e të dhënave dhe askund tjetër, dhe secili prej klientëve ka një kopje të kësaj skeme, kështu që ka kuptim të plotë që të zbatohet respektimi i skemës dhe integriteti i saj, për ta bërë atë direkt në bazën e të dhënave - ku informacioni ruhet.
Kjo është një urtësi e vjetër, madje e hackneed. Çelësat kryesorë dhe unikë janë të mirë. Çelësat e huaj janë në rregull. Kontrolli i kufizimeve është i mirë. Pohimet - Mirë.

Dhe, kjo nuk është e gjitha. Për shembull, duke përdorur Oracle, ndoshta do të dëshironit të specifikoni:

  • Në cilën hapësirë ​​tavoline ndodhet tabela juaj
  • Cila është vlera e saj pa PCTFREE
  • Cila është madhësia e cache-së në sekuencën tuaj (pas ID-së)

E gjithë kjo mund të mos ketë rëndësi në sistemet e vogla, por nuk është e nevojshme të prisni deri në kalimin në fushën e "të dhënave të mëdha" - mund të filloni të përfitoni nga optimizimet e ruajtjes të ofruara nga shitësi, si ato të përmendura më lart, shumë më herët. Asnjë nga ORM-të që kam parë (përfshirë jOOQ) nuk ofron akses në grupin e plotë të opsioneve DDL që mund të dëshironi të përdorni në bazën e të dhënave tuaja. ORM-të ofrojnë disa mjete për t'ju ndihmuar të shkruani DDL.

Por në fund të ditës, një skemë e projektuar mirë është shkruar me dorë në DDL. Çdo DDL e krijuar është vetëm një përafrim i tij.

Po modeli i klientit?

Siç u përmend më lart, në klient do t'ju duhet një kopje e skemës tuaj të bazës së të dhënave, pamja e klientit. Eshtë e panevojshme të thuhet, kjo pamje e klientit duhet të jetë në sinkron me modelin real. Cila është mënyra më e mirë për ta arritur këtë? Me një gjenerator kodi.

Të gjitha bazat e të dhënave ofrojnë meta-informacionet e tyre nëpërmjet SQL. Ja se si të merrni të gjitha tabelat në dialekte të ndryshme SQL nga databaza juaj:

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

Këto pyetje (ose të ngjashme, në varësi të faktit nëse duhet të merrni parasysh edhe pamjet, pamjet e materializuara, funksionet me vlerë tabele) ekzekutohen gjithashtu duke thirrur DatabaseMetaData.getTables() nga JDBC, ose duke përdorur meta-modulin jOOQ.

Nga rezultatet e pyetjeve të tilla, është relativisht e lehtë për të gjeneruar çdo përfaqësim nga ana e klientit të modelit tuaj të bazës së të dhënave, pavarësisht se çfarë teknologjie përdorni për klientin.

  • Nëse jeni duke përdorur JDBC ose Spring, mund të krijoni një grup konstantesh vargu
  • Nëse jeni duke përdorur JPA, atëherë mund t'i gjeneroni vetë entitetet
  • Nëse jeni duke përdorur jOOQ, mund të gjeneroni meta model jOOQ

Në varësi të aftësive API të klientit tuaj (p.sh. jOOQ ose JPA), modeli meta i krijuar mund të jetë vërtet i pasur dhe i plotë. Merrni, për shembull, mundësinë e bashkimeve të nënkuptuara, prezantuar në jOOQ 3.11, i cili mbështetet në meta-informacionet e krijuara në lidhje me marrëdhëniet kryesore të jashtme midis tabelave tuaja.

Tani çdo rritje e bazës së të dhënave do të përditësojë automatikisht kodin e klientit. Imagjinoni për shembull:

ALTER TABLE book RENAME COLUMN title TO book_title;

Do të dëshironit vërtet ta bëni këtë punë dy herë? Në asnjë rast. Ne thjesht kryejmë DDL-në, e drejtojmë atë përmes tubacionit tuaj të ndërtimit dhe marrim entitetin e përditësuar:

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

Ose klasa e përditësuar jOOQ. Shumica e ndryshimeve të DDL ndikojnë gjithashtu në semantikën, jo vetëm në sintaksë. Prandaj, mund të jetë e përshtatshme të shihet në kodin e përpiluar se cili kod do të (ose mund të ndikohet) nga rritja e bazës së të dhënave tuaja.

E vetmja e vertete

Pavarësisht se cilën teknologji përdorni, ekziston gjithmonë një model që është burimi i vetëm i së vërtetës për ndonjë nënsistem - ose të paktën ne duhet të përpiqemi për këtë dhe të shmangim konfuzionin e ndërmarrjes ku "e vërteta" është kudo dhe askund menjëherë. Gjithçka mund të jetë shumë më e lehtë. Nëse thjesht po shkëmbeni skedarë XML me ndonjë sistem tjetër, thjesht përdorni XSD. Hidhini një sy meta-modelit INFORMATION_SCHEMA të jOOQ në formë XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD kuptohet mirë
  • XSD shënon shumë mirë përmbajtjen XML dhe lejon vërtetimin në të gjitha gjuhët e klientit
  • XSD është i versionuar mirë dhe shumë i pajtueshëm me prapavijën
  • XSD mund të përkthehet në kodin Java duke përdorur XJC

Pika e fundit është e rëndësishme. Kur komunikojmë me një sistem të jashtëm duke përdorur mesazhe XML, duam të jemi të sigurt që mesazhet tona janë të vlefshme. Kjo është shumë e lehtë për t'u arritur me JAXB, XJC dhe XSD. Do të ishte çmenduri e madhe të mendosh se, në një qasje të projektimit Java-first, ku ne i bëjmë mesazhet tona si objekte Java, ato mund të jepen disi në mënyrë të kuptueshme në XML dhe të dërgohen për konsum në një sistem tjetër. XML i krijuar në këtë mënyrë do të ishte i cilësisë shumë të dobët, i padokumentuar dhe i vështirë për t'u zhvilluar. Nëse do të kishte një marrëveshje për nivelin e cilësisë së shërbimit (SLA) për një ndërfaqe të tillë, ne do ta prishnim menjëherë.

Për të qenë i sinqertë, kjo është pikërisht ajo që ndodh gjatë gjithë kohës me JSON API, por kjo është një histori tjetër, unë do të argumentoj herën tjetër ...

Bazat e të dhënave: ato janë të njëjta

Duke punuar me bazat e të dhënave, ju e kuptoni se ato janë në thelb të njëjta. Baza e të dhënave zotëron të dhënat e saj dhe duhet të menaxhojë skemën. Çdo modifikim i bërë në skemë duhet të zbatohet drejtpërdrejt në DDL në mënyrë që burimi i vetëm i së vërtetës të përditësohet.

Kur ka ndodhur përditësimi i burimit, të gjithë klientët duhet gjithashtu të përditësojnë kopjet e tyre të modelit. Disa klientë mund të shkruhen në Java duke përdorur jOOQ dhe Hibernate ose JDBC (ose të dyja). Klientë të tjerë mund të shkruhen në Perl (le t'i urojmë fat), të tjerët në C#. Nuk ka rëndësi. Modeli kryesor është në bazën e të dhënave. Modelet e gjeneruara nga ORM janë zakonisht të cilësisë së dobët, të dokumentuara dobët dhe të vështira për t'u zhvilluar.

Pra, mos bëni gabime. Mos bëni gabime që në fillim. Punoni nga një bazë të dhënash. Ndërtoni një tubacion vendosjeje që mund të automatizohet. Aktivizo gjeneruesit e kodeve që të kopjojnë me lehtësi modelin tuaj të bazës së të dhënave dhe ta hedhin atë te klientët. Dhe mos u shqetësoni për gjeneruesit e kodit. Ata janë të mirë. Me ta do të bëheni më produktivë. E tëra çfarë ju duhet të bëni është të shpenzoni pak kohë për t'i vendosur ato që në fillim dhe do të keni vite të tëra performancë të përmirësuar për të ndërtuar historinë e projektit tuaj.

Mos më falënderoni akoma, më vonë.

Sqarim

Për të qenë të qartë: Ky artikull nuk mbron në asnjë mënyrë që i gjithë sistemi (d.m.th., domeni, logjika e biznesit, etj., etj.) duhet të përkulet për t'iu përshtatur modelit tuaj të bazës së të dhënave. Ajo për të cilën po flas në këtë artikull është se kodi i klientit që ndërvepron me një bazë të dhënash duhet të veprojë në bazë të modelit të bazës së të dhënave në mënyrë që të mos riprodhojë modelin e bazës së të dhënave në statusin e "klasit të parë". Një logjikë e tillë zakonisht gjendet në shtresën e aksesit të të dhënave në klientin tuaj.

Në arkitekturat me dy nivele, të cilat ende ruhen në disa vende, një model i tillë sistemi mund të jetë i vetmi i mundshëm. Megjithatë, në shumicën e sistemeve, shtresa e aksesit të të dhënave më duket se është një "nënsistem" që përmbledh modelin e bazës së të dhënave.

përjashtime

Ka përjashtime nga çdo rregull, dhe unë e kam thënë më parë se qasja e parë e bazës së të dhënave dhe gjenerimi i kodit burim mund të jenë ndonjëherë të papërshtatshme. Këtu janë disa përjashtime të tilla (ndoshta ka të tjera):

  • Kur skema është e panjohur dhe duhet hapur. Për shembull, ju ofroni një mjet për të ndihmuar përdoruesit të lundrojnë në çdo diagram. Phew. Këtu nuk ka gjenerim kodesh. Por ende - baza e të dhënave para së gjithash.
  • Kur një qark duhet të krijohet në fluturim për të zgjidhur ndonjë problem. Ky shembull duket të jetë një version paksa i butë i modelit vlera e atributit të entitetit, d.m.th., ju nuk keni vërtet një skemë të mirëpërcaktuar. Në këtë rast, shpesh nuk mund të jeni fare i sigurt se një RDBMS do t'ju përshtatet.

Përjashtimet janë nga natyra të jashtëzakonshme. Në shumicën e rasteve që përfshijnë përdorimin e RDBMS, skema është e njohur paraprakisht, ajo është brenda RDBMS dhe është burimi i vetëm i "të vërtetës" dhe të gjithë klientët duhet të marrin kopje që rrjedhin prej saj. Në mënyrë ideale, kjo duhet të përfshijë një gjenerator kodi.

Burimi: www.habr.com

Shto një koment