Привет, Хабр!
Мы продолжаем исследовать тему Java и Spring, в том числе, на уровне баз данных. Сегодня предлагаем почитать о том, почему при проектировании больших приложений именно структура базы данных, а не код Java, должна иметь определяющее значение, как это делается, и какие исключения есть из этого правила.
В этой довольно запоздалой статье я объясню, почему считаю, что практически во всех случаях модель данных в приложении должна проектироваться «исходя из базы данных», а не «исходя из возможностей Java» (или другого клиентского языка, с которым вы работаете). Выбирая второй подход, вы вступаете на долгий путь боли и страданий, как только ваш проект начинает расти.
Статья написана по мотивам одного вопроса, заданного на Stack Overflow.
Интересные обсуждения на reddit в разделах /r/java и /r/programming.
Насколько же я удивился, что существует такая небольшая прослойка пользователей, которые, познакомившись с jOOQ, возмущаются тем фактом, что при работе jOOQ всерьез полагается на генерацию исходного кода. Никто не мешает вам использовать jOOQ так, как вы считаете нужным, и не заставляет использовать генерацию кода. Но по умолчанию (так, как описано в руководстве) работа с jOOQ происходит так: вы начинаете с (унаследованной) схемы базы данных, выполняете ее обратное проектирование при помощи генератора кода jOOQ, чтобы таким образом получить набор классов, представляющих ваши таблицы, а затем пишете типобезопасные запросы к этим таблицам:
Код генерируется или вручную за пределами сборки, или вручную при каждой сборке. Например, такая регенерация может последовать сразу после миграции базы данных Flyway, которую также можно выполнить вручную или автоматически.
С такими подходами к генерации кода – ручными и автоматическими – связаны различные философии, преимущества и недостатки, которые я не собираюсь подробно обсуждать в этой статье. Но, в целом, вся суть генерируемого кода в том, что он позволяет воспроизвести на Java ту «истину», которую мы принимаем как данность, либо в рамках нашей системы, либо вне ее. В некотором смысле, то же самое делают компиляторы, генерирующие байт-код, машинный код или какой-нибудь другой вид кода на основе исходников – мы получаем представление нашей «истины» на другом языке, независимо от конкретных причин.
Существует множество таких генераторов кода. Например, XJC может генерировать код Java на основе файлов XSD или WSDL. Принцип всегда одинаков:
Причем генерировать такое представление почти всегда бывает целесообразно – чтобы избежать избыточности.
На заметку: еще один, более современный и специфичный подход к генерации кода для jOOQ связан с использованием провайдеров типов, в таком виде, как они реализованы в F#. В таком случае код генерируется компилятором, собственно на этапе компиляции. В виде исходников такой код в принципе не существует. В Java существуют схожие, хотя и не столь изящные инструменты – это процессоры аннотаций, например, Lombok.
В определенном смысле, здесь происходят те же вещи, что и в первом случае, за исключением:
Кроме хитрого вопроса о том, каким образом лучше запускать генерацию кода – вручную или автоматически, приходится упомянуть и о том, что есть люди, считающие, что генерация кода вообще не нужна. Обоснование такой точки зрения, которое попадалось мне наиболее часто – в том, что тогда сложно настроить конвейер сборки. Да, действительно сложно. Возникают дополнительные инфраструктурные издержки. Если вы только начинаете работать с определенным продуктом (будь то jOOQ, или JAXB, или Hibernate, т.д.), на настройку рабочей среды уходит время, которое вы хотели бы потратить на изучение самого API, чтобы потом извлекать из него ценность.
Если слишком велики издержки, связанные с тем, чтобы разобраться в устройстве генератора – то, действительно, в API плохо поработали над юзабилити генератора кода (а в дальнейшем оказывается, что и пользовательская настройка в нем сложна). Удобство использования должно быть наивысшим приоритетом для любого такого API. Но это всего лишь один аргумент против генерации кода. В остальном абсолютно полностью вручную писать локальное представление внутренней или внешней истины.
Многие скажут, что у них нет времени всем этим заниматься. У них горят сроки сдачи по их Супер-Продукту. Когда-нибудь потом причешем конвейеры сборки, успеется. Я им отвечу:
Оригинал, Алан О'Рурк, Audience Stack
Но в Hibernate / JPA так просто писать код «под Java».
Действительно. Для Hibernate и его пользователей это одновременно и благо, и проклятье. В Hibernate можно просто написать пару сущностей, вот так:
И почти все готово. Теперь удел Hibernate – генерировать сложные «детали» того, как именно эта сущность будет определяться на DDL вашего «диалекта» SQL:
… и начинаем гонять приложение. Действительно крутая возможность, чтобы быстро приступать к работе и пробовать разные вещи.
Однако, позвольте. Я слукавил.
Вероятно, нет. Если вы разрабатываете ваш проект с нуля, то всегда удобно просто отбросить старую базу данных и сгенерировать новую, как только добавите нужные аннотации. Так, сущность Book в конечном итоге примет вид:
Круто. Сгенерировать заново. Опять же, в таком случае на старте будет очень легко.
Рано или поздно придется выходить в продакшен. Именно тогда такая модель перестанет работать. Потому что:
В продакшене уже нельзя будет при необходимости отбросить старую базу данных и начать все с чистого листа. Ваша база данных превратится в унаследованную.
Отныне и навсегда вам придется писать миграционные скрипты DDL, например, при помощи Flyway. А что в таком случае произойдет с вашими сущностями? Вы сможете либо адаптировать их вручную (и так удвоите себе объем работы), либо прикажете Hibernate заново сгенерировать их для вас (насколько велики шансы, что сгенерированный таким образом будет соответствовать вашим ожиданиям?) Вы в любом случае проигрываете.
Таким образом, как только вы перейдете в продакшен, вам потребуются горячие патчи. А их нужно выводить в продакшен очень быстро. Поскольку же вы не подготовились и не организовали для продакшена гладкую конвейеризацию ваших миграций, вы все дико пропатчиваете. А затем уже не успеваете все сделать правильно. И ругаете Hibernate, поскольку всегда виноват кто угодно, только не вы…
Вместо этого, с самого начала все можно было делать совершенно иначе. Например, поставить на велосипед круглые колеса.
Настоящая «истина» в схеме вашей базы данных и «суверенитет» над ней кроется внутри базы данных. Схема определяется только в самой базе данных и нигде больше, и у каждого из клиентов есть копия этой схемы, поэтому совершенно целесообразно навязывать соблюдение схемы и ее целостности, делать это прямо в базе данных – там, где и хранится информация.
Это старая даже избитая мудрость. Первичные и уникальные ключи – это хорошо. Внешние ключи – хорошо. Проверка ограничений – хорошо. Утверждения – хорошо.
Причем, это еще не все. Например, используя Oracle, вы, вероятно, захотите указать:
Возможно, все это и не важно в малых системах, но не обязательно дожидаться перехода в область «больших данных» — можно и гораздо раньше начать извлекать пользу от предоставляемых поставщиком оптимизаций хранения данных, таких, как упомянутые выше. Ни одна из ORM, какие мне доводилось видеть (в том числе, jOOQ) не обеспечивает доступа к полному набору опций DDL, которые вы, возможно, захотите использовать в вашей базе данных. ORM предлагают некоторые инструменты, которые помогают писать DDL.
Но, в конце концов, хорошо спроектированная схема вручную написана на DDL. Любой сгенерированный DDL является лишь ее аппроксимацией.
Как упоминалось выше, на клиенте вам потребуется копия схемы вашей базы данных, клиентское представление. Излишне упоминать, что это клиентское представление должно быть синхронизировано с реальной моделью. Как лучше всего этого добиться? При помощи генератора кода.
Все базы данных предоставляют свою метаинформацию через SQL. Вот как получить из вашей базы данных все таблицы на разных диалектах SQL:
Эти запросы (или подобные им, в зависимости от того, приходится ли также учитывать представления, материализованные представления, функции с табличным значением) также выполняются при помощи вызова
Из результатов таких запросов относительно легко сгенерировать любое клиентское представление модели вашей базы данных, независимо от того, какая технология используется у вас на клиенте.
В зависимости от того, какой объем возможностей предлагается вашим клиентским API (напр. jOOQ или JPA), сгенерированная мета-модель может быть по-настоящему насыщенной и полной. Возьмем, хотя бы, возможность неявных объединений, появившуюся в jOOQ 3.11, которая опирается на сгенерированную метаинформацию о взаимоотношениях внешних ключей, действующих между вашими таблицами.
Теперь любое приращение базы данных будет автоматически приводить к обновлению клиентского кода. Представьте себе, например:
Вы в самом деле хотели бы делать эту работу дважды? Ни в коем случае. Просто фиксируем DDL, прогоняем его через ваш конвейер сборки и получаем обновленную сущность:
Либо обновленный класс jOOQ. Большинство изменений DDL также отражаются на семантике, а не только на синтаксисе. Поэтому бывает удобно посмотреть в скомпилированном коде, какой код будет (или может быть) затронут приращением вашей базы данных.
Независимо от того, какой технологией вы пользуетесь, всегда есть одна модель, которая является единственным источником истины для некоторой подсистемы – или, как минимум, мы должны к этому стремиться и избегать такой enterprise-путаницы, где «истина» сразу везде и нигде. Все может быть гораздо проще. Если вы всего лишь обмениваетесь XML-файлами с какой-нибудь другой системой, просто пользуйтесь XSD. Посмотрите на мета-модель INFORMATION_SCHEMA из jOOQ в XML-форме:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd
Последний пункт важен. При коммуникации с внешней системой при помощи XML-сообщений мы хотим быть уверены в валидности наших сообщений. Этого очень легко добиться при помощи JAXB, XJC и XSD. Было бы полным безумием рассчитывать, что, при подходе к проектированию «сначала Java», где мы делаем наши сообщения в виде объектов Java, их можно было бы как-то внятно отобразить на XML и отправить для потребления в другую систему. XML, сгенерированный таким образом, был бы очень плохого качества, не документирован, и его сложно было бы развивать. Если бы по такому интерфейсу существовало соглашение об уровне качества обслуживания (SLA), мы бы сразу его запороли.
Честно говоря, именно это постоянно и происходит с API на JSON, но это уже другая история, в следующий раз поругаюсь…
Работая с базами данных, вы понимаете, что все они, в принципе, похожи. База владеет своими данными и должна руководить схемой. Любые модификации, вносимые в схему, должны реализовываться непосредственно на DDL, чтобы обновлялся единый источник истины.
Когда обновление источника произошло, все клиенты также должны обновить свои копии модели. Некоторые клиенты могут быть написаны на Java с использованием jOOQ и Hibernate или JDBC (или всех сразу). Другие клиенты могут быть написаны Perl (остается пожелать им удачи), третьи – на C#. Это не важно. Главная модель находится в базе данных. Модели, сгенерированные при помощи ORM, обычно плохого качества, плохо документированы, и их сложно развивать.
Поэтому не совершайте ошибок. С самого начала не совершайте ошибок. Работайте, исходя из базы данных. Постройте такой конвейер развертывания, который можно автоматизировать. Включите генераторы кода, чтобы удобно было копировать модель вашей базы данных и сбрасывать ее на клиенты. И прекратите беспокоиться о генераторах кода. Они хорошие. С ними вы станете продуктивнее. Нужно только с самого начала потратить немного времени на их настройку – и далее вас ждут годы повышенной производительности, из которых сложится история вашего проекта.
Пока не благодарите, потом.
Для ясности: Эта статья ни в коем случае не пропагандирует, что под модель вашей базы данных нужно прогибать всю систему (т.е., предметную область, бизнес-логику, т.д., т.п). В этой статье я говорю о том, что клиентский код, взаимодействующий с базой данных, должен действовать, отталкиваясь от модели базы данных, так, чтобы в нем самом не воспроизводилась модель базы данных в статусе «первого класса». Такая логика обычно располагается на уровне доступа к данным у вас на клиенте.
В двухуровневых архитектурах, которые до сих пор кое-где сохранились, такая модель системы может быть единственно возможной. Однако в большинстве систем уровень доступа данных кажется мне «подсистемой», инкапсулирующей модель базы данных.
Из любого правила есть исключения, и я уже говорил, что подход с первичностью базы данных и генерацией исходного кода иногда может оказаться неподходящим. Вот пара таких исключений (вероятно, найдутся и другие):
Исключения по природе своей исключительны. В большинстве случаев, связанных с использованием РСУБД, схема известна заранее, она находится внутри РСУБД и является единственным источником «истины», а всем клиентам приходится обзаводиться копиями, производными от нее. В идеале при этом нужно задействовать генератор кода.
Мы продолжаем исследовать тему Java и Spring, в том числе, на уровне баз данных. Сегодня предлагаем почитать о том, почему при проектировании больших приложений именно структура базы данных, а не код Java, должна иметь определяющее значение, как это делается, и какие исключения есть из этого правила.
В этой довольно запоздалой статье я объясню, почему считаю, что практически во всех случаях модель данных в приложении должна проектироваться «исходя из базы данных», а не «исходя из возможностей Java» (или другого клиентского языка, с которым вы работаете). Выбирая второй подход, вы вступаете на долгий путь боли и страданий, как только ваш проект начинает расти.
Статья написана по мотивам одного вопроса, заданного на Stack Overflow.
Интересные обсуждения на reddit в разделах /r/java и /r/programming.
Генерация кода
Насколько же я удивился, что существует такая небольшая прослойка пользователей, которые, познакомившись с 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#. В таком случае код генерируется компилятором, собственно на этапе компиляции. В виде исходников такой код в принципе не существует. В 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? – я точно знаю, он нам понадобится.
- A Hibernate точно сделает этот ключ идентифицирующим в Identity Specification?
Вероятно, нет. Если вы разрабатываете ваш проект с нуля, то всегда удобно просто отбросить старую базу данных и сгенерировать новую, как только добавите нужные аннотации. Так, сущность 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, т.е., у вас в самом деле нет четко определенной схемы. В данном случае зачастую даже вообще нельзя быть уверенным, что вам подойдет РСУБД.
Исключения по природе своей исключительны. В большинстве случаев, связанных с использованием РСУБД, схема известна заранее, она находится внутри РСУБД и является единственным источником «истины», а всем клиентам приходится обзаводиться копиями, производными от нее. В идеале при этом нужно задействовать генератор кода.
amarao
Я работаю с java-программистом, который такой подход исповедует. Ужасно. База данных никем толком не контролируется (есть чуть-чуть в flyway и всё), софт не способен работать с базами разных версий.
Рядом есть openstack, который прекрасно умеет работать в режиме sliding window между разными версиями схем базы данных. Насколько я понимаю, у них там sqlalchemy, и там код определяет структуру БД, и код же поддерживает несколько версий схем вместе с точно известным migration path.
sshikov
Да как бы дело даже не в том, что кто-то может испортить любой подход. Дело в том, что автор почему-то считает, что видит проблему в целом. А то что не укладывается в его видение, считает редкими исключениями. В то время как на самом деле они могут быть более частыми — просто он этого не видит.
Drunik
проблема синхронизации версий клиентской и серверной части всегда может быть и на любом уровне. Тут вопрос в организации тех.процесса — мы например клиентские модули храним в самой БД (динамические библиотеки) и загружаем их при подключении к ней. Там образом не может быть ситуации когда структура БД не синхронизована с клиентской частью, т.к. обновления охватывают сразу и клиентскую часть и серверную. Надо поработать со старой версией БД — берёшь её, а там и клиентские модули старой версии, своместимые с этой версией БД — всё работает. Конкретно в нашем случае нам этого достаточно, не претендую на универсальность.
Что касается темы — то если дальше развивать мысль, то нужно переносить часть бизнес-логики в уровень sql-сервера чтобы обеспечить максимальную производительность работы с большими объёмами данных. Буду с нетерпением ждать продолжения этой битвы ))
SergeyUstinov
Так все так или иначе часть задач делают с помощью sql. :)))
А те, кто так не делает, рассказывают на хабре, как они героически на сервере с офигенным количеством ядер считают раз в сутки 800 миллионов цифр…
commenter
Вы подход-то не поняли, вот в чём проблема.
Статья на примерах для неких миддлов, то есть знакомых с базовыми принципами, терминологией и имеющим практический опыт людей, поясняет архитектурный принцип, который и на уровне сеньора многие не понимают. То есть недостаточно качественно подан материал. И вообще качественно его подать, это как книгу по высшей математике написать для школьников. Так обычно почти ни у кого не получается. Ну и у автора не получилось.
Автор довольно длинно, из-за желания всё разжевать миддлам (хотя он сам наверняка считает, что пишет для новичков, при этом сам весьма вероятно являясь не очень далёким от миддла разработчиком), наливает много воды, плюс использует лексикон не совсем из той оперы. Например просто глаз режет, когда он заявляет про «истины» которые по его мнению что-то там реализует. Нет на этом уровне никаких истин, есть только удачные или неудачные шаблоны, а новички же, купив рекламируемую книжку, будут считать это именно истиной, чем затуманят свой мозг и накапают на мозги другим разработчикам.
В общем я бы назвал подход «оптимальное использование сущностей». То есть не БД, как настаивает автор, а во первых — оптимальность, и во вторых — моделирование сущностей. И от такой постановки уже элементарно вытекает необходимость во многом отталкиваться от БД. Не от чего-то там наверченного непонятными генераторами, которые насочиняли непонятные лица, привыкшие использовать исключительно модные библиотеки и никогда не делать собственных велосипедов, а от понимания сути происходящего — нам нужна правильная модель и она должна использоваться оптимально, то есть эффективно лишь до той степени, пока эта программная эффективность не убивает эффективность процесса.
К сожалению, массовый рынок образовательных услуг, включая вот такие книжки, получает львиную долю прибыли от начинающих, слегка разбавленных миддлами. Поэтому рынок опускается до уровня молодёжи и выдаёт «священные писания» (вспомним всяких Фаулеров и прочих «святых») в которых сомневаться — страшный грех (ибо мало опыта у читателя, да и продажи ведь упадут!). Ну и далее находится бездна желающих получить гонорар за излияние своего (часто примитивного) видения вот в таком виде, который вроде бы должен объяснять глубины, но на самом деле из-за отсутствия опыта у читателя скорее уводит его от пути действительно истинного.
Поэтому ваш знакомый вами и воспринимается со словами типа «ужасно». Правда как уже отметили выше — это лишь ваше личное восприятие одного единственного случая, из чего вы делаете весьма далеко идущие (и неправильные) обобщения.
И кстати, по разным версиям БД. Вам зачем много версий? Отпускаем резвиться одну команду, которая куролесит свои изменения, внедряет их в отрыве от потребностей всей конторы, потом так же отпускаем другую команду, потом вообще все куролесят что захотят, но зато все героически отстаивают необходимость работы с БД разных версий. Примерно так у вас было? И я вам подскажу — потом каждый выберет свой язык программирования, любимую ОС, тучу вспомогательных инструментов, и всё это заставит изучать толпу тестировщиков, администраторов, технических писателей и прочая, и прочая, и прочая. Верной дорогой идёте, товарищи!
amarao
Ох уж обесценили так обесценили. Что ж вы так, раз уж взяли обесценивать, так обесценивайте всё, а то какая-то непонятная неполнота. Ну, другого я от вас и не ждал.
В нашем случае поддержка нескольких версий от приложения требуется для возможности двигать версию приложения не меняя схему базы. Зачем двигать версию приложения? Для откатов. Почему не откатывать версию схемы? Потому что у новой схемы уже есть пользователи.
В openstack же поддержка sliding window сделана для возможности постепенного апгрейда, когда разные сервера работают на разных версиях ПО — и атомарный апгрейд схемы не ломает ничего.
commenter
Ну вы что, я лишь прокомментировал :)
Я не исключаю логичности в ваших действиях, но всё же при смене версии приложения на мобильных устройствах вполне удавалось менять и схему данных. Откатывать же приложение назад не позволяет (по простому), например маркетплейс. Поэтому в итоге все пользователи когда-то обновляются и новая версия меняет схему БД до нужной именно ей. Проблемы возможны разве что с синхронизацией пользователей с разными схемами, но это довольно узкая тема, далеко не всем оно надо. Да и с сервером синхронизация разноверсионных клиентов относительно легко выполняется через диспетчерирующую прокладку.
Если же речь идёт о серверах в различных подразделениях, то обычно обкатывают новое на отдельных подразделениях (поэтапное внедрение, оно важно не только из-за софтовых косяков, но в первую очередь — из-за организационых), если там что-то идёт не так, тогда откатывают и версию БД и версию приложения. Скрипты для этого (в обе стороны) заранее готовят.
Поддержка каких-то промежуточных сочетаний «версия схемы»-«версия ПО» есть задача очень опасная из-за комбинаторного роста сложности. Не знаю, что такое openstack, но комбинаторику он не отменяет никак.
Ну и так, к слову — атомарный апгрейд, видимо, не атомарный, а разовый. Собственно свойство атомарности является обязательным для любого апгрейда вообще, поэтому акцентировать именно на атомарности — ни к чему.
amarao
Про мобильные устройства вообще вы заговорили. Я говорю про обычный такой гео-распределённый кластер из нескольких нод. Вы можете обкатывать что угодно где угодно, но в какой-то момент приходит необходимость даунгрейда, причём частичного.
Насчёт того, что это "так сложно, что комбинаторный взрыв" — в java, может быть. В более приличных ORM'ах (я про sqlalchemy) существует простая полиси добавления, удаления и изменения полей, которая позволяет не плодить комбинаторный взрыв и при этом уметь работать с окне поддерживаемых схем базы данных, причём делать это без героических усилий со стороны программистов.
VolCh
Вы не поверите, но возможность писать разные части системы под разными ОС, на разных языках с разными базами данных или другими хранилищами считается одним из основных преимуществ сервисной или микросервисной архитектуры
amarao
… при этом если они все будут шариться в одной базе, это будет скорее антипаттерн, чем успех.
NightEagle
"с разными базами данных" упустили видимо..
amarao
Если мы имеем сервис "со своей базой" то всё хорошо, до тех пор, пока у этого сервиса не появляются rolling updates. Микросервисные товарищи любят приводить в пример "неважные микросервисы" (виджет погоды и т.д.), но есть сервисы важные и ничего с этим не сделаешь. Им нужен высокий аптайм, высокий аптайм означает rolling update или blue-green деплой, причём против одной и той же базы данных (у stateless сервисов без зависимости от данных всё просто, у но где-то по стеку снизу есть кто-то с зависимостью — и мы про него). И тут нам всё равно приходится иметь несколько версий приложения, работающих с одной и той же базой данных.
Либо мы заявляем бизнесу что "так невозможно, и каждую пятницу в 6 вечера мы выключаем google.com на два часа на апдейты" и слушаем реакцию бизнеса на это.
VolCh
По-моему, ситуации "несколько версий одного приложения/сервиса относительно небольшое время работают с одной базой данных" и "разные приложения/сервисы постоянно работают с одной базой данных" хоть и схожи формально, но практические подходы очень сильно различаются. В посте описана скорее вторая ситуация.
amarao
Хорошо написанное приложение эти сценарии не должно различать. Могут быть Большие Причины, почему rolling update может застрять на пол-года в полувыкаченном состояниии. И оно должно продолжать работать не хуже, чем было до начала апгрейда.
VolCh
А как по мне, то должно. Одно дело, когда базой владеют разные версии одного приложения и база никуда не экспортируется, других клиентов кроме этого приложения у базы нет. И совсем другое, если у базы 100500 "владельцев", и каждое изменение схемы у всех со всеми надо согласовывать. Тут сама база становится приложением/сервисом по сути, а остальные приложения/сервисы её клиенты