- PVSM.RU - https://www.pvsm.ru -
В этой весьма запоздалой статье я объясню почему, по моему мнению, в большинстве случаев при разработке модели данных приложения необходимо придерживаться подхода "database first". Вместо "Java[любой другой язык] first" подхода, который выведет вас на длинную дорожку, полную боли и страданий, как только проект начнет расти.
"Слишком занят, чтобы стать лучше" Licensed CC [1] by Alan O’Rourke / Audience Stack [2]. Оригинальное изображение [3]
Эта статья вдохновлена недавним вопросом на StackOverflow [4].
Интересные reddit-обсуждения /r/java [5] и /r/programming [6].
К моему удивлению, одна небольшая группа пользователей похоже была потрясена тем фактом, что jOOQ [7] сильно "завязан" на генерации исходного кода.
При том, что вы можете использовать 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-миграций [8], которые также могут запускаться как вручную, так и автоматически.
Существуют разные философии, преимущества и недостатки в отношении этих подходов к кодогенерации, которые я не хочу обсуждать в этой статье. Но по сути, смысл сгенерированного кода заключается в том, что он является Java-представлением того, что мы считаем неким "эталоном" (как внутри, так и снаружи нашей системы). В некотором роде компиляторы делают то же самое, когда генерируют байт-код, машинный код или какой-либо другой исходный код из исходников — в итоге мы получаем представление о нашем "эталоне" на другом специфичном языке.
Существует достаточно много таких генераторов кода. Например, XJC может генерировать Java-код из файлов XSD или WSDL [9]. Принцип всегда один и тот же:
И почти всегда имеет смысл именно генерировать это представление, чтобы избежать лишней работы и лишних ошибок.
Примечательно, что еще один, более современный, подход к генерации кода в jOOQ — это Type Providers, (как он сделан в F# [10]), где код генерируется компилятором при компиляции и никогда не существует в исходной форме. Аналогичный (но менее сложный) инструмент в Java — это обработчики аннотаций, например, Lombok [11].
В обоих случаях происходит все тоже самое что и в обычной кодогенерации, кроме:
Помимо каверзного вопроса, нужно ли генерировать код вручную или автоматически, некоторые люди считают, что код вообще не нужно генерировать. Причина, которую я слышу чаще всего — что такую генерацию сложно реализовать в CI/CD pipeline. И да, это правда, т.к. мы получаем накладные расходы на создание и поддержку дополнительной инфраструктуры, тем более если вы новичок в используемых инструментах (jOOQ, JAXB, Hibernate и др.).
Если накладные расходы на изучение работы кодогенератор слишком высоки, то действительно пользы будет мало. Но это единственный аргумент против. В большинстве остальных случаев совершенно не имеет никакого смысла вручную писать код, который является обычным представлением модели чего-либо.
Многие люди утверждают, что у них нет времени на это, т.к. именно сейчас нужно как можно скорее "выкатить" очередной MVP. А доработать свой CI/CD pipeline они смогут когда-нибудь потом. В таких случаях я обычно говорю: "Ты слишком занят, чтобы стать лучше".
Да, это правда. Это одновременно и радость, и боль для пользователей 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);
Это действительно отличный способ для быстрого старта разработки — остается только запустить приложение.
Но не все так радужно. Еще остается множество вопросов:
Похоже, что нет. Но пока проект находится в стадии разработки, вы всегда можете выбросить свою текущую базу данных и сгенерировать все с нуля, добавив нужные аннотации к модели.
Итак, класс 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 [8]. При этом, что происходит с вашими клиентскими классами? Вы можете либо адаптировать их вручную (что приведет к двойной работе), либо попросить Hibernate генерировать их (но насколько велики шансы того, что результат такой генерации будет соответствовать ожиданиям?). В итоге вас могут ожидать большие проблемы.
Как только код попадает в production, в него почти сразу необходимо вносить исправления, причем как можно быстрее.
И т.к. установка миграций БД не встроена в ваш сборочный конвейер, придется устанавливать такие патчи вручную на свой страх и риск. Чтобы вернуться назад и сделать все правильно уже не хватит времени. Его хватит только на то, чтобы винить Hibernate во всех своих бедах.
Вместо этого вы могли бы поступить совершенно иначе с самого начала. А именно использовать круглые колеса вместо квадратных.
Эталон схемы данных и контроль над ней находится в ведомстве вашей СУБД. База данных — это единственное место, где определена схема, и все клиенты имеют копию этой схемы, но не наоборот. Данные находятся в вашей базе данных, а не в вашем клиенте, поэтому имеет смысл обеспечить контроль схемы и ее целостности именно там, где находятся данные.
Это старая мудрость, ничего нового. Первичные и уникальные ключи хороши. Внешние ключи прекрасны. Проверка ограничений на стороне БД замечательна. Assertion (когда они окончательно реализованы) [12] великолепны.
И это еще далеко не все. Например, если вы используете Oracle, вы можете указать:
Возможно все это не имеет значения в небольших системах, зато в более крупных системах вам не придется идти по пути "больших данных", пока вы не выжмите все соки из своего текущего хранилища. Ни одна 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() [13] конкретного JDBC-драйвера, либо в модуле jOOQ-meta.
Из результатов таких запросов относительно легко создать любое клиентское представление модели БД, независимо от того, какая именно технология доступа к данным используется.
В зависимости от количества функций, предлагаемых вашим API доступа к данным (jOOQ, JPA или что-то еще), сгенерированная метамодель может быть действительно богатой и полной. Как пример, функция неявного соединения в jOOQ 3.11, которая опирается на метаинформацию о взаимоотношениях внешних ключей между вашими таблицами [14].
Теперь любое изменение схемы базы данных автоматически приведет к обновлению клиентского кода.
Представьте, что нужно переименовать колонку в таблице:
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. Как метамодель INFORMATION_SCHEMA jOOQ в формате XML: https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd [15]
Обратим отдельное внимание на последний пункт. При общении с внешней системой через XML-сообщения мы должны быть уверены в валидности сообщений. И это действительно очень легко сделать с помощью таких вещей как JAXB, XJC и XSD. Было бы сумасшествием думать об уместности Java-first подхода в данном случае. Генерируемый на основе объектов XML получится низкого качества, будет плохо задокументирован и трудно расширяем. И если на такое взаимодействие есть SLA, то вы будете разочарованы.
Честно говоря, это похоже на то, что сейчас происходит с различными API для JSON, но это уже совершенна другая история...
При работе с БД тут все тоже самое. База данных владеет данными, и она так же должна быть хозяином схемы этих данных. Все модификации схемы должны быть выполнены посредством DDL напрямую, чтобы обновить эталон.
После обновления эталона все клиенты должны обновить свои представления о модели. Некоторые клиенты могут быть написаны на Java, используя либо jOOQ и/или Hibernate, либо JDBC. Другие клиенты могут быть написаны на Perl (удачи им) или даже на C#. Это не имеет никакого значения. Основная модель находится в базе данных. Тогда как модели, созданные с помощью ORM, имеют низкое качество, недостаточно хорошо документированы и трудно расширяемы.
Поэтому не делайте этого, причем с самого начала разработки. Вместо этого начните с базы данных. Создайте автоматизированный CI/CD конвейер. Используйте в нем генерацию кода, чтобы автоматически генерировать модель базы данных для клиентов при каждой сборке. И перестаньте волноваться, все будет хорошо. Все что требуется — немного первоначальных усилий по настройке инфраструктуры, но в результате вы получите выигрыш в процессе разработки для остальной части вашего проекта на годы вперед.
Не надо благодарностей.
Для закрепления: эта статья никоим образом не утверждает, что модель базы данных должна распространяться на всю вашу систему (на предметную область, бизнес-логику и пр.). Мои заявления заключается лишь в том, что клиентский код, взаимодействующий с базой данных, должен быть лишь представлением схемы БД, но никак не определять и не формировать ее.
В двухуровневых архитектурах, которые по-прежнему имеют место быть, схема БД может быть единственным источником информации о модели вашей системы. Однако в большинстве систем я рассматриваю уровень доступа к данным как «подсистему», которая инкапсулирует модель базы данных. Как-то так.
Как и в любом другом хорошем правиле, в нашем тоже есть свои исключения (и я уже предупреждал, что подход database first и генерация кода не всегда является правильным выбором). Вот эти исключения (возможно, список не полный):
Особенность этих исключений в том, что они довольно редко всречаются в живой природе. В большинстве же случаев при использовании реляционных БД схема заранее известна и является "эталоном" вашей модели, а клиенты должны работать с копией этой модели, сгенерированной с использованием генераторов кода.
Автор: mgramin
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/283360
Ссылки в тексте:
[1] Licensed CC: https://creativecommons.org/licenses/by/2.0/
[2] Alan O’Rourke / Audience Stack: http://audiencestack.com/static/blog.html
[3] Оригинальное изображение: https://www.flickr.com/photos/toddle_email_newsletters/15596940251/in/photostream
[4] Эта статья вдохновлена недавним вопросом на StackOverflow: https://stackoverflow.com/q/50706556/521799
[5] /r/java: https://redd.it/8p0uj3
[6] /r/programming: https://redd.it/8p0qle
[7] jOOQ: https://www.jooq.org
[8] такая генерация может происходить сразу же после установки Flyway-миграций: https://blog.jooq.org/2014/06/25/flyway-and-jooq-for-unbeatable-sql-development-productivity
[9] XJC может генерировать Java-код из файлов XSD или WSDL: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/xjc.html
[10] как он сделан в F#: https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers
[11] Lombok: https://projectlombok.org
[12] Assertion (когда они окончательно реализованы): https://community.oracle.com/ideas/13028
[13] DatabaseMetaData.getTables(): https://docs.oracle.com/javase/10/docs/api/java/sql/DatabaseMetaData.html#getTables(java.lang.String,java.lang.String,java.lang.String,java.lang.String%5B%5D)
[14] функция неявного соединения в jOOQ 3.11, которая опирается на метаинформацию о взаимоотношениях внешних ключей между вашими таблицами: https://blog.jooq.org/2018/02/20/type-safe-implicit-join-through-path-navigation-in-jooq-3-11
[15] https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd: https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd
[16] паттерна Entity-attribute-value: https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model
[17] Источник: https://habr.com/post/413597/?utm_campaign=413597
Нажмите здесь для печати.