Përshëndetje, Habr!
Ne vazhdojmë të eksplorojmë temën dhe , duke përfshirë, në nivelin e bazave të të dhënave. Sot ofrojmë të lexoni përse, kur projektoni aplikacione të mëdha, struktura e bazës së të dhënave duhet të ketë rëndesi vendimtare, si funksionon kjo dhe cilat janë përjashtimet nga ky rregull.
Në këtë artikull mjaft të vonuar, do të shpjegoj se përse e besoj se në pothuajse të gjitha rastet modeli i të dhënave në një aplikacion duhet të projektohet “nga baza e të dhënave”, jo “nga mundësitë e Java-s” (ose ndonjë gjuhe tjetër klienti me të cilën punoni). Duke zgjedhur qasjen e dytë, ju hyni në një rrugë të gjatë dhimbjeje dhe vuajtjeje, sapo projekti juaj fillon të rritet.
Artikulli është shkruar duke u mbështetur në , e cila u bë në Stack Overflow.
Diskutime interesante në reddit në seksionet dhe .
Gjenerimi i kodit
Sa shumë u befasova që ekziston një grup i vogël përdoruesish, të cilët, pasi u njohën me jOOQ, janë të habitur nga fakti se kur punoni me jOOQ, besoheni seriozisht në gjenerimin e kodit burimor. Askush nuk ju pengon ta përdorni jOOQ ashtu siç e konsideroni të nevojshme, dhe askush nuk ju detyron të përdorni gjenerimin e kodit. Por, si parazgjedhje (ashtu siç përshkruhet në manual), puna me jOOQ ndodh kështu: filloni me (ndërprerjen) e skemës së bazës së të dhënave, bëni seksionin e saj si pasqyrë me ndihmën e gjeneratorit të kodit jOOQ, në mënyrë që të merrni një grup klasash që përfaqësojnë tabelat tuaja, dhe pastaj shkruani pyetje të sigurta për këto tabela:
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Informacioni për llojet është nxjerrë
// mbi bazën e kodit të gjeneruar, në të cilin i referohet kushti
// SELECT
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ emrat e gjeneruar
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}Kodi gjenerohet ose manualisht jashtë ndërtimit, ose manualisht gjatë çdo ndërtimi. Për shembull, një rregjenerim i tillë mund të ndodhë menjëherë pas .
Gjenerimi i kodit burimor
Me këto qasje për gjenerimin e kodit – manuale dhe automatike – lidhen filozofi të ndryshme, përparësi dhe disavantazhe, të cilat nuk kam ndërmend t'i diskutoj në detaje në këtë artikull. Por, për përgjithësi, thelbi i kodit të gjeneruar është se ai lejon riprodhimin në Java të “të vërtetës” që e pranojmë si të dhënë, qoftë brenda sistemit tonë, apo jashtë saj. Në një farë kuptimi, të njëjtën gjë bëjnë kompilatorët, që gjenerojnë kod bajt, kod makine ose ndonjë lloj tjetër kodi mbi bazën e kodit burimor – marrim një përfaqësim të “të vërtetës” sonë në një gjuhë tjetër, pavarësisht nga arsyet specifike.
Ekzistojnë shumë gjenerues të tillë të kodit. Për shembull, Parimi është gjithmonë i njëjtë:
- Ekziston një e vërtetë e caktuar (brenda apo jashtë) – për shembull, specifikimi, modeli i të dhënave, etj.
- Na nevojitet një përfaqësim lokal i kësaj të vërtete në gjuhën tonë të programimit.
Dhe është gjithmonë e arsyeshme të gjenerohet një përfaqësim i tillë – për të shmangur përsëritjen.
Ofruesit e llojeve dhe përpunimi i annotimeve
Për t'u mbajtur mend: një qasje më moderne dhe specifike për gjenerimin e kodit për jOOQ është e lidhur me përdorimin e ofruesve të llojeve, Në këtë rast, kodi gjenerohet nga kompilator, në fakt në fazën e kompilimit. Në formën e burimorëve, ky kod nuk ekziston në parim. Në Java ekzistojnë mjete të ngjashme, edhe pse ndoshta jo aq elegante – ato janë procesorët e annotimeve. për shembull, .
Në një farë kuptimi, këtu ndodhin të njëjtat gjëra si në rastin e parë, përveçse:
- Nuk e shihni kodin e gjeneruar (ndoshta kjo situatë dikujt i dukej jo aq e frikshme?)
- Duhet të garantoni që llojet mund të ofrohen, dmth, “e vërteta” duhet të jetë gjithmonë e qasshme. Kjo është e lehtë në rastin e Lombok, i cili annoton “të vërtetën”. Më e vështirë me modelet e bazave të të dhënave, puna e të cilave varet nga një lidhje aktive dhe gjithmonë e disponueshme.
Cila është problemi me gjenerimin e kodit?
Përveç pyetjes se si është më mirë të nisni gjenerimin e kodit – manualisht apo automatikisht, duhet të përmendim edhe ata që besojnë se gjenerimi i kodit nuk është i nevojshëm fare. Justifikimi më i zakonshëm për këtë pikëpamje është se e bën të vështirë konfigurimin e linjës së ndërtimit. Po, është me të vërtetë e vështirë. Kemi kosto të shtesë infrastrukturore. Nëse sapo keni filluar të punoni me një produkt të caktuar (qoftë jOOQ, JAXB, Hibernate, etj.), ju nevojitet kohë për të vendosur mjedisin e punës, kohë që do të dëshironit ta shpenzonit për të mësuar API-në, për të nxjerrë pastaj vlerë prej saj.
Nëse kostot për të kuptuar funksionimin e gjeneratorit janë tepër të mëdha – atëherë, me të vërtetë, API-ja ka bërë punë të keqe në përdorshmërinë e gjeneratorit të kodit (ndërsa më vonë ndodh që edhe konfigurimi i përdoruesit në të është i vështirë). Lehtësia e përdorimit duhet të jetë prioriteti më i lartë për çdo API të tillë. Por kjo është vetëm një argument kundër gjenerimit të kodit. Pjesa tjetër është se absolutisht do të duhet ta shkruani manualisht përfaqësimin lokal të së vërtetës së brendshme ose të jashtme.
Shumë do të thonë se nuk kanë kohë për të gjithë këto punë. Ata kanë afate të ngushta në Super-Produktin e tyre. Ndonjëherë më vonë do t’i përmirësojmë linjat e ndërtimit, do të na dalë. Atëherë do t'u përgjigjem:

,
Por në Hibernate/JPA është kaq e lehtë të shkruash kod "për Java".
E vërtetë. Për Hibernate dhe përdoruesit e tij, kjo është njëherësh një bekim dhe një mallkim. Në Hibernate mund të shkruash lehtësisht disa entitete, kështu:
@Entity
class Book {
@Id
int id;
String title;
}Dhe pothuajse gjithçka është gati. Tani, fokusi i Hibernate është të gjenerojë detaje të komplikuara se si do të përcaktohet kjo entitet në DDL të SQL “dialektit” tuaj:
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 fillojmë të ekzekutojmë aplikacionin. Një mundësi vërtet e shkëlqyer për të filluar shpejt dhe për të provuar gjëra të ndryshme.
Megjithatë, lejoni. Kam gënjyer.
- A do të aplikojë vërtet Hibernate përcaktimin e këtij çelësi primar të emëruar?
- A do të krijojë Hibernate indeksin në TITLE? – unë e di saktësisht, do ta kemi nevojë.
- A do ta bëjë Hibernate këtë çelës identifikues në Specifikimin Identity?
Me siguri se jo. Nëse po zhvilloni projektin tuaj nga zero, është gjithmonë e lehtë të hiqni bazën e të dhënave të vjeter dhe të gjeneroni një të re, sa herë që të shtoni annotimet e nevojshme. Kështu, entiteti Book në fund do të marrë këtë pamje:
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
Shkëlqyer. Riperitni nga e para. Prapë, në një rast të tillë do të jetë shumë e lehtë në fillim.
Por më vonë do të duhet të paguani për këtë.
Herët ose vonë do të duhet të kaloni në prodhim. Pikërisht atëherë një model i tillë do të ndalojë së funksionuari. Sepse:
Në prodhim nuk do të jeni në gjendje të hiqni bazën e të dhënave të vjeter dhe të filloni gjithçka nga e para. Baza juaj e të dhënave do të bëhet e trashëguar.
Që nga tani e tutje do të duhet të shkruani . E çfarë do të ndodhë në këtë rast me entitetet tuaja? Ju do të mund të adaptoni ato manualisht (dhe kështu do të dyfishoni punën tuaj), ose do t'i thoni Hibernate të gjenerojë përsëri ato për ju (sa të mëdha janë shanset që kjo gjeneratë të përputhet me pritshmëritë tuaja?) Në çdo rast, ju humbni.
Prandaj, sapo të kaloni në prodhim, do t'ju nevojiten patches të nxehta. Dhe ato duhet të ndodhin shpejt në prodhim. Përveçse nuk jeni përgatitur dhe nuk keni organizuar një rrjedhë të qetë të migrimeve tuaja për prodhim, do të jeni të ngarkuar me patching. Dhe pastaj do të filloni të mos arrini të bëni gjithçka siç duhet. Dhe do ta fajësoni Hibernate, sepse gjithmonë është fajtor kushdo tjetër, përveç jush…
Në vend të kësaj, që nga fillimi mund ta bëni gjithçka ndryshe. Për shembull, të vendosni goma të rrethit në biçikletë.
Fillimisht baza e të dhënave
E vërteta e vërtetë e skemës së bazës suaj të të dhënave dhe "sovraniteti" mbi të qëndron brenda bazës së të dhënave. Skema përcaktohet vetëm brenda bazës së të dhënave dhe asnjëvend tjetër, dhe çdo klient ka një kopje të kësaj skeme, kështu që është krejtësisht e arsyeshme të insistoni në respektimin e skemës dhe integritetit të saj, ta bëni këtë në vetë bazën e të dhënave – aty ku ruhet informacioni.
Kjo është një mençuri e vjetër dhe e përdorur. Çelësat primarë dhe unikë janë të mirë. Çelësat e jashtëm – të mirë. Kontrolli i kufizimeve – i mirë. – të mirë.
Për më tepër, kjo nuk është gjithçka. Për shembull, duke përdorur Oracle, ndoshta do të dëshironit të specifikoni:
- Në cilin hapësirë tabelash ndodhet tabela juaj
- Cila është vlera e saj PCTFREE
- Cili është madhësia e caches në sekuencën tuaj (pas identifikuesit)
Mund të mos jetë gjithçka kaq e rëndësishme në sistemet e vogla, por nuk është e nevojshme të prisni kalimin në fushën e "të dhënave të mëdha" — mund të filloni të shfrytëzoni përfitimet e optimizimeve të ruajtjes së të dhënave që ofrohen nga ofruesi shumë më herët, si ato të përmendura më sipër. Asnjë nga ORM-të që kam parë (përfshirë jOOQ) nuk ofron qasje në të gjitha opsionet DDL që mund të dëshironi të përdorni në bazën tuaj të të dhënave. ORM-të ofrojnë disa mjete që ndihmojnë në shkruarjen e DDL.
Por, në fund të fundit, një skemë e mirë e dizajnuar është shkruar manualisht në DDL. Çdo DDL i gjeneruar është vetëm një aproximim i saj.
Çfarë ndodh me modelin e klientit?
Siç u përmend më sipër, në klient do t'ju nevojitet një kopje e skemës së bazës suaj të të dhënave, një paraqitje klienti. Nuk është e nevojshme të përmendet që kjo paraqitje klienti duhet të jetë e sinkronizuar me modelin e vërtetë. Si është mënyra më e mirë për ta arritur atë? Nëpërmjet një gjeneruesi kod.
Të gjitha bazat e të dhënave ofrojnë metainformacionin e tyre përmes SQL. Ja se si të merrni të gjitha tabelat nga baza juaj e të dhënave në dialekte të ndryshme SQL:
-- 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, varësisht nga nëse duhet gjithashtu të llogariten paraqitjet, paraqitjet e materializuara, funksionet me vlerë tabelare) gjithashtu ekzekutohen nëpërmjet thirrjes në JDBC, apo nëpërmjet modulit të metainformacionit jOOQ.
Nga rezultatet e këtyre pyetjeve është mjaft e lehtë të gjeneroni çdo paraqitje klienti të modelit të bazës suaj të të dhënave, pavarësisht nga teknologjia që përdorni në klient.
- Nëse përdorni JDBC ose Spring, mund të krijoni një set konstantash string
- Nëse përdorni JPA, mund të gjeneroni entitetet tuaja
- Nëse përdorni jOOQ, mund të gjeneroni metamodelin e jOOQ
Varësisht nga sasia e mundësive që ofron API juaj i klientit (p.sh. jOOQ ose JPA), metamodeli i gjeneruar mund të jetë me të vërtetë i pasur dhe i plotë. Merrni, për shembull, mundësinë e bashkimeve të implicit që , e cila mbështetet në metainformacionin e gjeneruar për marrëdhëniet e çelësave të jashtëm që veprojnë midis tabelave tuaja.
Tani çdo rritje e bazës së të dhënave do të çojë automatikisht në përditësimin e kodit të klientit. Imagjinoni, për shembull:
ALTER TABLE book RENAME COLUMN title TO book_title;A do të dëshironit vërtet ta bënit këtë punë dy herë? Aspak. Thjesht regjistrojeni DDL-në, kalojeni përmes linjës së ndërtimit dhe merrni entitetin e përditësuar:
@Entity
@Table(name = "book", indexes = {
// A e keni menduar për këtë?
@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ërmirësuar jOOQ. Shumica e ndryshimeve DDL gjithashtu reflektohen në semantikë, jo vetëm në sintaksë. Prandaj, ndonjëherë është e dobishme të shikoni në kodin e kompiluar se cili kod do të (ose mund të) preket nga rritja e bazës suaj të të dhënave.
E vërteta e vetme
Pavarësisht nga teknologjia që përdorni, gjithmonë ka një model që është burimi i vetëm i vërtetë i së vërtetës për një nën-sistem – ose, të paktën, ne duhet të përpiqemi për këtë dhe të shmangim atë ndotjen sipërmarrëse, ku "e vërteta" është menjëherë kudo dhe askund. Gjithçka mund të jetë shumë më e thjeshtë. Nëse thjesht shkëmbeni skedarë XML me ndonjë sistem tjetër, thjesht përdorni XSD. Shikoni metamodelin e INFORMATION_SCHEMA nga jOOQ në formatin XML:
- XSD është e lehtë për tu kuptuar
- XSD e ndan shumë mirë përmbajtjen XML dhe lejon validimin në të gjitha gjuhët e klientit
- XSD ka versionim të shkëlqyer dhe ka një përputhshmëri të zhvilluar me versionet e mëparshme
- XSD mund të testohet në kod Java duke përdorur XJC
Pika e fundit është e rëndësishme. Kur komunikojmë me një sistem të jashtëm përmes mesazheve XML, duam të jemi të sigurt për vlefshmërinë e mesazheve tona. Kjo është shumë e lehtë të arrihet duke përdorur JAXB, XJC dhe XSD. Do të ishte një marrëzi të mendojmë se, me qasjen e dizajnit "Java si prioritet", ku i bëjmë mesazhet tona në formën e objekteve Java, ato do të mund të shndërrohen në XML dhe të dërgohen për konsum në një sistem tjetër. XML i gjeneruar në këtë mënyrë do të ishte me cilësi të dobët, pa dokumentim, dhe e 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 këtë ndërfaqe, do ta kishim sabotuar menjeherë.
Sinqerisht, kjo është gjithmonë ajo që ndodh me API-të në JSON, por kjo është një tjetër histori, herën tjetër do ta kritikoj…
Baza të dhënash: është e njëjta gjë
Duke punuar me baza të dhënash, kupton se të gjitha ato, në thelb, janë të ngjashme. Baza zotëron të dhënat e saj dhe duhet të drejtojë skemën. Çdo modifikim që bëhet në skemë duhet të realizohet drejtpërdrejt në DDL, në mënyrë që të përditësohet burimi i vetëm i së vërtetës.
Kur burimi është përditësuar, të gjitha klientët gjithashtu duhet të përditësojnë kopjet e tyre të modelit. Disa klientë mund të jenë shkruajtur në Java duke përdorur jOOQ dhe Hibernate ose JDBC (ose të gjitha njëherazi). Klientë të tjerë mund të jenë shkruar në Perl (të gjithë vetëm mund t'u urojmë fat), të tjerë – në C#. Nuk ka rëndësi. Modeli kryesor ndodhet në bazën e të dhënave. Modelet e gjeneruara me ORM janë zakonisht të dobëta, pa dokumentim të mirë, dhe të vështira për t'u zhvilluar.
Prandaj, mos bëni gabime. Mos bëni gabime që nga fillimi. Punoni duke u bazuar në bazën e të dhënave. Ndërtoni një pipeline deploye që mund të automatizohet. Nëfshini gjeneratorët e kodit në mënyrë që të ishte e lehtë të kopjonit modelin e bazës suaj të të dhënave dhe ta dërgoni atë te klientët. Dhe ndaloni së shqetësuari për gjeneratorët e kodit. Ata janë të mirë. Me ta do të bëheni më produktivë. Duhet vetëm të shpenzoni pak kohë në fillim për t'i konfiguruar ata – dhe më pas ju presin vite produktiviteti të rritur, nga të cilat do të krijohet historia e projektit tuaj.
Mos më falenderoni tani, më vonë.
Shpjegim
Për qartësi: Ky artikull nuk propagandon aspak që modeli i bazës suaj të dhënash duhet të imponojë gjithë sistemin (dmth., zonën e subjektit, logjikën e biznesit, etj.). Në këtë artikull po flas se kodi i klientit, që interakton me bazën e të dhënave, duhet të veprojë duke u nisur nga modeli i bazës së të dhënave, në mënyrë që në vetë të mos riprodhoj modelin e bazës së të dhënave në statusin e "klasës së parë". Një logjikë e tillë zakonisht ndodhet në nivelin e aksesit në të dhëna nga ana juaj në klient.
Në arkitekturat me dy nivele, që kanë mbetur ende ndonjëherë, një model i tillë sistemi mund të jetë i vetmi i mundshëm. Megjithatë, në shumicën e sistemeve niveli i aksesit në të dhëna më duket si një "nën sistem", që inkapsulon modelin e bazës së të dhënave.
Përjashtimet
Nga çdo rregull ka përjashtime, dhe kam thënë më parë se qasja me prioritet të bazës së të dhënave dhe gjenerimin e kodit mund të duket ndonjëherë e papërshtatshme. Ja disa përjashtime të tilla (me siguri do të ketë edhe të tjera):
- Kur skema është e panjohur, dhe duhet hapur. Për shembull, jeni një ofrues mjeti, që ndihmon përdoruesit të orientohen në çdo skemë. Uf. Këtu nuk ka gjenerim kodi. Por prapëseprapë – baza e të dhënave është e para.
- Kur skema duhet të gjenerohet në flutur për të zgjidhur një detyrë të caktuar. Ky shembull duket si një version paksa i çuditshëm i modelit , dmth., në të vërtetë nuk keni një skemë të qartë të përcaktuar. Në këtë rast shpesh madje nuk është e sigurt se do t'ju përshtatet një RDBMS.
Përjashtimet për natyrën e tyre janë përjashtuese. Në shumicën e rasteve që lidhen me përdorimin e RDBMS, skema është e njohur paraprakisht, ajo ndodhet brenda RDBMS dhe është burimi i vetëm i "të vërtetës", dhe të gjithë klientët duhet të fitojnë kopje të saj, të nxjerra prej saj. Në mënyrë ideale, në këtë rast duhet përdorur një gjenerator kodi.
Burimi: habr.com
