De waarheid eerst, of waarom het systeem moet worden ontworpen op basis van de databasestructuur

Hé Habr!

We blijven het onderwerp verkennen Java и veerook op databaseniveau. Vandaag bieden we aan om te lezen waarom bij het ontwerpen van grote applicaties de databasestructuur en niet de Java-code van doorslaggevend belang moet zijn, hoe dit wordt gedaan en wat de uitzonderingen op deze regel zijn.

In dit nogal late artikel zal ik uitleggen waarom ik denk dat in bijna alle gevallen het datamodel in een applicatie moet worden ontworpen "vanuit de database" in plaats van "vanuit de mogelijkheden van Java" (of welke clienttaal je ook gebruikt). werken met). Door de tweede benadering te kiezen, betreedt u een lange weg van pijn en lijden zodra uw project begint te groeien.

Het artikel is geschreven op basis van een vraag, gegeven op Stack Overflow.

Interessante discussies over reddit in secties /r/java и / r / programmeren.

Code generatie

Wat ben ik verrast dat er zo'n kleine laag gebruikers is die, na kennis te hebben gemaakt met jOOQ, het kwalijk neemt dat jOOQ serieus afhankelijk is van het genereren van broncode om te draaien. Niemand houdt u tegen om jOOQ te gebruiken zoals u wilt, en niemand dwingt u om codegeneratie te gebruiken. Maar standaard (zoals beschreven in de handleiding) werkt jOOQ als volgt: u begint met een (verouderd) databaseschema, reverse-engineering met de jOOQ-codegenerator om een ​​set klassen te krijgen die uw tabellen vertegenwoordigen, en schrijft vervolgens type- veilige query's tegen deze tabellen:

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

De code wordt handmatig buiten de build gegenereerd of handmatig bij elke build. Een dergelijke regeneratie kan bijvoorbeeld direct daarna volgen Flyway-databasemigratie, die ook handmatig of automatisch kan worden uitgevoerd.

Genereren van broncode

Er zijn verschillende filosofieën, voor- en nadelen verbonden aan deze benaderingen van het genereren van code - handmatig en automatisch - die ik in dit artikel niet in detail ga bespreken. Maar over het algemeen is het hele punt van de gegenereerde code dat u hiermee in Java de "waarheid" kunt reproduceren die wij als vanzelfsprekend beschouwen, hetzij binnen ons systeem, hetzij daarbuiten. In zekere zin doen compilers die bytecode, machinecode of een ander soort code uit de broncode genereren hetzelfde: we krijgen een weergave van onze "waarheid" in een andere taal, ongeacht specifieke redenen.

Er zijn veel van dergelijke codegeneratoren. Bijvoorbeeld, XJC kan Java-code genereren op basis van XSD- of WSDL-bestanden. Het principe is altijd hetzelfde:

  • Er is een waarheid (intern of extern) - bijvoorbeeld een specificatie, een gegevensmodel, enz.
  • We hebben een lokale weergave van deze waarheid nodig in onze programmeertaal.

Bovendien is het bijna altijd aan te raden om zo'n representatie te genereren - om redundantie te voorkomen.

Typ Providers en annotatieverwerking

Opmerking: een andere, modernere en specifiekere benadering voor het genereren van code voor jOOQ omvat het gebruik van typeproviders, zoals ze zijn geïmplementeerd in F#. In dit geval wordt de code gegenereerd door de compiler, eigenlijk in de compilatiefase. Dergelijke code bestaat in principe niet in de vorm van broncodes. In Java zijn er vergelijkbare, hoewel niet zo elegante tools - dit zijn bijvoorbeeld annotatieprocessors Lombok.

In zekere zin gebeuren hier dezelfde dingen als in het eerste geval, behalve:

  • Je ziet de gegenereerde code niet (misschien lijkt deze situatie niet zo weerzinwekkend voor iemand?)
  • U moet ervoor zorgen dat typen kunnen worden opgegeven, dat wil zeggen dat "true" altijd beschikbaar moet zijn. Dit is gemakkelijk in het geval van Lombok, dat "waarheid" annoteert. Bij databasemodellen die afhankelijk zijn van een live verbinding die altijd beschikbaar is, ligt het iets moeilijker.

Wat is het probleem met het genereren van codes?

Naast de lastige vraag hoe het beter is om codegeneratie te starten - handmatig of automatisch, moet ik vermelden dat er mensen zijn die geloven dat codegeneratie helemaal niet nodig is. De rechtvaardiging voor dit standpunt, dat ik het vaakst ben tegengekomen, is dat het dan moeilijk is om de build-pipeline op te zetten. Ja, het is echt moeilijk. Er zijn extra infrastructuurkosten. Als je net begint met een bepaald product (of het nu jOOQ, of JAXB, of Hibernate, enz. is), kost het tijd om een ​​workbench op te zetten die je zou willen besteden aan het leren van de API zelf om er waarde uit te halen.

Als de kosten die gepaard gaan met het begrijpen van het apparaat van de generator te hoog zijn, dan heeft de API inderdaad slecht werk geleverd aan de bruikbaarheid van de codegenerator (en in de toekomst blijkt dat maatwerk daarin ook moeilijk is). Bruikbaarheid moet de hoogste prioriteit hebben voor een dergelijke API. Maar dat is slechts één argument tegen het genereren van code. Schrijf anders volledig met de hand de lokale weergave van interne of externe waarheid.

Velen zullen zeggen dat ze geen tijd hebben om dit allemaal te doen. Ze hebben een deadline voor hun superproduct. Ooit zullen we de montagebanden uitkammen, we hebben tijd. Ik zal ze beantwoorden:

De waarheid eerst, of waarom het systeem moet worden ontworpen op basis van de databasestructuur
Origineel, Alan O'Rourke, Publieksstapel

Maar in Hibernate / JPA is het zo eenvoudig om code "in Java" te schrijven.

Echt. Voor Hibernate en zijn gebruikers is dit zowel een zegen als een vloek. In Hibernate kun je eenvoudig een aantal entiteiten schrijven, zoals deze:

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

En bijna alles is klaar. Het doel van Hibernate is nu om complexe "details" te genereren over hoe deze entiteit precies zal worden gedefinieerd in de DDL van uw "dialect" van 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);

... en start de toepassing. Een heel coole functie om snel aan de slag te gaan en verschillende dingen uit te proberen.

Laat mij echter. Ik loog.

  • Zal Hibernate de definitie van deze benoemde primaire sleutel daadwerkelijk afdwingen?
  • Zal Hibernate een index maken op TITLE? Ik weet zeker dat we het nodig hebben.
  • Zal Hibernate van deze sleutel een identiteitssleutel maken in de identiteitsspecificatie?

Waarschijnlijk niet. Als u uw project helemaal opnieuw ontwikkelt, is het altijd handig om de oude database gewoon weg te gooien en een nieuwe te genereren zodra u de nodige annotaties toevoegt. Dus de Book-entiteit zal uiteindelijk de vorm aannemen:

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

Koel. Regenereren. Nogmaals, in dit geval zal het in het begin heel gemakkelijk zijn.

Maar daar moet je later voor betalen.

Vroeg of laat moet je de productie in. Dan werkt het model niet meer. Omdat:

In productie zal het niet meer mogelijk zijn om, indien nodig, de oude database weg te gooien en alles vanaf nul te beginnen. Uw database wordt een legacy-database.

Vanaf nu en voor altijd zul je moeten schrijven DDL-migratiescripts, bijvoorbeeld met behulp van Flyway. En wat gebeurt er in dit geval met uw entiteiten? U kunt ze handmatig aanpassen (en uw werklast verdubbelen) of Hibernate ze voor u laten regenereren (hoe waarschijnlijk is het dat degene die op deze manier is gegenereerd om aan uw verwachtingen te voldoen?). U verliest hoe dan ook.

Dus zodra u in productie gaat, heeft u hot patches nodig. En ze moeten heel snel in productie worden genomen. Omdat je geen soepele pipelining van je migraties voor productie hebt voorbereid en georganiseerd, ben je wild aan het patchen. En dan heb je geen tijd om alles goed te doen. En je scheldt Hibernate uit, omdat het altijd iemands schuld is, maar jij niet ...

In plaats daarvan had vanaf het allereerste begin alles heel anders kunnen worden gedaan. Zet bijvoorbeeld ronde wielen op een fiets.

Database eerst

De echte "waarheid" in uw databaseschema en "soevereiniteit" daarover ligt binnen de database. Het schema wordt alleen in de database zelf gedefinieerd en nergens anders, en elk van de clients heeft een kopie van dit schema, dus het is volkomen logisch om naleving van het schema en de integriteit ervan af te dwingen, om het goed in de database te doen - waar de informatie wordt opgeslagen.
Dit is oude, zelfs afgezaagde wijsheid. Primaire en unieke sleutels zijn goed. Vreemde sleutels zijn prima. Beperkingscontrole is goed. Verklaringen - Prima.

En dat is niet alles. Als u bijvoorbeeld Oracle gebruikt, zou u waarschijnlijk het volgende willen specificeren:

  • In welke tablespace staat je table?
  • Wat is haar PCTFREE-waarde
  • Wat is de cachegrootte in uw reeks (achter de id)

Dit alles maakt misschien niet uit in kleine systemen, maar het is niet nodig om te wachten tot de overgang naar het rijk van "big data" - u kunt al veel eerder profiteren van door de leverancier geleverde opslagoptimalisaties, zoals de hierboven genoemde. Geen van de ORM's die ik heb gezien (inclusief jOOQ) biedt toegang tot de volledige set DDL-opties die u mogelijk in uw database wilt gebruiken. ORM's bieden enkele hulpmiddelen om u te helpen bij het schrijven van DDL.

Maar uiteindelijk wordt een goed ontworpen schema met de hand geschreven in DDL. Elke gegenereerde DDL is slechts een benadering ervan.

Hoe zit het met het klantmodel?

Zoals hierboven vermeld, heeft u op de client een kopie nodig van uw databaseschema, de clientweergave. Uiteraard moet dit klantbeeld synchroon lopen met het echte model. Wat is de beste manier om dit te bereiken? Met een codegenerator.

Alle databases leveren hun meta-informatie via SQL. Ga als volgt te werk om alle tabellen in verschillende SQL-dialecten uit uw database te halen:

	-- 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 query's (of soortgelijke, afhankelijk van of u ook views, gerealiseerde views, tabelwaardefuncties moet overwegen) worden ook uitgevoerd door aan te roepen DatabaseMetaData.getTables() van JDBC, of ​​met behulp van de jOOQ meta-module.

Op basis van de resultaten van dergelijke query's is het relatief eenvoudig om aan de clientzijde een representatie van uw databasemodel te genereren, ongeacht welke technologie u op de client gebruikt.

  • Als u JDBC of Spring gebruikt, kunt u een reeks tekenreeksconstanten maken
  • Als u JPA gebruikt, kunt u de entiteiten zelf genereren
  • Als u jOOQ gebruikt, kunt u een jOOQ-metamodel genereren

Afhankelijk van hoeveel mogelijkheden uw client-API biedt (bijv. jOOQ of JPA), kan het gegenereerde metamodel erg rijk en compleet zijn. Neem bijvoorbeeld de mogelijkheid van impliciete joins, geïntroduceerd in jOOQ 3.11, die berust op gegenereerde meta-informatie over externe sleutelrelaties tussen uw tabellen.

Nu zal elke database-increment automatisch de clientcode bijwerken. Stel je bijvoorbeeld voor:

ALTER TABLE book RENAME COLUMN title TO book_title;

Zou je deze job echt twee keer willen doen? In geen geval. We leggen gewoon de DDL vast, voeren deze door uw build-pijplijn en halen de bijgewerkte entiteit op:

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

Of de bijgewerkte jOOQ-klasse. De meeste DDL-wijzigingen hebben ook invloed op de semantiek, niet alleen op de syntaxis. Daarom kan het handig zijn om in de gecompileerde code te zien welke code zal (of zou kunnen) worden beïnvloed door het verhogen van uw database.

De enige waarheid

Welke technologie u ook gebruikt, er is altijd één model dat de enige bron van waarheid is voor een bepaald subsysteem - of we moeten hier in ieder geval naar streven en verwarring in de onderneming vermijden waarin 'waarheid' overal en nergens tegelijk is. Alles kan veel makkelijker. Als u alleen XML-bestanden uitwisselt met een ander systeem, gebruikt u gewoon XSD. Bekijk het INFORMATION_SCHEMA-metamodel van jOOQ in XML-vorm:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD is goed begrepen
  • XSD markeert XML-inhoud zeer goed en maakt validatie in alle klanttalen mogelijk
  • XSD heeft een goed versiebeheer en is zeer achterwaarts compatibel
  • XSD kan worden vertaald in Java-code met behulp van XJC

Het laatste punt is belangrijk. Wanneer we met een extern systeem communiceren via XML-berichten, willen we er zeker van zijn dat onze berichten geldig zijn. Dit is heel eenvoudig te realiseren met JAXB, XJC en XSD. Het zou pure waanzin zijn om te denken dat, in een Java-eerste ontwerpbenadering waarbij we onze berichten maken als Java-objecten, ze op de een of andere manier begrijpelijk kunnen worden weergegeven in XML en voor consumptie naar een ander systeem kunnen worden verzonden. De op deze manier gegenereerde XML zou van zeer slechte kwaliteit zijn, niet gedocumenteerd en moeilijk te ontwikkelen. Als er een akkoord zou zijn over het niveau van servicekwaliteit (SLA) op zo'n interface, zouden we het meteen verpesten.

Om eerlijk te zijn, dit is precies wat er de hele tijd gebeurt met de JSON API, maar dat is een ander verhaal, ik zal de volgende keer discussiëren ...

Databases: ze zijn hetzelfde

Als je met databases werkt, begrijp je dat ze in wezen allemaal hetzelfde zijn. De database is eigenaar van de gegevens en moet het schema beheren. Eventuele wijzigingen in het schema moeten direct in DDL worden geïmplementeerd, zodat de enige bron van waarheid wordt bijgewerkt.

Wanneer de bronupdate heeft plaatsgevonden, moeten alle clients ook hun exemplaren van het model bijwerken. Sommige clients kunnen in Java zijn geschreven met behulp van jOOQ en Hibernate of JDBC (of beide). Andere clients kunnen in Perl zijn geschreven (laten we ze geluk wensen), andere in C#. Het maakt niet uit. Het hoofdmodel staat in de database. Door ORM gegenereerde modellen zijn meestal van slechte kwaliteit, slecht gedocumenteerd en moeilijk te ontwikkelen.

Dus maak geen fouten. Maak vanaf het begin geen fouten. Werk vanuit een database. Bouw een implementatiepijplijn die kan worden geautomatiseerd. Schakel codegeneratoren in om uw databasemodel gemakkelijk te kopiëren en op clients te dumpen. En stop met je zorgen te maken over codegeneratoren. Ze zijn goed. Met hen wordt u productiever. Het enige dat u hoeft te doen, is wat tijd besteden aan het vanaf het begin instellen, en u zult jaren van verbeterde prestaties hebben om het verhaal van uw project op te bouwen.

Bedank me nog niet, later.

Verduidelijking

Voor alle duidelijkheid: dit artikel pleit er op geen enkele manier voor dat het hele systeem (d.w.z. domein, bedrijfslogica, enz., enz.) moet worden aangepast aan uw databasemodel. Waar ik het in dit artikel over heb, is dat clientcode die met een database interageert, moet werken op basis van het databasemodel, zodat het niet zelf het databasemodel reproduceert in de "eerste klas"-status. Dergelijke logica bevindt zich meestal op de gegevenstoegangslaag op uw client.

In architecturen met twee niveaus, die op sommige plaatsen nog steeds bewaard zijn gebleven, is een dergelijk systeemmodel mogelijk het enige mogelijke. In de meeste systemen lijkt de gegevenstoegangslaag mij echter een "subsysteem" te zijn dat het databasemodel omvat.

Исключения

Er zijn uitzonderingen op elke regel, en ik heb al eerder gezegd dat de benadering van eerst de database en het genereren van broncode soms ongepast kan zijn. Hier zijn een paar van dergelijke uitzonderingen (er zijn waarschijnlijk andere):

  • Wanneer het schema onbekend is en moet worden geopend. U biedt bijvoorbeeld een tool waarmee gebruikers door elk diagram kunnen navigeren. Opluchting. Er is hier geen codegeneratie. Maar toch - allereerst de database.
  • Wanneer een circuit direct moet worden gegenereerd om een ​​probleem op te lossen. Dit voorbeeld lijkt een enigszins gerafelde versie van het patroon te zijn entiteit attribuut waarde, d.w.z. je hebt niet echt een goed gedefinieerd schema. In dit geval weet u vaak niet eens zeker of een RDBMS bij u past.

Uitzonderingen zijn van nature uitzonderlijk. In de meeste gevallen waarbij RDBMS wordt gebruikt, is het schema van tevoren bekend, bevindt het zich in het RDBMS en is het de enige bron van "waarheid", en moeten alle klanten daarvan afgeleide kopieën aanschaffen. Idealiter zou dit een codegenerator moeten omvatten.

Bron: www.habr.com

Voeg een reactie