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

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

Продължаваме да изследваме темата 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 не е свършена добра работа върху удобството на генератора на код (а по-късно се оказва, че и потребителската настройка в него е сложна). Удобството на използване трябва да бъде най-високият приоритет за всеки такъв 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 ще направи този ключ идентифициращ в Specification на Identity?

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

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

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