У реляционных баз данных есть один небольшой недостаток - они не выводят табличные данные на экран пользователя. С одной стороны - какой же это недостаток, когда технология занимается исключительно своими базаданновыми вещами и не лезет туда, где её не надо. С другой - приходится данные пробрасывать через другой язык программирования, а то и два-три в случае веба. Ну и сам язык запросов забывать не надо. Он тоже вроде как язык.

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

И вроде не велика проблема - делаем структуру в нашем любимом языке, такую же, как в БД и из наших кубиков складывается слово "счастье". Складываться-то оно складывается, но ненадолго. Очень быстро структура БД и структура в коде начинают разъезжаться. А если не начинают, то потыкайте в свой проект палочкой - скорее всего он уже умер. Как говорится "не щебечет дохлый щегол, а мёртвый проект не меняет свою структуру". Ну и далее по тексту...

Не сказать бы, что эту проблему не научились решать. Решают, и ещё как. Например, я слышал такой способ: надо просто аккуратно вносить изменения и в БД и одновременно в код, и не ошибаться. Тогда ничего разъезжаться не будет. Вы тоже поржали, да?

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

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

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

Наверняка вы слышали на собеседовании вопрос "чем отличается code-first от db-first, какие преимущества и недостатки". Этот вопрос очень любят на собеседованиях и я вам принёс ответ на него. Тут я хотел добавить мем с Пахомом, но постеснялся. Поэтому только таблица.

Подход

Преимущества

Недостатки

Code-first

Приводит структуру БД в соответствии с кодом

Приводит структуру БД в соответствии с кодом

DB-first

Приводит структуру в коде в соответствии с БД

Приводит структуру в коде в соответствии с БД

Не стоит благодарить, я от чистого сердца вам помогаю.

Я себя всегда относил к лагерю "DB-first". Ну потому что это же данные. Вот чисто интуитивно - как это позволить какому-то мигратору ковырять базу? А вдруг кто-то случайно удалил или переименовал поле в коде? Тогда что, просто удалится столбец в БД?

Когда случается ошибка при генерации модели в db-first - то ничего страшного. Легко перегенерить, поправить или даже восстановить из гита. В случае обратного преобразования структуры, т.е. из кода делаем структуру БД, то затрагивается не только структура, но и сами данные. А это очень ценная штука. Из гита их не взять, в бекапе лежат вчерашние, а зеркало уже перезатёрлось.

Как-то я уже привык считать, что данные первичны. Да и когда мы делаем новый проект, обычно данные уже есть. Даже если это не готовая реляционная база, то как минимум эксельные файлы. Сначала делается структура в БД, а потом вокруг этой структуры уже пишется код. И структуре уделяется максимальное внимание, потому что "garbage in - garbage out". А править структуру БД намного сложнее, чем код. Потому что там ещё и данные лежат.

Code-first я считал неким таким баловством, которое проще изучить, когда только сталкиваешься с реалиями БД и делаешь свои первые пет-проекты. И считал, что в дальнейшем этот подход принесёт проблемы при росте данных, в которых, на минуточку, вложено много труда пользователей системы. Правда code-first хорошо лежит в гите, а значит и хорошо бранчится по фич-веткам, чтобы потом благополучно смерджиться. Это явно плюс.

В реальности оба подхода работают и имеют право на жизнь. Тогда в чём же проблема и причём тут аксиома Эскобара?

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

Источник истины

Запечатлённый в XII веке факт познания Истины. Фотография тщательно скрывается властями
Запечатлённый в XII веке факт познания Истины. Фотография тщательно скрывается властями

Да, это единственная разница. Просто разные источники истины.

Тадам! Оказывается это тот же Шалтай-Болтай, только в анфас и профиль. Один подход декларирует, что истина в коде, а второй - что истина в БД. Не "правда", а именно истина. Когда данные разъезжаются, правы и код и БД. Да и вообще, как БД может быть не права? Она не умеет врать. И код не умеет врать. Это прерогатива кожанных (с недавних пор ещё и некоторых электронных болванов).

Просто мы условно считаем, что БД глаголет истину, а код изначально еретичен. Ну или наоборот. Это не значит, что код врёт. Он просто неверный, в том плане, что за ним следить надо, чтобы он следовал истинному и не оступался от пути праведного.

Вот только мой любимый db-first перестаёт работать при наличии нескольких окружений. Вот есть прод база, есть стейдж, есть дев, есть тест. Ну и локальная база под каждую фичу. И у всех истина. Ну не бывает же так, что у всех истина, правда? Да ещё и разная. Code-first имеет симметричный фатальный недостаток - он перестаёт работать при наличии более одного сервиса на БД. А если много сервисов на нескольких окружениях - то вообще хоть вешайся.

Не сказать бы, что эти проблемы не научились решать. Научились, и ещё как. Как говорится, если парадигма не работает, то вы добавили слишком мало костылей. В случае db-first это сложные синхронизаторы окружений, это история миграций и много чего ещё. Не очень-то просто.

Но меня сейчас интересует не конструкция костылей для работы подходов, а какое-то более фундаментальное решение вопроса. И оно у меня есть.

В туториалах и жарких спорах между сторонниками этих двух подходов забывают, что это классическая ложная дихотомия. Существует как минимум ещё один подход, который логичен и понятен. Давайте объясню на примере проектирования реляционных баз данных.

Ну вот смотрите: когда у нас отношение 1:М, то ссылающийся айди указывается в правой таблице. Когда М:1, то в левой. А когда М:М? Правильно, делается таблица-связка. Отдельная.

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

Такой источник истины принято называть моделью. А подход - model-first.

Вот шарписты помнят старый Entity Framework, который использует подход model-first. И уже наверное представляют, о чём я говорю. Только в EF model-first... плюшевый. Почему плюшевый?

Потому что .edmx файлы модели находятся внутри солюшена. Потому что только EF с ними работает. А если вам надо добавить другой проект к этой же модели? А если захотите использовать dapper или вообще самописный маппер? А если у вас там ещё подпружиненный CRM на hibernate крутится и джангисты из логистики лезут периодически в ту же базу по своим логистическим делам? А я напомню, всё не в статике, дрейф структуры никто не отменял. Пока вы пилите свою часть, часть модели уже уплыла. Можете, конечно, попросить джангистов ничего не трогать, пока вы пилите свою часть, но я догадываюсь, что вам ответят.

Нет, настоящий model-first должен быть независим от языка, фреймворка и мировоззрения создателя генератора кода. Истинно говорю вам. Только так его можно расшарить между командами и сервисами. Соответственно, технология должна быть language-agnostic. В EF модель гвоздями прибита к вижуал студии (которая протухла на маках год назад, а на линукс вообще никогда не завозилась) и к си шарпу. Я ничего не имею против шарпа - это мой любимый язык. Но я не могу поддержать лозунг "давайте всё перепишем на один язык, чтобы всем стало хорошо". Ну ладно, даже если у вас все сервисы написаны на шарпе с EF, то всё равно отсутствует "гальваническая развязка" между моделью и структурой в коде. То есть все проекты, которые пользуются этой моделью, вынуждены иметь полный набор сущностей из модели. Хотя далеко не всё им надо.

Модель должна быть единой для всех сервисов и расшарена между командами. Вам в любом случае придётся договариваться между собой, как должна выглядеть структура данных проекта. Вот модель - это как раз письменный контракт о ваших договорённостях. Там должна присутствовать та самая ИСТИНА, о которой я так долго говорю. А ещё хорошо было бы эту самую истину положить в гит, версионировать, диффануть, разделить на фич-ветки чтобы потом смержить или даже просто обмениваться по емейлу или показать в зуме, чтобы определить, чья истина истиннее.

Три года назад я искал third-party инструменты, которые без боли и страданий позволят работать с моделью.

Их. Просто. Нет.
Их. Просто. Нет.

Вот парадигма есть, а инструментов - нет. Как то самое слово, про которое говорила Фаина Раневская. Я начал понимать, почему подход не пользуется популярностью.

Максимально приближена к моим требованиям была одна софтина немецкого производства, но она больше была заточена под ER-диаграммы, чем под серьёзные проекты. Мне же хотелось большего.

Софтина, кстати, была весьма платная. Что-то около 250 долларов на разраба в год. Сейчас ещё дороже. В бесплатной версии не работает с файлами модели. Отличные условия, да? Просто входной билет в model-first - 250 долларов в год. А лучше ничего нет. Изобретайте, пожалуйста, свои велосипеды. Только чтобы там гуй был, поддержка идеологии RDBMS со всеми нюансами, продвинутая синхронизация структуры БД и кодогенерация. Даже на первый взгляд не очень просто выглядит.

Поэтому два года назад я начал писать свой database model editor. С этими самыми, да. Как я вижу, как я чувствую. Тоже с ER-диаграммами (пусть будут), но уже с упором именно на разработку и поддержку сложных проектов. Это же просто - за пару месяцев можно прототип склепать. Чуть сложнее, чем когтеточки, зато не так прибыльно.

За два года решил несметное количество неожиданных проблем. И только сейчас закончил работу над основной идеологией, когда уже можно действительно пользоваться продуктом в реальной разработке. Можно сказать MVP. Сейчас хочу набрать бесплатных тестеров для устранения багов.

Пожалуй, о возможностях продукта нужно будет сделать отдельную статью. Но вот хочется привести пример из последних изменений: я добавил фичу "Named Rows". Это когда часть данных (именно данных, а не структуры) попадает в ваш кодогенератор, позволяя к отдельным объектам из таблицы обращаться в коде по имени.

Там я решил проблему разных окружений, разных языков, разных фреймворков и даже разных подходов. Решил психологическую проблему "можно ли доверять изменение БД стороннему приложению". Я по-своему реализовал "гальваническую развязку" между моделью и её представлением в коде. Там много интересных решений. Но сегодня я хотел рассказать именно о том, почему "model-first". Поэтому на сегодня всё.

Да прибудет с вами Истина. Всем пше згыр!

Комментарии (9)


  1. sepulkary
    12.10.2025 19:11

    А зачем зеркалить модели в базу? Чистая архитектура рекомендует писать бизнес-логику так, как удобно для бизнес-домена, а база висит где-то там, за интерфейсом.

    Понятно, что это идеал, часто бизнес-логика размазана по хранимкам и триггерам, но стоит ли зеркалить...


    1. Kerman Автор
      12.10.2025 19:11

      Ну как зачем? Модель создана для того, чтобы зеркалиться в базу. Это её назначение. Если мы этого не делаем, то у нас DB-first и модель нам не нужна.

      Чистая архитектура рекомендует писать бизнес-логику так, как удобно для бизнес-домена, а база висит где-то там, за интерфейсом.

      Так model-first и не вступает в противоречие с DDD. Они вообще на разных уровнях. Model-first решает проблему поддержки консистентности структуры базы и кода. В вашем случае можно было бы использовать, ну назовём это "3d model first". Когда предметная область задаёт структуру данных, то есть модель, а из модель подтягивает структуру БД до истинной и генерит код.


      1. sepulkary
        12.10.2025 19:11

        Если я правильно понимаю чистую архитектуру:

        • бизнес-логика - это то, чем люди будут заниматься даже при отсутствии какой-либо вычислительной техники вообще; скажем, банкиры будут считать сложный процент, а астрофизики - параметры звёзд, даже если у них останется только бумажка с ручкой; соответственно, при создании модели вообще про такие вещи, как зеркалирование, SQL, NoSQL думать нельзя, это инфицирует бизнес-логику чуждым кодом;

        • можно сказать, что вообще "никто не first"; Роберт Мартин особо подчёркивал (и даже гордился), что БД - это такой же легко заменяемый сегмент внешнего круга ПО, как, скажем, веб-интерфейс (с учётом реального положения дел, такое, конечно, можно встретить нечасто), и "проблемы базы шерифа не волнуют";

        • консистентность и правда хромает.

        Но это, разумеется, только один из подходов к проектированию ПО, абсолютизировать его было бы неразумно.


        1. whoisking
          12.10.2025 19:11

          Не работает эта чистейшая архитектура на практике без "инфицирования бизнес-логики чужим кодом". Начиная с простого примера - уникальности сущностей. Создаём констрейнты и в этот момент уже смешиваем слой бд с бизнес-логикой. Как мы должны поступить в чистой архитектуре? Писать код, который будет проходить по всем записям и проверять, есть ли эта сущность уже в хранилище?


        1. Kerman Автор
          12.10.2025 19:11

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

          Без вычислительной техники - возможно. Без схемы данных - неа.

          Бизнес-логика просто ничего не сможет сделать без модели. У всех звёзд одни и те же параметны, но разные. Процент пишется одними и теми же цифрами по одному и тому же правилу, от такой же цифры слева от процента. И получится абсолютно прямоугольная таблица, только нарисованная ручкой.


          1. sepulkary
            12.10.2025 19:11

            Без структур данных действительно мало что получится. Цитируя Торвальдса, "Плохие программисты беспокоятся о коде. Хорошие программисты беспокоятся о структурах данных и их взаимосвязях".

            Но структура данных ≠ схема данных. Возможно, вам для бизнес-логики оптимально подойдёт красно-чёрное или префиксное дерево, и заранее засовывать его в прокрустово ряляционное ложе было бы, на мой взгляд, большой ошибкой.


  1. kmatveev
    12.10.2025 19:11

    Блин, как же приятно читать текст, написанный живым человеком!

    Наша команда реализовала model-first, используя protobuf-описания в качестве модели. Из них генерируются классы на C++ и Java, из них же генерируются таблицы в базе, protobuf используется как формат сериализации. Из недостатков: нет описания связей, нам пока норм, в будущем можно будет доработать плагины, чтобы внести эту функциональность.


    1. Kerman Автор
      12.10.2025 19:11

      Спасибо. Я для написания прошлой статьи делал систему, где всё наоборот. Была модель, а из неё делались entity классы и proto-файлы. Вот репозиторий. Туда я тоже джойнов не завёз, хотя в оригинальной системе они были.


  1. whoisking
    12.10.2025 19:11

    Вот парадигма есть, а инструментов - нет.

    Знаю такой один, правда, завязан на постгрес

    https://docs.geldata.com/reference/datamodel

    (Ранее назывался edgedb)