Сегодня я буду говорить о Spring Data JDBC, и мы попытаемся укротить этого зверя, как и обещает название. Я понимаю, что вы, вероятно, уже немного устали, это последний доклад дня, поэтому прошу набраться терпения. Я постарался сделать презентацию максимально лёгкой для восприятия.

⚠️ А если хочется глубже погрузиться в разработку реальных приложений, у нас в Академии скоро стартует новый поток «Java в продакшене»: https://spring‑aio.ru/java_in_production

Несколько слов обо мне. Я инженер‑программист и пишу много кода, прежде всего на Java и Kotlin, но также и на других языках. Время от времени я участвую в проектах с открытым исходным кодом, в том числе в проектах Spring, Spring Data и Spring Data JDBC. Кроме того, я пишу технические статьи для таких ресурсов, как Baeldung, который большинство из вас наверняка хотя бы раз читало — по крайней мере, я на это надеюсь. Ещё я выступаю на конференциях, включая Spring I/O и Devoxx, например.

Прежде чем начать, я хочу подчеркнуть одну вещь. Мне лично нравятся и JPA, и Spring Data JDBC. Я считаю, что оба решения хороши в подходящих для них условиях, и сегодня мы обсудим, что это за условия, по крайней мере, с моей точки зрения.

Природа ORM в Java мире

Начать свой доклад я хочу с того, чтобы постараться провести для вас чёткую границу, где, на мой взгляд, хорошо себя показывает Spring Data JDBC, а где Hibernate.

Когда мы пишем бэкенд‑приложения, нам обычно приходится работать с базой данных. У Java‑разработчиков выбор не так уж велик. Чаще всего мы используем реализацию JPA, обычно Hibernate, и у каждого из нас сложились собственные отношения с Hibernate. Думаю, объяснять это не нужно.

Через некоторое время кто‑то в команде говорит: 

«Посмотрите, у команды Spring Data в наборе решений есть ORM Spring Data JDBC. Может быть, стоит попробовать? Вдруг это действительно отличный вариант. Почему бы и нет?»

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

  • Где моя связь many-to-one

  • Где же many-to-many

  • А как сделать FetchType.LAZY?

Изучив документацию, разработчики понимают, что Spring Data JDBC всего этого не предлагает. В некоторых сообществах даже принято думать, что разработчики ORM каждый вечер ложатся спать с одной мыслью: как бы завтра ещё сильнее усложнить жизнь другим разработчикам. 

Но так бывает не всегда:)

Так о чём же этот доклад? Сначала я объясню, чем, на мой взгляд, является Spring Data JDBC и чем он не является. Затем расскажу, где Spring Data JDBC особенно полезен и в каких случаях его стоит применять. Наконец, мы поговорим о проектировании агрегатов в Spring Data JDBC и о правильных подходах к этому с учётом моего опыта.

Как мы разрабатывали ПО в 2005 году

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

Допустим, что нам надо написать бэкенд приложения, а его предметная область — это домен «Java конференции», вроде той, на которой мы сейчас находимся, например Spring I/O. 

Любая предметная область состоит из набора сущностей, кторые тем или иным образом связаны между собой. Что это могут быть за сущности? 

Во‑первых, например, это может быть билет на конференцию, то есть тот самый билет, который вы покупаете, чтобы сюда попасть. 

Во‑вторых, есть сессии, то есть доклады. Сейчас вы как раз присутствуете на одном из них и слушаете меня. У доклада есть хотя бы один докладчик, а иногда их несколько. При этом один докладчик может выступить с несколькими докладами, поэтому между докладчиками и докладами существует логическая связь many-to-many. Подчеркну: эта связь именно на уровне предметнйо области. Её не обязательно представлять в базе данных отдельной third‑party таблицы, но на уровне предметной области она существует всегда.

У доклада также может быть несколько отзывов. Участник конференции может оставить отзыв, поэтому между этими двумя сущностями существует логическая связь one-to-many или, если смотреть с другой стороны, many-to-one

Так как бы мы, Java разработчики, спроектировали бы такую систему в 2005 году? 

Скорее всего, у нас было бы монолитное приложение на Java EE, вероятно, с фронтендом на JSP (Java Server Pages), и одна большая база данных, почти точно, реляционная. В ней находились бы сотни таблиц (если мы говорим про реальный бизнес домен) и довольно сложная схема. Для работы с базой мы использовали бы какое‑либо ORM‑решение, вероятнее всего Hibernate. Hibernate уже существовал в 2005 году и к тому времени был достаточно стабильным инструментом. Состояние всей предметной области хранилось бы в одной базе данных. Так это работало тогда, а в довольно большом количестве легаси систем работает и сейчас.

Тем не менее, такой подход создаёт несколько проблем. 

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

Тем не менее, наличие всего состояния нашего домена в одном месте влечёт за собой и другие последствия. И вот какие.

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

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

Если немного подумать, то такая свобода при формировании запросов возникает именно потому, что все данные собраны в одной базе. И наш ORM должен быть достаточно умным и способным, чтобы уметь работать с БД по‑разному. 

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

Иногда данные стоит кэшировать, а иногда нет. Например, расписание конференции меняется крайне редко, поэтому его, вероятно, можно где‑нибудь закэшировать (допустим, в кэше второго уровня Hibernate). Конкретный механизм сейчас не важен.

То есть, кэширование это тоже одна из возможностей, которая нам нужна. Можно задать справедливый вопрос — «а точно ли ORM должен это делать?», и ответ на него требует отдельного большого доклада. 

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

Когда Java приобрела популярность как средство создания монолитных приложений, Hibernate заслуженно был очень хорошим решением. Огромная сложность схемы порождала множество разных способов доступа к данным, и Hibernate усложнялся в ответ. JPA действительно хорошо решала эту задачу.

Рост сложности и предметно‑ориентированное проектирование (он же DDD)

Но это всё проектирование ПО в 2005 году. Что происходит дальше и почему вообще DDD?

Дело в том, что монолитные системы, по мере своего развития и усложнения, скорее всего, со временем превратится во что‑то подобное:

Вероятно, кто‑то из вас узнаёт здесь собственные проекты. Я точно узнаю несколько своих:)

Шло время, и люди стали осозновать — с ростом сложности мы уже не можем беспорядочно читать и сохранять данные: такой подход приводит к хаосу

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

Здесь и появляется предметно‑ориентированное проектирование, или Domain‑Driven Design (а позже и CQRS). 

Я настоятельно рекомендую прочитать эти две книги (книгу Вон Вернона точно), но для нас сейчас важнее другое: DDD стал потихоньку входить в массы. И теперь, мы проведём ещё один мыслительный эксперимент. Допусти прошло десять лет, и мы снова должны, с нуля, спроектировать такую систему уже для другой конференции.

Как мы разрабатывали ПО в 2015 году

Как выглядела бы эта система, если бы мы проектировали её в 2015 году? К тому моменту Domain‑Driven Design уже закрепился как подход, и что ёщё важно, примерно в тех же окрестностях начала становится популярной микросервисная архитектура. Многие не осознают, что эти две концепции во многом рассчитаны на совместное применение.

В 2005 году наше приложение для конференции и его предметная область выглядели бы как описанный выше монолит. В 2015 году это, скорее всего, был бы набор Java‑приложений на Spring Framework, а возможно, даже на Spring Boot, который к тому времени уже вышел. Единого монолитного приложения у нас больше не было бы, во всяком случае, мы бы к этому не стремились.

Предметная область конференции и набор сущностей остаются прежними, но теперь область распределена между несколькими микросервисами. Сама предметная область не меняется, но разделяется ответственность за неё.

База данных на микросервис

В микросервисах мы обычно следуем паттерну «база данных на микросервис». Что это означает? В монолитном приложении одна база данных принимает на себя всю нагрузку. Если она выходит из строя, перестают выполняться все запросы к данным и система становится недоступной. Для монолитов это было серьёзной проблемой.

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

Паттерн «база данных на микросервис» оказался полезным по нескольким причинам. 

Во‑первых, он создаёт чёткие границы владения. Если данные повреждены или какое‑либо состояние стало неуправляемым, мы знаем, какой сервис отвечает за поддержание инвариантов конкретно этой части домена. Если для данных нужно обеспечить соблюдение тех или иных ограничений, понятно, где именно должны находиться эти проверки. Только сервис докладчиков может хранить сведения о докладчиках, никакой другой сервис делать этого не может.

Кроме того, мы получаем инкапсуляцию. Разработчикам сервиса докладов не нужно знать, как именно хранятся докладчики. Это может быть реляционная база данных, Cassandra или MongoDB, для нас это не имеет значения.

Такой паттерн также заметно упрощает миграцию. Если мы хотим перейти с Oracle Database на PostgreSQL, больше не требуется за один раз переносить огромную базу с сотнями процедур и таблиц. Можно сначала перенести базу одного сервиса, затем другого и так далее. Это лишь одно из множества преимуществ паттерна. Он уже стал общепринятым, и я уверен, что большинство из вас как минимум о нём слышало, а надеюсь, и использует его.

Ограниченные контексты и агрегаты

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

В зависимости от того, как мы разделили монолит на микросервисы, такую часть предметной области можно назвать ограниченным контекстом (он же Bounded Context). Ограниченный контекст это термин из Domain‑Driven Design. Это достаточно сложное понятие, и у нас сейчас нет времени разбирать все подробности его определения. Сильно упрощая, давайте считать его группой агрегатов набором агрегатов, которые «воспринимаются» вместе.

Как правило, каждый микросервис обслуживает тот или иной ограниченный контекст: часть более крупной модели предметной области, содержащую один или несколько агрегатов. 

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

Строгая согласованность внутри агрегата

Ещё один важный принцип микросервисной архитектуры и Domain‑Driven Design заключается в том, что состояние одного агрегата нельзя разделять между несколькими базами данных. Это нарушило бы строгую консистетность, что противоречит вообще понятию Aggregate в DDD.

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

Если нам нужно хранить состояние агрегата доклада, весь агрегат должен целиком находиться в одной базе данных. Разделить его нельзя. Агрегат доклада, что бы в него ни входило, полностью хранится в сервисе докладов, а агрегат докладчика — в сервисе докладчиков.

Ссылки между агрегатами

Из этого следует ещё одно важное последствие. В монолитном приложении с Hibernate можно было сказать, что у доклада несколько докладчиков, и смоделировать связь many‑to‑many, при которой объект доклада содержит набор объектов докладчиков, и один докладчик может сделать несколько докладов.

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

Их состояние, скорее всего, находится под управлением совершенно другой системы, другого микросервиса. И в микросервисной архитектуре агрегат доклада не может содержать прямые ссылки на объекты докладчиков. Вместо этого он может и должен хранить их идентификаторы. В правильно спроектированной микросервисной архитектуре нам часто не только не следует, но и невозможно хранить ссылку на объект из другого агрегата: этот объект находится в другом микросервисе и не управляется нами (он и не должен!).

Я понимаю, что начинаю с самых азов, но поверьте: мы приближаемся к сути. 

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

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

Если рассмотреть развитие нашей небольшой предметной области с четырьмя сущностями, сначала мы разделяем её на Bounded Context‑ы, а затем проектируем агрегаты внутри каждого из них. Так мы сделали бы в 2015 году, и так многие проектируют системы сегодня.

Почему Spring Data JDBC подходит для микросервисов

К чему всё это? Зачем я это объясняю? В монолитных Java‑приложениях всё доступно по ссылке. Мы можем обратиться к любой связи или таблице. Если нужно обновить докладчика, HTTP‑запрос не требуется: Hibernate или другой ORM‑фреймворк просто выполнит SQL‑запрос.

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

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

Поскольку мы храним Aggergates в рамках Bounded Context‑ов, то связи типа Many‑To‑X представляют собой ссылки между, а не внутри Aggregate‑ов. И скорее всего мы не выражаем их в качестве таблиц в нашей БД, а мы храним идентификаторы на них, так как другая система занимается менеджментом этой части домена.

Также, мы всегда помним, что за функциональность приходится платить сложностью. Некоторые разработчики избегают Hibernate именно поэтому: он обладает множеством возможностей, но при этом сложен, а в микросервисах большинство этих возможностей, вероятно, не нужны.

Поэтому Spring Data JDBC так хорошо подходит для микросервисов. Если вы пишете микросервисы на Java с использованием Spring Framework, Spring Data JDBC это часто хороший выбор

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

Ну что, теперь, давайте, поговорим конкретно про дизайн Aggregates с Spring Data JDBC!

Правило 1. Начинайте с небольших агрегатов

Hibernate предназначен для монолитов. Теперь, когда наша микросервисная архитектура определена, попробуем спроектировать агрегаты. Первое правило касается их границ: как следует проектировать агрегаты в целом?

Агрегаты как деревья

Чтобы на этот вопрос отвтеить, давайте сделаем шаг назад — какова вообще природа агрегата?

У нас есть доклад, например, тот, на котором вы сейчас присутствуете, и у него несколько отзывов. Если присмотреться, эта структура представляет собой дерево с точки зрения компьютерных наук. Кроме того, в таком дереве существуют только связи one‑to‑many или one‑to‑one.

Это довольно очевидно, но объяснение всё же необходимо, потому что разработчикам с опытом работы с JPA может не хватать физических связей many‑to‑many. Связь many‑to‑many не имеет смысла в дереве, так как в таком случае у промежуточного или leaf узла оказалось бы несколько родителей. 

Если у структуры несколько родителей, то это уже не дерево: у неё было бы несколько корней, а такого быть не может, поскольку корень дерева это корень агрегата. Например, один отзыв о докладе не может находиться под управлением нескольких докладов и иметь двух родителей — это ломает домен.

Вставка агрегата

Как Spring Data JDBC вставляет агрегат? Сначала начинается транзакция. Затем вставляется корень агрегата, то есть доклад в нашем случае. После доклада добавляются листовые или промежуточные узлы, то есть в нашем случае отзывы. В завершение транзакция фиксируется.

Чтение агрегата

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

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


С точки зрения базы данных это проблема. Администраторы OLTP баз данных хотят, чтобы транзакции были как можно короче: длительные транзакции надолго занимают соединения с базой и по разным причинам снижают производительность. Эти причины можно обсудить в секции вопросов и ответов, но в целом долгие транзакции в OLTP хранилищах почти всегда плохая идея.

Обновление агрегата

Как Spring Data JDBC обновляет агрегат? Как можно догадаться, сначала открывается транзакция. Затем выполняется оператор UPDATE для доклада: на основании переданного объекта обновляется сам доклад, например, его описание. После этого удаляются все промежуточные и листовые узлы, то есть в нашем случае все отзывы, а затем они вставляются заново.

Даже если требуется изменить только один листовой узел, то есть один отзыв, Spring Data JDBC всё равно удалит все отзывы и вставит их повторно, включая единственный изменённый. Затем транзакция фиксируется. Обратите внимание на последовательность: сначала обновление, затем удаление и повторная вставка.

У такого поведения есть причина. Может возникнуть закономерный вопрос: «Почему бы просто не выполнить обновление нужного Talk Review?» Но как контрибьютор, я могу вас заверить: всё не так просто, и эта проблема гораздо сложнее, чем может показаться.

Для нас важно, что агрегат целиком блокируется на всё время транзакции.

Например, в PostgreSQL выполнение операторов UPDATE или DELETE внутри транзакции не позволяет другим транзакциям изменять те же данные. Две параллельные транзакции не могут одновременно изменять один и тот же агрегат. Т.е чем крупнее агрегат доклада, тем больше блокировок устанавливается и тем ниже становится производительность.

Возникает первый вопрос: кажется, что большие агрегаты создают проблемы, верно? Да, как правило, создают, и немало. Если после этого доклада вы запомните только одно правило, пусть это будет самое важное правило проектирования агрегатов в Spring Data JDBC: начинайте с малого и не пытайтесь перемудрить.

Однажды Джошуа Блох сказал в интервью о проектировании API: “When in doubt, leave it out. You can always add but you can never remove”.

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

Следующий вопрос: «означает ли это, что у каждой сущности должен быть собственный агрегат?» Часто ответ да, означает, по крайней мере в начальной стадии проекта. Если у вас пять, десять или пятнадцать сущностей, все они могут быть отдельными агрегатами. Не объединяйте их без веской причины.

Что это означает для нашего примера? Если есть доклад с несколькими отзывами, изначально доклад и отзывы следует представить как два отдельных агрегата.

Правило 2. Внутри агрегата начинайте с сущностей

Второе правило касается внутреннего устройства агрегата. Обещаю, я больше не задержу вас надолго:)

Сущности и объекты‑значения

Если вы читали книги по Domain‑Driven Design, включая книгу Эрика Эванса, то знаете: объекты модели предметной области обычно относятся к одной из двух категорий — это либо сущности (они же entities), либо value‑object‑ы.

Что такое сущность? Это объект, который можно определить по его идентификатору. Этот идентификатор, согласно DDD, присваивается во момент создания сущности в домене и является неизменяемым напротяжении всего жизненного цикла сущности (об этом ниже). 

Например, у доклада есть идентичность (identity), поэтому его можно однозначно определить. По своей природе сущность изменяема и, следовательно, обладает жизненным циклом. Например, у доклада может быть статус. После отправки его статус SUBMITTED. Вмодели предметной области может быть операция перевода доклада из состояния SUBMITTED в состояние ACCEPTED. Изменяя сущность, мы переводим её из одного состояния в другое. Это и есть жизненный цикл.

Value‑объекты, напротив, «идентифицируются» своим внутренним состоянием. Я беру слово «идентифицируются» в кавычки, потому что, как правило, value‑объекты в DDD не пытаются идентифицировать вообще. По своей природе они неизменяемы: изменить их состояние невозможно в теории, так как у них нет identity и нет жизненного цикла.

У value‑объектов на практике лишь одна задача — представлять собой некое состояние предметной области.

В Java объекты‑значения часто реализуют в виде record‑классов, хотя record в теории может быть и сущностью. Главным критерием выступает именно наличие identity: сущности можно идентифицировать, а объекты‑значения нет. Это важнейшее различие.

К чему я это всё говорю. В книгах по Domain‑Driven Design обычно рекомендуется отдавать предпочтение объектам‑значениям: они неизменяемы, потокобезопасны и скаляризуемы и так далее Я не буду сейчас уходить в детали, у нас на это мало времени, но value‑объекты действительно имеют ряд преимуществ.

Но несмотря на то, что в теории value‑объектыхороши,у них есть особенности, о которых необходимо знать.

Рассмотрим агрегат из нашего примера: доклад с несколькими отзывами. Если строго следовать правилам Domain‑Driven Design, в корне модели всегда должна находиться сущность (по‑другому просто не может быть). Если у этой сущности вообще есть дочерние объекты (а без необходимости их лучше не добавлять!) они должны быть value‑объектами. Такая структура обычно считается хорошим агрегатом.

Обновление объектов‑значений

Так в чём же проблема? Если коротко, то пробелма часто в том, что люди недооценивают свою потребность жизненного цикла объектов внутри Aggregate.

Давайте на примере покажу. Предположим, вот мы решили добавить возможность в системе дать пользователю обновить отзыв по докладу. 

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

Это необходимо потому, что, на практике, мы не можем по‑настоящему отличить один value‑объект от другого — это часто путь вникуда

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

И вот нам и остаётся один путь. Обновление отзыва будет выглядеть примерно так. 

У нас есть транзакционный метод, который принимает идентификатор доклада, то есть идентификатор корня агрегата, и новый snapshot со всеми отзывами: как ранее привязанными к докладу, так и изменённым. С помощью репозитория Spring Data JDBC мы находим нужный доклад по идентификатору, а затем заменяем его отзывы.

Метод replaceTalkReviews должен находиться в самом объекте доклада, потому что, если отзывы входят в тот же агрегат, именно доклад управляет ими. Метод очищает список существующих отзывов и добавляет новые или обновлённые. Найти конкретный отзыв по идентификатору нельзя, поскольку отзывы представлены value‑объектами.

В этом и заключается более общая проблема value‑объектов: на практике их приходится всегда re‑snapshot‑ить, так как по замыслу они все взаимозаменяемы.

Предположим, кто‑то в зале решает обновить свой отзыв после того, как у доклада уже накопилось 150 отзывов. Мы не можем определить, какой именно отзыв нужно изменить. Остаётся удалить всё, заново вставить остальные 149 отзывов и добавить обновлённый.

Чем больше коллекция value‑объектов, тем дороже обходится полное пересоздание её snapshot‑а. Восстановление коллекции с большим количеством элементов может оказаться очень затратным.

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

Value‑Объекты в целом хороши, даже прекрасны, но это скорее следующий этап развития вашей доменной модели. Если представить эволюцию нашего проекта во времени, вначале у нас есть схема с билетами, докладчиками, докладами и отзывами. По умолчанию каждый из этих объектов образует отдельный агрегат. 

Если позднее мы решим объединить два агрегата, например, доклад и отзыв, то сначала смоделируем дочерние элементы в aggregate как сущности, а не как value‑объекты. Корень агрегата всегда является сущностью, потому что у него обязательно должна быть идентичность.

Итоги

Подведём итоги.

Во‑первых, Spring Data JDBC обычно не очень хорошо подходит для монолитных приложений. Если у вас монолит, я не думаю, что стоит рассматривать миграцию: Hibernate прекрасно справляется со своей задачей. Подобный переход, вероятнее всего, создаст гораздо больше проблем, чем решит.

Но если вы проектируете микросервисную систему, Spring Data JDBC стоит рассмотреть, и я даже рекомендовал бы его использовать. Spring Data JDBC заставляет задуматься о модели предметной области, её агрегатах и о том, как вы разделяете монолитное приложение на микросервисы или изначально создаёте эти микросервисы. Здесь он подходит очень хорошо.

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

Наконец, не торопитесь использовать value‑объекты. Я знаю, что их рекомендуют и что в целом они желательны, но не переходите к ним слишком быстро. Необходимость выполнять независимые операции внутри агрегата часто недооценивают, хотя для производительности такие операции нередко критически важны. Value‑объекты это хорошая цель, но не начинайте с них: переходите к ним только тогда, когда это действительно оправдано.

⚠️ А если хочется глубже погрузиться в разработку реальных приложений, у нас в Академии скоро стартует новый поток «Java в продакшене»: https://spring‑aio.ru/java_in_production

Завершение

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

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

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


  1. MasterErik
    08.09.2026 12:38

    Технически безграмотная статья. Пример "Поскольку схема очень сложна и нам нужно поддерживать множество способов доступа к её данным, требуются ленивая загрузка, каскадирование, графы загрузки и множество других паттернов и возможностей. Следовательно, необходим очень функциональный слой персистентности. Именно поэтому Hibernate и JPA были важны ..." тут поменяны местами причина и следствие. ленивая загрузка, каскадирование, графы загрузки и пр.. требуются для Hibernate и JPA, но никак не наоборот! Для работы с базой нужен только SQL и ничего более. С SQL все на порядок проще. Нужно просто его выучить на продвинутом уровне.  Spring JDBC — это не тот механизм, который стоит использовать, он действительно не удачный. Но Hibernate это неудача в квадрате, что в монолите, что в микросервисе. В статье смешено несколько подходов микросервисы и монолиты, Hibernate vs SQL. Это нужно рассматривать изолировано, что не было каши.


    1. maxzh83
      08.09.2026 12:38

      Spring JDBC — это не тот механизм, который стоит использовать, он действительно не удачный. Но Hibernate это неудача в квадрате

      Что же стоит использовать? JOOQ? JdbcTemplate?


      1. MasterErik
        08.09.2026 12:38

        MyBatis, вариант размещением SQL в XML.


        1. maxzh83
          08.09.2026 12:38

          Мда уж, я думал что-то интересное предложите... Нет, спасибо, пользовался. Больше не хочется


          1. MasterErik
            08.09.2026 12:38

            Очень интересно, что не понравилось. Я применял её 8 лет, в начале немного неправильно. После устранения авто генерации, не было ни одной проблемы.


    1. AlexViolin
      08.09.2026 12:38

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


      1. MasterErik
        08.09.2026 12:38

        Промышленные технологии то же бывают неудачными. Тут чистая психология, а не реальные аргументы. Слава богу в эпоху AI психологию можно не учитывать. AI удобнее писать напрямую SQL без магии. На это тратится меньше токенов.


      1. AstarothAst
        08.09.2026 12:38

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