Главная » Хабрахабр » [Перевод] «Истина в последней инстанции» или зачем нужен Database First Design

[Перевод] «Истина в последней инстанции» или зачем нужен Database First Design

Вместо "Java[любой другой язык] first" подхода, который выведет вас на длинную дорожку, полную боли и страданий, как только проект начнет расти. В этой весьма запоздалой статье я объясню почему, по моему мнению, в большинстве случаев при разработке модели данных приложения необходимо придерживаться подхода "database first".

Оригинальное изображение
image
"Слишком занят, чтобы стать лучше" Licensed CC by Alan O’Rourke / Audience Stack.

Эта статья вдохновлена недавним вопросом на StackOverflow.

Интересные reddit-обсуждения /r/java и /r/programming.

Кодогенерация

К моему удивлению, одна небольшая группа пользователей похоже была потрясена тем фактом, что jOOQ сильно "завязан" на генерации исходного кода.

При том, что вы можете использовать jOOQ именно так как вам удобно, предпочтительным способ (согласно документации) является начать именно с уже существующей схемы БД, затем сгенерировать необходимые клиентские классы (соответствующие вашим таблицам) с помощью jOOQ, а после этого уже спокойно писать типобезопасные запросы для этих таблиц:

for (Record2<String, String> record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Type information derived from the // generated code referenced from the below SELECT clause .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ Generated names .from(ACTOR) .orderBy(1, 2)) { // ...
}

Например, такая генерация может происходить сразу же после установки Flyway-миграций, которые также могут запускаться как вручную, так и автоматически. Код может генерироваться либо вручную вне сборки, либо автоматически с каждой сборкой.

Генерация исходного кода

Но по сути, смысл сгенерированного кода заключается в том, что он является Java-представлением того, что мы считаем неким "эталоном" (как внутри, так и снаружи нашей системы). Существуют разные философии, преимущества и недостатки в отношении этих подходов к кодогенерации, которые я не хочу обсуждать в этой статье. В некотором роде компиляторы делают то же самое, когда генерируют байт-код, машинный код или какой-либо другой исходный код из исходников — в итоге мы получаем представление о нашем "эталоне" на другом специфичном языке.

Например, XJC может генерировать Java-код из файлов XSD или WSDL. Существует достаточно много таких генераторов кода. Принцип всегда один и тот же:

  • Существует некий эталон (внешний или внутренний), такой как спецификация, модель данных и пр.
  • Необходимо получить собственное представление об этом эталоне на нашем привычном языке программирования.

И почти всегда имеет смысл именно генерировать это представление, чтобы избежать лишней работы и лишних ошибок.

"Type providers" и обработка аннотаций

Аналогичный (но менее сложный) инструмент в Java — это обработчики аннотаций, например, Lombok. Примечательно, что еще один, более современный, подход к генерации кода в jOOQ — это Type Providers, (как он сделан в F#), где код генерируется компилятором при компиляции и никогда не существует в исходной форме.

В обоих случаях происходит все тоже самое что и в обычной кодогенерации, кроме:

  • Вы не видите сгенерированный код (возможно, для многих это уже большой плюс?)
  • Вы должны обеспечить доступность вашего "эталона" при каждой компиляции. Это не доставляет никаких проблем в случае с Lombok, который непосредственно аннотирует сам исходный код, который и является "эталоном" в данном случае. Немного сложнее с моделями баз данных, которые полагаются на всегда доступное "живое" соединение.

В чем проблема с генерацией кода?

Причина, которую я слышу чаще всего — что такую генерацию сложно реализовать в CI/CD pipeline. Помимо каверзного вопроса, нужно ли генерировать код вручную или автоматически, некоторые люди считают, что код вообще не нужно генерировать. мы получаем накладные расходы на создание и поддержку дополнительной инфраструктуры, тем более если вы новичок в используемых инструментах (jOOQ, JAXB, Hibernate и др.). И да, это правда, т.к.

Но это единственный аргумент против. Если накладные расходы на изучение работы кодогенератор слишком высоки, то действительно пользы будет мало. В большинстве остальных случаев совершенно не имеет никакого смысла вручную писать код, который является обычным представлением модели чего-либо.

именно сейчас нужно как можно скорее "выкатить" очередной MVP. Многие люди утверждают, что у них нет времени на это, т.к. В таких случаях я обычно говорю: "Ты слишком занят, чтобы стать лучше". А доработать свой CI/CD pipeline они смогут когда-нибудь потом.

"Но ведь Hibernate/JPA делает Java first разработку гораздо проще"

Это одновременно и радость, и боль для пользователей 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 нужное мне имя для первичного ключа?
  • Создаст ли необходимый мне индекс на поле TITLE?
  • Будет ли генерироваться уникальное значение ID для каждой записи?

Но пока проект находится в стадии разработки, вы всегда можете выбросить свою текущую базу данных и сгенерировать все с нуля, добавив нужные аннотации к модели.
Итак, класс Book в конечном виде будет выглядеть примерно так: Похоже, что нет.

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

Но вы заплатите за это, чуть позже

Рано или поздно ваше приложение попадает в production, и описанная схема перестанет работать:

данные в ней используются и могут стоить кучу денег. В живой и настоящей системе вы больше не сможете просто так взять и выбросить свою базу данных, т.к.

При этом, что происходит с вашими клиентскими классами? С этого момента вам необходимо писать скрипты миграций на каждое изменение в модели данных, например, используя Flyway. В итоге вас могут ожидать большие проблемы. Вы можете либо адаптировать их вручную (что приведет к двойной работе), либо попросить Hibernate генерировать их (но насколько велики шансы того, что результат такой генерации будет соответствовать ожиданиям?).

Как только код попадает в production, в него почти сразу необходимо вносить исправления, причем как можно быстрее.

установка миграций БД не встроена в ваш сборочный конвейер, придется устанавливать такие патчи вручную на свой страх и риск. И т.к. Его хватит только на то, чтобы винить Hibernate во всех своих бедах. Чтобы вернуться назад и сделать все правильно уже не хватит времени.

А именно использовать круглые колеса вместо квадратных. Вместо этого вы могли бы поступить совершенно иначе с самого начала.

Вперед к "Database First"

База данных — это единственное место, где определена схема, и все клиенты имеют копию этой схемы, но не наоборот. Эталон схемы данных и контроль над ней находится в ведомстве вашей СУБД. Данные находятся в вашей базе данных, а не в вашем клиенте, поэтому имеет смысл обеспечить контроль схемы и ее целостности именно там, где находятся данные.

Первичные и уникальные ключи хороши. Это старая мудрость, ничего нового. Проверка ограничений на стороне БД замечательна. Внешние ключи прекрасны. Assertion (когда они окончательно реализованы) великолепны.

Например, если вы используете Oracle, вы можете указать: И это еще далеко не все.

  • В каком табличном пространстве находится ваша таблица
  • Какое значение PCTFREE она имеет
  • Каков размер кэша последовательности (sequence)

Ни одна ORM, которую я когда-либо видел (в том числе jOOQ) не позволит вам использовать полный набор параметров DDL, которые предоставляет ваша СУБД. Возможно все это не имеет значения в небольших системах, зато в более крупных системах вам не придется идти по пути "больших данных", пока вы не выжмите все соки из своего текущего хранилища. ORM предлагают только некоторые инструменты, которые помогут написать DDL.

Весь автоматически сгенерированный DDL являются лишь приближением к этому. В конечном счете, хорошо продуманная схема должна быть написана только вручную с помощью СУБД-специфичного DDL.

Что насчет клиентской модели?

Излишне говорить, что это представление должно быть синхронизировано с реальной моделью. Как уже упоминалось ранее, вам понадобится некое представление схемы БД на стороне клиента. Конечно же используя генераторы кода. Как это сделать?

Вот так, например, можно получить список всех таблиц из разных баз данных: Все базы данных предоставляют доступ к своей метаинформации посредством старого доброго 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-meta.

Из результатов таких запросов относительно легко создать любое клиентское представление модели БД, независимо от того, какая именно технология доступа к данным используется.

  • Если вы используете JDBC или Spring, вы можете создать группу String-констант
  • Если используете JPA, можете сами создавать объекты
  • Если используете jOOQ, можете создать метамодели jOOQ

Как пример, функция неявного соединения в jOOQ 3. В зависимости от количества функций, предлагаемых вашим API доступа к данным (jOOQ, JPA или что-то еще), сгенерированная метамодель может быть действительно богатой и полной. 11, которая опирается на метаинформацию о взаимоотношениях внешних ключей между вашими таблицами.

Теперь любое изменение схемы базы данных автоматически приведет к обновлению клиентского кода.

Представьте, что нужно переименовать колонку в таблице:

ALTER TABLE book RENAME COLUMN title TO book_title;

Ни за что. Вы уверены, что хотите выполнить эту работу дважды? Просто закомитьте этот DDL, запустите сборку и наслаждайтесь обновленным объектом:

@Entity
@Table(name = "book", indexes = { // Would you have thought of this? @Index(name = "i_book_title", columnList = "book_title")
})
class Book { @Id @GeneratedValue(strategy = IDENTITY) int id; @Column("book_title") String **bookTitle**;
}

Таким образом, здорово видеть в сгенерированном коде клиента на что именно повлияли последние изменения в БД. Так же полученный клиентский нет необходимости компилировать каждый раз (как минимум до следующего изменения в схеме БД), что уже может быть большим плюсом!
Большинство изменений DDL также являются семантическими изменениями, а не только синтаксическими.

Правда всегда одна

Или, как минимум, мы должны стремиться к этому и избегать неразберихи в бизнесе, где «эталон» есть везде и нигде одновременно. Независимо от того, какую технологию вы используете, всегда должна быть только одна модель, которая и является эталоном для подсистемы. К примеру, если вы обмениваетесь XML-файлами с какой-либо другой системой, вы наверняка используете XSD. Это делает все намного проще. 10. Как метамодель INFORMATION_SCHEMA jOOQ в формате XML: https://www.jooq.org/xsd/jooq-meta-3. 0.xsd

  • XSD хорошо понятен
  • XSD отлично описывает XML-контент и позволяет осуществлять валидацию на всех клиентских языках
  • XSD позволяет легко управлять версиями и сохранять обратную совместимость
  • XSD можно превратить в Java-код с помощью XJC

При общении с внешней системой через XML-сообщения мы должны быть уверены в валидности сообщений. Обратим отдельное внимание на последний пункт. Было бы сумасшествием думать об уместности Java-first подхода в данном случае. И это действительно очень легко сделать с помощью таких вещей как JAXB, XJC и XSD. И если на такое взаимодействие есть SLA, то вы будете разочарованы. Генерируемый на основе объектов XML получится низкого качества, будет плохо задокументирован и трудно расширяем.

Честно говоря, это похоже на то, что сейчас происходит с различными API для JSON, но это уже совершенна другая история...

Чем базы данных хуже?

База данных владеет данными, и она так же должна быть хозяином схемы этих данных. При работе с БД тут все тоже самое. Все модификации схемы должны быть выполнены посредством DDL напрямую, чтобы обновить эталон.

Некоторые клиенты могут быть написаны на Java, используя либо jOOQ и/или Hibernate, либо JDBC. После обновления эталона все клиенты должны обновить свои представления о модели. Это не имеет никакого значения. Другие клиенты могут быть написаны на Perl (удачи им) или даже на C#. Тогда как модели, созданные с помощью ORM, имеют низкое качество, недостаточно хорошо документированы и трудно расширяемы. Основная модель находится в базе данных.

Вместо этого начните с базы данных. Поэтому не делайте этого, причем с самого начала разработки. Используйте в нем генерацию кода, чтобы автоматически генерировать модель базы данных для клиентов при каждой сборке. Создайте автоматизированный CI/CD конвейер. Все что требуется — немного первоначальных усилий по настройке инфраструктуры, но в результате вы получите выигрыш в процессе разработки для остальной части вашего проекта на годы вперед. И перестаньте волноваться, все будет хорошо.

Не надо благодарностей.

Пояснения

Мои заявления заключается лишь в том, что клиентский код, взаимодействующий с базой данных, должен быть лишь представлением схемы БД, но никак не определять и не формировать ее. Для закрепления: эта статья никоим образом не утверждает, что модель базы данных должна распространяться на всю вашу систему (на предметную область, бизнес-логику и пр.).

Однако в большинстве систем я рассматриваю уровень доступа к данным как «подсистему», которая инкапсулирует модель базы данных. В двухуровневых архитектурах, которые по-прежнему имеют место быть, схема БД может быть единственным источником информации о модели вашей системы. Как-то так.

Исключения

Вот эти исключения (возможно, список не полный): Как и в любом другом хорошем правиле, в нашем тоже есть свои исключения (и я уже предупреждал, что подход database first и генерация кода не всегда является правильным выбором).

  • Когда схема неизвестна заранее и ее необходимо исследовать. Например, вы поставщик инструмента, помогающего пользователям осуществлять навигацию по любой схеме. Само собой, тут не может быть никакой генерации кода. Но в любом случае, придется иметь дело с самой БД напрямую и ее схемой.
  • Когда для какой-то задачи необходимо создать схему "на лету". Это может быть похоже на одну из вариаций паттерна Entity-attribute-value, т.к. у вас нет четко определенной схемы. Также как и нет уверенности, что RDBMS в данном случае это верный выбор.

В большинстве же случаев при использовании реляционных БД схема заранее известна и является "эталоном" вашей модели, а клиенты должны работать с копией этой модели, сгенерированной с использованием генераторов кода. Особенность этих исключений в том, что они довольно редко всречаются в живой природе.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Частичка программы HolyJS 2018 Moscow

Конференция состоится 24–25 ноября. HolyJS 2018 Moscow уже совсем скоро. В этот раз программа получилась весьма разнообразной, однако несложно выделить главные тенденции: Доклады из первых рук (#firsthand) — доклады о инструментах/решениях от их авторов. Мы особенно тщательно подошли к выбору ...

[Перевод] GPU консоли Nintendo DS и его интересные особенности

Я хотел бы рассказать вам о работе GPU консоли Nintendo DS, об его отличиях от современных GPU, а также выразить своё мнение о том, почему использование Vulkan вместо OpenGL в эмуляторах не принесёт никаких преимуществ. Это может пригодиться для эмуляции ...