Вистината прво, или зошто системот треба да се дизајнира врз основа на дизајнот на базата на податоци

Еј Хабр!

Продолжуваме да ја истражуваме темата Јава и пролет, вклучително и на ниво на база на податоци. Денеска ве покануваме да прочитате зошто, при дизајнирање на големи апликации, од одлучувачко значење треба да биде структурата на базата на податоци, а не кодот на Java, како се прави тоа и какви исклучоци има од ова правило.

Во оваа прилично доцна статија, ќе објаснам зошто верувам дека во скоро сите случаи, моделот на податоци во апликацијата треба да биде дизајниран „од базата на податоци“ наместо „од можностите на Java“ (или кој било јазик на клиентот што го имате работи со). Со преземањето на вториот пристап, вие се подготвувате за долг пат на болка и страдање штом вашиот проект ќе почне да расте.

Статијата е напишана врз основа на едно прашање, дадена на Stack Overflow.

Интересни дискусии за reddit во делови /r/java и /r/програмирање.

Генерирање кодови

Колку бев изненаден што има толку мал сегмент на корисници кои, откако се запознаа со jOOQ, се огорчени од фактот дека jOOQ сериозно се потпира на генерирање изворен код за да работи. Никој не ве спречува да го користите jOOQ како што ви одговара, или да ве тера да користите генерирање код. Но, стандардниот (како што е опишан во прирачникот) начин на работа со jOOQ е да започнете со шема на (наследна) база на податоци, да ја обновите инженерството користејќи го генераторот на кодот jOOQ за да добиете збир на класи што ги претставуваат вашите табели, а потоа да запишете тип -безбедни прашања до овие табели:

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

Кодот се генерира или рачно надвор од склопот или рачно на секое склопување. На пример, таквата регенерација може да следи веднаш потоа Миграција на базата на податоци Flyway, што исто така може да се направи рачно или автоматски.

Генерирање на изворен код

Постојат различни филозофии, предности и недостатоци поврзани со овие пристапи за генерирање кодови - рачно и автоматско - за кои нема да разговарам детално во оваа статија. Но, генерално, целата поента на генерираниот код е тоа што ни овозможува да ја репродуцираме во Java онаа „вистина“ што ја земаме здраво за готово, било во нашиот систем или надвор од него. Во извесна смисла, тоа е она што компајлерите го прават кога генерираат бајтекод, машински код или некоја друга форма на изворен код - добиваме претстава за нашата „вистина“ на друг јазик, без оглед на конкретните причини.

Има многу такви генератори на кодови. На пример, XJC може да генерира Java код базиран на датотеки XSD или WSDL. Принципот е секогаш ист:

  • Има одредена вистина (внатрешна или надворешна) - на пример, спецификација, модел на податоци итн.
  • Ни треба локално претставување на оваа вистина во нашиот програмски јазик.

Покрај тоа, речиси секогаш е препорачливо да се генерира таква репрезентација за да се избегне вишок.

Даватели на типови и обработка на прибелешки

Забелешка: друг, помодерен и специфичен пристап за генерирање код за jOOQ е користење на даватели на типови, како што се имплементирани во F#. Во овој случај, кодот е генериран од компајлерот, всушност во фазата на компилација. Во принцип, таков код не постои во изворна форма. Јава има слични, иако не толку елегантни алатки - процесори за прибелешки, на пример, Lombok.

Во извесна смисла, овде се случуваат истите работи како и во првиот случај, со исклучок на:

  • Не го гледате генерираниот код (можеби оваа ситуација некому му изгледа помалку одбивно?)
  • Мора да се осигурате дека може да се обезбедат типови, односно „вистинито“ мора секогаш да биде достапно. Ова е лесно во случајот со Ломбок, кој ја нотира „вистината“. Малку е покомплицирано со моделите на бази на податоци кои се потпираат на постојано достапна врска во живо за работа.

Што е проблемот со генерирањето код?

Покрај незгодното прашање за тоа како најдобро да се изврши генерирањето код - рачно или автоматски, мораме да споменеме и дека има луѓе кои веруваат дека генерирањето код воопшто не е потребно. Оправдувањето за оваа гледна точка, на која најчесто налетав, е дека тогаш е тешко да се постави цевковод за изградба. Да, навистина е тешко. Се јавуваат дополнителни инфраструктурни трошоци. Ако штотуку започнувате со одреден производ (без разлика дали е jOOQ, или JAXB, или Hibernate, итн.), за поставување на производствена средина е потребно време што повеќе би сакале да го потрошите за да го научите самиот API за да можете да извлечете вредност од него .

Ако трошоците поврзани со разбирањето на структурата на генераторот се премногу високи, тогаш, навистина, API направи лоша работа за употребливоста на генераторот на код (а подоцна се покажа дека прилагодувањето на корисникот во него е исто така тешко). Употребливоста треба да биде највисок приоритет за секое такво API. Но, ова е само еден аргумент против генерирањето код. Инаку, апсолутно е целосно рачно да се напише локално претставување на внатрешната или надворешната вистина.

Многумина ќе речат дека немаат време да го направат сето ова. Им истекува роковите за нивниот Супер производ. Еден ден ќе ги средиме транспортерите, ќе имаме време. Јас ќе им одговорам:

Вистината прво, или зошто системот треба да се дизајнира врз основа на дизајнот на базата на податоци
Авторски, Алан О'Рурк, Стак од публика

Но, во Hibernate/JPA е толку лесно да се напише Java код.

Навистина. За Hibernate и неговите корисници, ова е и благослов и проклетство. Во Hibernate можете едноставно да напишете неколку ентитети, како ова:

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

И речиси сè е подготвено. Сега останува на Hibernate да генерира сложени „детали“ за тоа како точно овој ентитет ќе биде дефиниран во DDL на вашиот 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);

... и почнете да ја стартувате апликацијата. Навистина одлична можност да започнете брзо и да пробате различни работи.

Сепак, ве молам дозволете ми. лажев.

  • Дали Hibernate всушност ќе ја спроведе дефиницијата за овој именуван примарен клуч?
  • Дали Hibernate ќе создаде индекс во TITLE? – Сигурно знам дека ќе ни треба.
  • Дали Hibernate точно ќе направи овој клуч да се идентификува во спецификацијата на идентитетот?

Најверојатно не. Ако го развивате вашиот проект од нула, секогаш е погодно едноставно да ја отфрлите старата база на податоци и да генерирате нова веднаш штом ќе ги додадете потребните прибелешки. Така, ентитетот Книга на крајот ќе ја има формата:

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

Кул. Регенерирајте. Повторно, во овој случај ќе биде многу лесно на почетокот.

Но, ќе мора да платите за тоа подоцна

Порано или подоцна ќе мора да одите во производство. Тогаш овој модел ќе престане да работи. Бидејќи:

Во производството, веќе нема да може, доколку е потребно, да се отфрли старата база на податоци и да се започне од нула. Вашата база на податоци ќе стане наследство.

Отсега и засекогаш ќе мора да пишувате Скрипти за миграција на DDL, на пример, користејќи Flyway. Што ќе се случи со вашите субјекти во овој случај? Можете или да ги приспособите рачно (и со тоа да го удвоите обемот на работа), или да му кажете на Hibernate да ги регенерира за вас (колку е веројатно вака генерираните да ги исполнат вашите очекувања?) Во секој случај, ќе изгубите.

Значи, откако ќе влезете во производство, ќе ви требаат топли закрпи. И тие треба многу брзо да се стават во производство. Бидејќи не подготвивте и не организиравте мазен цевковод на вашите миграции за производство, диво крпете сè. И тогаш веќе немате време да направите сè правилно. А вие го критикувате Hibernate, бидејќи секогаш некој друг е виновен, само не вие...

Наместо тоа, работите можеа да се направат сосема поинаку од самиот почеток. На пример, ставете тркалезни тркала на велосипед.

Прво, база на податоци

Вистинската „вистина“ во шемата на вашата база на податоци и „суверенитетот“ над неа лежи во базата на податоци. Шемата е дефинирана само во самата база на податоци и никаде на друго место, и секој клиент има копија од оваа шема, така што има совршена смисла да се наметне усогласеноста со шемата и нејзиниот интегритет, да се направи тоа директно во базата на податоци - каде што се информациите. складирани.
Ова е стара, дури и проникната мудрост. Примарните и единствените клучеви се добри. Странските клучеви се добри. Проверувањето на ограничувањата е добро. Тврдења - Добро.

Покрај тоа, тоа не е сè. На пример, користејќи Oracle, веројатно би сакале да наведете:

  • Во кој простор за маса е вашата маса?
  • Која е неговата вредност без PCTFREE?
  • Која е големината на кешот во вашата секвенца (зад идентификацијата)

Ова можеби не е важно во малите системи, но не треба да чекате додека не се преселите во областа на големите податоци - можете многу порано да започнете да имате корист од оптимизациите за складирање обезбедени од продавачот како оние споменати погоре. Ниту еден од ORM-ите што ги видов (вклучувајќи го и jOOQ) не обезбедува пристап до целосниот сет на опции за DDL што можеби сакате да ги користите во вашата база на податоци. ORM-ите нудат некои алатки кои ви помагаат да пишувате DDL.

Но, на крајот на денот, добро дизајнираното коло е рачно напишано во DDL. Секој генериран DDL е само приближна негова.

Што е со моделот на клиентот?

Како што споменавме погоре, на клиентот ќе ви треба копија од шемата на вашата база на податоци, поглед на клиентот. Непотребно е да се спомене, овој приказ на клиентот мора да биде синхронизиран со вистинскиот модел. Кој е најдобриот начин да се постигне ова? Користење на генератор на код.

Сите бази на податоци ги обезбедуваат своите мета информации преку SQL. Еве како да ги добиете сите табели од вашата база на податоци на различни 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

Овие прашања (или слични, во зависност од тоа дали треба да ги земете предвид и прегледите, материјализираните прикази, функциите вреднувани според табелата) исто така се извршуваат со повикување DatabaseMetaData.getTables() од JDBC, или со користење на мета-модулот jOOQ.

Од резултатите од таквите прашања, релативно е лесно да се генерира каква било претстава на клиентската страна на моделот на вашата база на податоци, без оглед на тоа каква технологија ја користите на клиентот.

  • Ако користите JDBC или Spring, можете да креирате збир на константи на низа
  • Ако користите JPA, можете да ги генерирате самите ентитети
  • Ако користите jOOQ, можете да го генерирате мета-моделот jOOQ

Во зависност од тоа колку функционалност нуди вашиот клиент API (на пр. jOOQ или JPA), генерираниот мета модел може да биде навистина богат и целосен. Земете ја, на пример, можноста за имплицитни спојувања, воведен во jOOQ 3.11, што се потпира на генерирани мета информации за врските со странски клучеви што постојат помеѓу вашите табели.

Сега секое зголемување на базата на податоци автоматски ќе го ажурира кодот на клиентот. Замислете на пример:

ALTER TABLE book RENAME COLUMN title TO book_title;

Дали навистина би сакале да ја работите оваа работа двапати? Во никој случај. Едноставно поврзете го DDL, стартувајте го низ вашиот build pipeline и добијте го ажурираниот ентитет:

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

Или ажурираната класа jOOQ. Повеќето промени во DDL влијаат и на семантиката, а не само на синтаксата. Затоа, може да биде корисно да се погледне во составениот код за да се види кој код ќе (или би можел) да биде засегнат од зголемувањето на вашата база на податоци.

Единствената вистина

Без оглед на тоа која технологија ја користите, секогаш постои еден модел кој е единствениот извор на вистината за некој потсистем - или, во најмала рака, треба да се стремиме кон тоа и да избегнеме таква конфузија на претпријатието, каде што „вистината“ е насекаде и никаде одеднаш. . Сè може да биде многу поедноставно. Ако само разменувате XML-датотеки со некој друг систем, само користете XSD. Погледнете го мета-моделот INFORMATION_SCHEMA од jOOQ во XML форма:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD е добро разбран
  • XSD многу добро ја токенизира XML содржината и овозможува валидација на сите јазици на клиентите
  • XSD е добро верзиран и има напредна компатибилност наназад
  • XSD може да се преведе во Java код користејќи XJC

Последната точка е важна. Кога комуницираме со надворешен систем користејќи XML пораки, сакаме да бидеме сигурни дека нашите пораки се валидни. Ова е многу лесно да се постигне со користење на JAXB, XJC и XSD. Би било целосно лудост да се мисли дека, со пристапот на дизајнот „Јава на прво место“ каде што ги правиме нашите пораки како објекти на Java, тие некако би можеле кохерентно да се мапираат на XML и да се испратат на друг систем за потрошувачка. XML генериран на овој начин би бил со многу слаб квалитет, недокументиран и тежок за развој. Ако имаше договор за ниво на услуга (SLA) за таков интерфејс, веднаш ќе го зафрливме.

Искрено, ова се случува постојано со JSON API, но тоа е друга приказна, следниот пат ќе се скарам...

Бази на податоци: тие се иста работа

Кога работите со бази на податоци, сфаќате дека сите тие се во основа слични. Базата ги поседува нејзините податоци и мора да управува со шемата. Сите модификации направени на шемата мора да се имплементираат директно во DDL за да се ажурира единствениот извор на вистината.

Кога ќе се појави ажурирање на изворот, сите клиенти мора да ги ажурираат и своите копии од моделот. Некои клиенти можат да бидат напишани во Java користејќи jOOQ и Hibernate или JDBC (или и двете). Другите клиенти можат да бидат напишани во Perl (само им посакуваме среќа), додека други можат да бидат напишани во C#. Не е важно. Главниот модел е во базата на податоци. Моделите генерирани со користење на ORM обично се со слаб квалитет, слабо документирани и тешко се развиваат.

Затоа, не правете грешки. Не правете грешки од самиот почеток. Работете од базата на податоци. Изградете гасовод за распоредување што може да се автоматизира. Овозможете генератори на кодови за да го олесните копирањето на моделот на вашата база на податоци и фрлањето на клиентите. И престанете да се грижите за генераторите на кодови. Тие се добри. Со нив ќе станете попродуктивни. Само треба да потрошите малку време за да ги поставите од самиот почеток - а потоа ве очекуваат години на зголемена продуктивност, кои ќе ја сочинуваат историјата на вашиот проект.

Не ми се заблагодарувај уште, подоцна.

појаснување

Да бидеме јасни: овој напис во никој случај не застапува дека треба да го свиткате целиот систем (т.е. домен, деловна логика, итн., итн.) за да одговара на моделот на вашата база на податоци. Она што го велам во оваа статија е дека кодот на клиентот што е во интеракција со базата на податоци треба да дејствува врз основа на моделот на базата на податоци, така што тој самиот не го репродуцира моделот на базата на податоци во статус „прва класа“. Оваа логика обично се наоѓа на слојот за пристап до податоци на вашиот клиент.

Во архитектурите на две нивоа, кои сè уште се зачувани на некои места, таков системски модел можеби е единствениот можен. Меѓутоа, во повеќето системи, слојот за пристап до податоци ми се чини дека е „потсистем“ што го инкапсулира моделот на базата на податоци.

Исклучоци

Постојат исклучоци од секое правило, а јас веќе реков дека пристапот за генерирање на база на податоци и изворен код понекогаш може да биде несоодветен. Еве неколку такви исклучоци (веројатно има и други):

  • Кога шемата е непозната и треба да се открие. На пример, вие сте снабдувач на алатка која им помага на корисниците да се движат на кој било дијаграм. Уф. Тука нема генерирање кодови. Но, сепак, базата на податоци е на прво место.
  • Кога коло мора да се генерира во лет за да се реши некој проблем. Овој пример изгледа како малку фантастична верзија на моделот вредност на атрибутот на ентитет, т.е., навистина немате јасно дефинирана шема. Во овој случај, честопати дури и не можете да бидете сигурни дека RDBMS ќе ви одговара.

Исклучоците се по природа исклучителни. Во повеќето случаи кои вклучуваат употреба на RDBMS, шемата е однапред позната, таа се наоѓа во рамките на RDBMS и е единствениот извор на „вистината“ и сите клиенти треба да добијат копии добиени од неа. Идеално, треба да користите генератор на код.

Извор: www.habr.com

Додадете коментар