Hallo, Habr!
We blijven het onderwerp verkennen en , inclusief op het niveau van databases. Vandaag delen we waarom bij het ontwerpen van grote applicaties de database-structuur, en niet de Java-code, bepalend moet zijn, hoe dat gedaan wordt en welke uitzonderingen er op deze regel zijn.
In dit vrij laat artikel leg ik uit waarom ik geloof dat in vrijwel alle gevallen het datamodel in een applicatie 'vanuit de database' moet worden ontworpen en niet 'vanuit de mogelijkheden van Java' (of een andere clients taal waarmee je werkt). Door de tweede aanpak te kiezen, begin je aan een lange weg vol pijn en lijden zodra je project begint te groeien.
Het artikel is geïnspireerd door , gesteld op Stack Overflow.
Interessante discussies op reddit in de secties en .
Code generatie
Ik was erg verrast dat er zo'n kleine groep gebruikers is die, na kennisgemaakt te hebben met jOOQ, zich verontwaardigt over het feit dat jOOQ serieus afhankelijk is van source code generatie. Niemand houdt je tegen om jOOQ te gebruiken zoals je dat wilt, noch verplicht je om code generatie te gebruiken. Maar standaard (zoals beschreven in de handleiding) werkt het met jOOQ als volgt: je begint met een (geërfde) database schema, voert reverse engineering uit met behulp van de jOOQ code generator om zo een set klassen te verkrijgen die jouw tabellen vertegenwoordigen, en vervolgens schrijf je type-veilige queries naar deze tabellen:
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Type-informatie is afgeleid van
// de gegenereerde code waarnaar wordt verwezen in de onderstaande
// SELECT-voorwaarde
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ gegenereerde namen
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}De code wordt ofwel handmatig buiten de build gegenereerd, of handmatig bij elke build. Bijvoorbeeld, een dergelijke regeneratie kan volgen direct na .
Code generatie
Met dergelijke benaderingen van codegeneratie – handmatig en automatisch – zijn er verschillende filosofieën, voordelen en nadelen verbonden, die ik in dit artikel niet in detail wil bespreken. Maar over het algemeen draait alles om de gegenereerde code die ons in staat stelt om de "waarheid" die we als gegeven beschouwen, weer te geven in Java, hetzij binnen ons systeem, hetzij daarbuiten. In zekere zin doen compilers hetzelfde door bytecode, machinecode of een andere vorm van code te genereren op basis van de brondocumenten – we krijgen een representatie van onze "waarheid" in een andere taal, ongeacht de specifieke redenen.
Er zijn veel van zulke codegenerators. Bijvoorbeeld, . Het principe is altijd hetzelfde:
- Er is een bepaalde waarheid (intern of extern) – bijvoorbeeld, specificaties, datamodellen, enz.
- We hebben een lokale representatie van deze waarheid nodig in onze programmeertaal.
Het is vaak zinnig om zo'n representatie te genereren – om overbodigheid te vermijden.
Typeproviders en annotatieverwerking
Ter informatie: een andere, modernere en specifiekere benadering van codegeneratie voor jOOQ is gerelateerd aan het gebruik van typeproviders, . In dat geval wordt de code gegenereerd door de compiler, daadwerkelijk tijdens de compilatiefase. In de vorm van brondocumenten bestaat deze code in principe niet. In Java zijn er vergelijkbare, hoewel minder elegante tools – dat zijn annotatieprocessors, bijvoorbeeld, .
In zekere zin gebeurt hier hetzelfde als in het eerste geval, met uitzondering van:
- U ziet de gegenereerde code niet (misschien lijkt dit voor sommigen niet zo afschrikwekkend?)
- U moet ervoor zorgen dat de types ter beschikking kunnen worden gesteld, dat wil zeggen, de "waarheid" moet altijd toegankelijk zijn. Dit is eenvoudig in het geval van Lombok, dat de “waarheid” annoteert. Het is iets ingewikkelder met databasemodellen, waarvan het functioneren afhankelijk is van een constant beschikbare actieve verbinding.
Wat is het probleem met codegeneratie?
Naast de slimme vraag over hoe je de codegeneratie het beste kunt starten – handmatig of automatisch – moet ik ook vermelden dat er mensen zijn die denken dat codegeneratie helemaal niet nodig is. De meest gehoorde rechtvaardiging voor dit standpunt is dat het dan moeilijk is om de build-pijplijn in te stellen. Dat klopt, dat is inderdaad moeilijk. Er komen extra infrastructuurkosten bij kijken. Als je net begint met een bepaald product (of het nu jOOQ, JAXB, Hibernate, etc. is), kost het opzetten van de werkomgeving tijd die je liever aan het leren van de API besteedt, zodat je er later waarde uit kunt halen.
Als de kosten om de werking van de generator te begrijpen te hoog zijn, dan hebben ze inderdaad niet goed aan de gebruiksvriendelijkheid van de codegenerator gewerkt (en blijkt later dat zelfs de gebruikersinstellingen moeilijk zijn). Gebruiksgemak moet de hoogste prioriteit hebben voor elke API. Maar dit is slechts één argument tegen codegeneratie. Daarnaast moet je het lokale representatie van interne of externe waarheden volledig handmatig schrijven.
Veel mensen zullen zeggen dat ze geen tijd hebben om zich hiermee bezig te houden. Hun deadlines voor hun Superproduct zijn in gevaar. Later passen we de build-pijplijnen wel aan, dat komt wel goed. Ik antwoord hen:

,
Maar in Hibernate / JPA is het zo eenvoudig om code "voor Java" te schrijven.
Inderdaad. Voor Hibernate en zijn gebruikers is dat zowel een zegen als een vloek. In Hibernate kun je eenvoudig een paar entiteiten schrijven, zo:
@Entity
class Book {
@Id
int id;
String title;
}En bijna alles is klaar. Nu is het aan Hibernate om de ingewikkelde "details" te genereren van hoe precies deze entiteit gedefinieerd zal worden in de DDL van jouw SQL-dialect:
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);... en we beginnen de applicatie te draaien. Echt een geweldige mogelijkheid om snel aan de slag te gaan en verschillende dingen uit te proberen.
Maar wacht even. Ik heb gelogen.
- Zal Hibernate echt de definitie van deze benoemde primaire sleutel toepassen?
- Zal Hibernate een index in TITLE maken? – ik weet zeker dat we dat nodig zullen hebben.
- Zal Hibernate deze sleutel definiëren in de Identity Specificatie?
Waarschijnlijk niet. Als je je project vanaf nul ontwerpt, is het altijd handig om de oude database gewoon weg te gooien en een nieuwe te genereren zodra je de benodigde annotaties hebt toegevoegd. Zo zal de entiteit Boek er uiteindelijk uitzien als:
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
Cool. Opnieuw genereren. Nogmaals, in dat geval zal het vanaf het begin erg eenvoudig zijn.
Maar later moet je ervoor betalen.
Vroeg of laat moet je in productie gaan. Pas dan zal zo'n model niet meer werken. Omdat:
In productie kun je de oude database niet meer weggooien en opnieuw beginnen. Jouw database wordt geërfd.
Voortaan zul je moeten schrijven . Maar wat gebeurt er in dat geval met jouw entiteiten? Je kunt ze handmatig aanpassen (en zo de werkbelasting verdubbelen), of je kunt Hibernate vragen ze opnieuw voor je te genereren (hoe groot is de kans dat hetgeen zo gegenereerd is, aan jouw verwachtingen voldoet?) Je verliest hoe dan ook.
Dus, zodra je in productie gaat, heb je hotfixes nodig. En die moeten heel snel in productie worden gebracht. Omdat je je niet hebt voorbereid en geen soepele doorvoer van jouw migraties voor productie hebt georganiseerd, zul je alles op een chaotische manier patchen. En dan heb je niet de tijd om alles goed te doen. En je verwijt Hibernate, omdat altijd iedereen schuldig is, behalve jij...
In plaats daarvan had je vanaf het begin alles heel anders kunnen aanpakken. Bijvoorbeeld, ronde wielen op de fiets zetten.
Eerst de database.
De echte "waarheid" in de structuur van jouw database en de "soevereiniteit" daarover ligt binnen de database. De structuur wordt alleen binnen de database gedefinieerd en nergens anders, en elke klant heeft een kopie van die structuur, dus het is volkomen logisch om de naleving van de structuur en integriteit ervan af te dwingen, dit direct in de database te doen - daar waar de informatie wordt opgeslagen.
Dit is oude, zelfs ingezette wijsheid. Primaire en unieke sleutels zijn goed. Buitenlandse sleutels zijn goed. Beperkingen controleren is goed. – zijn goed.
En dit is nog niet alles. Bijvoorbeeld, als je Oracle gebruikt, wil je waarschijnlijk aangeven:
- In welke tablespace je tabel zich bevindt
- Wat de PCTFREE waarde is
- Wat de grootte van de cache is in jouw sequentie (achter de identificatie)
Dit alles lijkt misschien niet relevant voor kleine systemen, maar je hoeft niet te wachten tot je in het gebied van 'big data' komt — je kunt al veel eerder profiteren van de optimalisaties voor gegevensopslag die aanbieders bieden, zoals hierboven genoemd. Geen enkele ORM die ik heb gezien (inclusief jOOQ) biedt toegang tot de volledige set DDL-opties die je mogelijk wilt gebruiken in je database. ORM's bieden enkele tools die helpen bij het schrijven van DDL.
Maar uiteindelijk is een goed ontworpen schema handmatig geschreven in DDL. Elke gegenereerde DDL is slechts een benadering van het.
Wat betreft het klantmodel?
Zoals hierboven vermeld, heb je aan de klantzijde een kopie van je databaseschema nodig, het klantview. Het is overbodig te vermelden dat dit klantview gesynchroniseerd moet zijn met het daadwerkelijke model. Hoe kun je dit het beste bereiken? Met behulp van een codegenerator.
Alle databases bieden hun metadata via SQL. Hier is hoe je vanuit je database alle tabellen kunt verkrijgen in verschillende SQL-dialekten:
-- 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
Deze queries (of soortgelijke, afhankelijk van of ook views, gematerialiseerde views, of functies met tabelwaarden in aanmerking moeten worden genomen) worden ook uitgevoerd met behulp van de aanroep van JDBC, of met de jOOQ meta-module.
Vanuit de resultaten van dergelijke queries is het relatief eenvoudig om elk klantview model van jouw database te genereren, ongeacht welke technologie je aan de klantzijde gebruikt.
- Als je JDBC of Spring gebruikt, kun je een set stringconstanten aanmaken
- Als je JPA gebruikt, kun je de entiteiten zelf genereren
- Als je jOOQ gebruikt, kun je het jOOQ meta-model genereren
Afhankelijk van de mogelijkheden die uw client API biedt (bijvoorbeeld jOOQ of JPA), kan het gegenereerde meta-model echt rijk en volledig zijn. Neem bijvoorbeeld de mogelijkheid van impliciete joins, , die is gebaseerd op de gegenereerde metadata over de relaties van vreemde sleutels die tussen uw tabellen bestaan.
Nu zal elke wijziging in de database automatisch leiden tot een update van de client code. Stel je bijvoorbeeld voor:
ALTER TABLE book RENAME COLUMN title TO book_title;Wilt u dit echt twee keer doen? Zeker niet. We leggen gewoon de DDL vast, draaien deze door uw build-pijplijn en krijgen de bijgewerkte entiteit:
@Entity
@Table(name = "book", indexes = {
// Heeft u hier ooit aan gedacht?
@Index(name = "i_book_title", columnList = "book_title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
@Column("book_title")
String bookTitle;
}Of de bijgewerkte jOOQ klasse. De meeste DDL-wijzigingen weerspiegelen zich ook in de semantiek, niet alleen in de syntaxis. Daarom kan het handig zijn om in de gecompileerde code te kijken welke code zal (of kan worden) aangetast door een wijziging in uw database.
De enige waarheid
Ongeacht welke technologie u gebruikt, er is altijd één model dat de enige bron van waarheid is voor een bepaalde subsysteem – of, op zijn minst, we moeten ernaar streven en die enterprise-chaos vermijden waarin 'waarheid' overal en nergens tegelijk is. Alles kan veel eenvoudiger zijn. Als u alleen XML-bestanden uitwisselt met een ander systeem, gebruik dan gewoon XSD. Kijk naar het INFORMATION_SCHEMA meta-model van jOOQ in XML-vorm:
- XSD is goed begrijpbaar
- XSD markeert de XML-inhoud heel goed en maakt validatie in alle clienttalen mogelijk
- XSD is goed versiebeheerbaar en heeft een goed uitgewerkte achterwaartse compatibiliteit
- XSD kan worden getransformeerd naar Java-code met behulp van XJC
Het laatste punt is belangrijk. Bij communicatie met een extern systeem via XML-berichten willen we er zeker van zijn dat onze berichten geldig zijn. Dit is heel eenvoudig te bereiken met JAXB, XJC en XSD. Het zou volslagen waanzin zijn om te verwachten dat, bij een 'Java-first' ontwerpmethode waarbij we onze berichten als Java-objecten maken, deze op een duidelijke manier in XML konden worden weergegeven en verzonden voor consumptie door een ander systeem. XML die op deze manier is gegenereerd, zou van zeer slechte kwaliteit zijn, niet gedocumenteerd en moeilijk te onderhouden. Als er een service level agreement (SLA) voor zo'n interface zou zijn, zouden we het meteen verpesten.
Eerlijk gezegd gebeurt dit constant met JSON-API's, maar dat is een ander verhaal, daar zal ik de volgende keer tegen klagen...
Databases: ze zijn hetzelfde.
Wanneer je met databases werkt, begrijp je dat ze in principe allemaal vergelijkbaar zijn. Een database bezit zijn eigen gegevens en moet het schema beheren. Alle aanpassingen aan het schema moeten rechtstreeks in de DDL worden doorgevoerd, zodat de enige bron van waarheid wordt bijgewerkt.
Wanneer de update van de bron heeft plaatsgevonden, moeten alle clients ook hun kopieën van het model bijwerken. Sommige clients kunnen in Java zijn geschreven met behulp van jOOQ en Hibernate of JDBC (of allemaal tegelijk). Andere clients kunnen in Perl zijn geschreven (vergeet ze niet veel succes te wensen), en weer anderen in C#. Dat doet er niet toe. Het belangrijkste model bevindt zich in de database. Modellen die zijn gegenereerd met ORM hebben meestal een slechte kwaliteit, zijn slecht gedocumenteerd en moeilijk te onderhouden.
Dus maak geen fouten. Maak vanaf het begin geen fouten. Werk vanuit de database. Bouw een deployment-pijplijn die kan worden geautomatiseerd. Voeg codegeneratoren toe om het gemakkelijk te maken om het model van je database te kopiëren en naar clients te verzenden. En stop met je zorgen te maken over codegeneratoren. Ze zijn goed. Je wordt productiever met hen. Je moet alleen in het begin wat tijd besteden aan de configuratie – en daarna wachten je jaren van verhoogde productiviteit, waarin het verhaal van je project zich zal ontvouwen.
Bedankt nog niet, later.
Uitleg
Ter verduidelijking: Dit artikel promoot absoluut niet dat je het hele systeem (d.w.z. het domein, de bedrijfslogica, enz.) moet buigen naar het model van je database. In dit artikel bespreek ik dat de klantcode die met de database interageert, moet handelen op basis van het database-model, zodat het model zelf niet in de status van 'eerste klas' in de code wordt gereproduceerd. Deze logica bevindt zich doorgaans op het datatoegangsniveau aan jouw kant.
In tweelaagse architecturen, die nog steeds op sommige plaatsen voorkomen, kan zo'n systeemmodel de enige mogelijkheid zijn. Echter, in de meeste systemen lijkt het datatoegangs-niveau voor mij een 'sub-systeem' te zijn, dat het database-model encapsuleert.
Uitzonderingen
Van elke regel zijn er uitzonderingen, en ik heb al gezegd dat de benadering met databasemanifestatie en het genereren van broncode soms ongepast kan zijn. Hier zijn een paar van die uitzonderingen (waarschijnlijk zijn er er meer):
- Wanneer het schema onbekend is en geopend moet worden. Bijvoorbeeld, je bent een leverancier van een tool die gebruikers helpt bij elke schema. Pff. Dit kan zonder code-generatie. Maar toch – de database heeft voorrang.
- Wanneer het schema on-the-fly moet worden gegenereerd voor het oplossen van een bepaalde taak. Dit voorbeeld lijkt een licht extravagante versie van het patroon , dat wil zeggen, je hebt werkelijk geen duidelijk gedefinieerd schema. In dit geval is het vaak zelfs onmogelijk om zeker te zijn dat een RDBMS voor jou geschikt is.
Uitzonderingen zijn van nature uitzonderlijk. In de meeste gevallen die verband houden met het gebruik van RDBMS is het schema van tevoren bekend, bevindt het zich binnen de RDBMS en is het de enige bron van 'waarheid', terwijl alle klanten moeten beschikken over afgeleiden kopieën. Idealiter moet daarbij een code-generator worden ingeschakeld.
Bron: habr.com
