Истината преди всичко: защо системата трябва да се проектира, взимайки предвид структурата на базата данни

Здравей, Хабр!

Продължаваме да изследваме темата Java и Spring, включително на ниво бази данни. Днес предлагаме да прочетете защо при проектирането на големи приложения структурата на базата данни, а не Java кодът, трябва да бъде решаваща и как се постига това, както и какви изключения съществуват от това правило.

В тази доста закъсняла статия ще обясня защо смятам, че в практически всички случаи моделът на данни в приложението трябва да се проектира "въз основа на базата данни", а не "въз основа на възможностите на Java" (или друг клиентски език, с който работите). Избирайки втория подход, поемате дълъг път на болка и страдание, веднага щом проектът ви започне да расте.

Статията е написана по мотиви на един въпрос, зададен на Stack Overflow.

Интересни дискусии на reddit в разделите /r/java и /r/programming.

Генерация на код

Колко ми беше изненадващо, че съществува толкова малка прослойка потребители, които, след като се запознаят с jOOQ, се възмущават от факта, че при работа с jOOQ се разчита сериозно на генерирането на изходен код. Никой не ви пречи да използвате jOOQ така, както смятате за нужно, и не ви задължава да използвате генериране на код. Но по подразбиране (както е описано в ръководството) работата със jOOQ протича така: започвате с (наследена) схема на базата данни, извършвате обратното проектиране с помощта на генератора на код jOOQ, за да получите набор от класове, представляващи вашите таблици, и след това пишете типобезопасни заявки към тези таблици:

	for (Record2 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#. В такъв случай кодът се генерира от компилатора, всъщност на етапа на компилация. В вид на изходен код такъв код принципно не съществува. В Java съществуват подобни, макар и не толкова елегантни инструменти – това са процесори на анотации, например, Lombok.

В определен смисъл, тук се случват същите неща, както в първия случай, с изключение на:

  • Не виждате генерирания код (възможно е това да изглежда не толкова отблъскващо на някого?)
  • Трябва да гарантирате, че типовете могат да бъдат предоставяни, тоест, "истината" винаги трябва да бъде достъпна. Това е лесно в случая с Lombok, който анотира “истината”. Малко по-сложно е с моделите на бази данни, работата на които зависи от постоянно достъпна активна връзка.

Каква е проблемът с генерирането на код?

Освен хитрия въпрос за това как по-добре да стартираме генерирането на код – ръчно или автоматично, трябва да спомена и че има хора, които смятат, че генерирането на код изобщо не е необходимо. Обосновката на тази гледна точка, с която съм се срещал най-често, е, че тогава е трудно да се настрои конвейер за сглобяване. Да, наистина е трудно. Възникват допълнителни инфраструктурни разходи. Ако току-що започвате работа с определен продукт (независимо дали става дума за jOOQ, JAXB, Hibernate и т.н.), времето, прекарано в настройка на работната среда, е време, което бихте искали да вложите в изучаване на самото API, за да извлечете стойност от него.

Ако разходите, свързани с разбирането на устройството на генератора, са твърде високи – значи API-то не е направило добър UX за генератора на код (а впоследствие се оказва, че и потребителската настройка в него е сложна). Удобството при използване трябва да бъде най-висок приоритет за всяко такова API. Но това е само един аргумент против генерирането на код. В останалите случаи е абсолютно необходимо да напишете локалното представление на вътрешната или външната истина изцяло ръчно.

Много хора ще кажат, че нямат време да се занимават с всичко това. Те имат крайни срокове за своя Супер-продукт. Някой ден ще подобрим конвейерите за сглобяване, ще имаме време. Аз им отговарям:

Истината преди всичко: защо системата трябва да се проектира, взимайки предвид структурата на базата данни
Оригинал, Алан О'Рурк, Audience Stack

Но в 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 ще направи този ключ идентифициращ в спецификацията за идентичност?

Вероятно, не. Ако разработвате проекта си от нула, винаги е удобно просто да откажете старата база данни и да генерирате нова, след като добавите нужните анотации. Така, сущността Book в крайна сметка ще изглежда така:

	@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, пускаме го през вашия сборен конвейер и получаваме обновената сущност:

@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 също се отразяват на семантиката, а не само на синтаксиса. Затова понякога е удобно да се види в скомпилирания код какъв код ще бъде (или може да бъде) засегнат от изменението в базата данни.

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

Независимо от това коя технология използвате, винаги има една модел, който е единствен източник на истина за определена подсистема – или, поне, ние трябва да се стремим към това и да избягваме такава enterprise-бъркотия, където „истината“ е веднага навсякъде и никъде. Всичко може да бъде много по-просто. Ако просто обменяте 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“, където правим нашите съобщения под формата на Java обекти, те биха могли по някакъв начин да се отобразят ясно на XML и да бъдат изпратени за консумация от друга система. XML, генериран по този начин, щеше да е с много лошо качество, недокументиран и трудно за развитие. Ако по такъв интерфейс съществуваше споразумение за ниво на обслужване (SLA), веднага щяхме да го провалим.

Честно казано, точно това постоянно се случва с API на JSON, но това е друга история, следващия път ще се оплача…

Базите данни: това е едно и също

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

Когато актуализацията на източника е направена, всички клиенти също трябва да актуализират своите копия на модела. Някои клиенти може да бъдат написани на Java, използвайки jOOQ и Hibernate или JDBC (или всичките наведнъж). Други клиенти може да бъдат написани на Perl (остава да им пожелаем успех), третите – на C#. Няма значение. Основният модел е в базата данни. Моделите, генерирани с помощта на ORM, обикновено са с лошо качество, слабо документирани и трудни за развитие.

Затова не допускайте грешки. От самото начало не допускайте грешки. Работете, изхождайки от базата данни. Постройте такъв конвейер за разгръщане, който може да бъде автоматизиран. Включете генератори на код, за да е удобно да копирате модела на вашата база данни и да го разпращате на клиентите. И спратете да се безпокоите за генераторите на код. Те са добри. С тях ще станете по-продуктивни. Просто трябва от самото начало да инвестирате малко време в настройката им – и след това ви очакват години на повишена производителност, които ще съставят историята на вашия проект.

Още не благодарете, по-късно.

Обяснение

За яснота: Тази статия по никакъв начин не пропагандира, че моделът на вашата база данни трябва да бъде наложен на цялата система (т.е. предметната област, бизнес логиката и т.н.). В тази статия говоря за това, че клиентският код, взаимодействуващ с базата данни, трябва да действа, основавайки се на модела на базата данни, така че в него самото да не се възпроизвежда моделът на базата данни в статус „пръв клас“. Тази логика обикновено се намира на нивото на достъпа до данните при вас на клиента.

В двуслойни архитектури, които все още на места са запазени, такъв модел на системата може да бъде единствено възможният. Въпреки това, в повечето системи, нивото на достъп до данни ми изглежда като „подсистема“, инкапсулираща модела на базата данни.

Изключения

От всяко правило има изключения, и вече казах, че подходът с приоритет на базата данни и генерирането на изходния код понякога може да се окаже неподходящ. Ето две такива изключения (вероятно ще има и други):

  • Когато схемата е неизвестна и трябва да бъде осветена. Например, вие сте доставчик на инструмент, който помага на потребителите да се ориентират в която и да е схема. Уф. Тук без генериране на код. Но все пак – базата данни е на първо място.
  • Когато схемата трябва да бъде генерирана на лето за решаване на определена задача. Този пример изглежда като малко по-сложна версия на шаблона entity attribute value, т.е. вие наистина нямате ясно определена схема. В този случай често не може да сте сигурни, че РСУБД ще е подходяща за вас.

Изключенията по своята природа са изключителни. В повечето случаи, свързани с използването на РСУБД, схемата е известна предварително, тя е в рамките на РСУБД и е единственият източник на „истината“, а всички клиенти трябва да имат копия, производни от нея. В идеалния случай при това трябва да се използва генератор на код.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster