Обзор доклада "Меняем Spring Data JPA на Spring Data JDBC!"

January 3, 2023

Привет!

Посмотрел Меняем Spring Data JPA на Spring Data JDBC! и хотя докладчик в начале сказал, что не призывает использовать JPA, мне что-то захотелось написать микропост в защиту Spring Data JDBC (далее - просто JDBC).

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

Так вот JDBC - это, конечно же, не магическая пуля, где всё получается легко и просто. Но у меня большинство проблем не совпадает с теми, что подсвечены в докладе. Потому что мой подход к разработке совпадает с DDD и, как следствие, с JDBC. Поэтому у меня проблемы там где и DDD и JDBC традиционные работают плохо - выборка данных для UI и, в особенности, для админок.

Для того, чтобы решить проблемы с выборкой для UI я стараюсь собирать агрегаты в одну таблицу/или представление. Где-то я денормализую базовую схему данных и использую составные колонки для сущностей (массивы и jsonb), где-то создаю денормализованные представления.

А вот сделать выборку данных для админки с динамической фильтрацией, сортировкой и пагинацией в JDBC 2 было просто невозможно. В недавно вышедшем JDBC 3 завезли SpEL в @Query и Query by example и стало чуть получше, но всё равно это очень ограниченные инструменты и по ночам мне иногда снятся хорошие сны с JPA-шными Specifications.

И тем не менее, когда я просыпаюсь - я иду делать проекты на JDBC. Потому что я утверждаю, что поддерживать модульные проекты с функциональной архитектурой на порядок проще/дешевле/приятнее, чем большие комья грязи с бесконтрольным изменением состояния. А такие ключевые особенности/требования/лучшие практики JPA, как:

  • связный граф сущностей, отражающий структуру БД;
  • ленивая загрузка;
  • обязательное наличие сеттеров у сущностей;
  • дёрти-чекинг.

исключает и возможность разбить систему на модули и использовать функциональную архитектуру.

Тем самым обрекая JPA-based приложения на превращение в большие комья грязи, так как они убирают какие бы то ни было барьеры, ограждающие программиста о того, чтобы проскакать по ссылкам из одного конца графа в другой, там через сеттер проставить какое угодно состояние и потом незаметно его закоммитать.

Надеюсь, в 23-ем году я осилю развернуть эти тезисы и написать полноценные посты о том, почему модульные программы с функциональной архитектурой существенно проще поддерживать, и о том почему бессмысленно пытаться писать модульные программы с ФА на базе JPA.

Теперь поехали к докладу. Наверное, стоит его сначала целиком посмотреть и только потом уже возвращаться к моим заметкам.

Мы говорим именно про переезд ["хорошего" энтерпрайзного приложения]. Не надо этого делать. Чтобы не говорил анкл Боб, базы данных и фреймворки - это не незначительные детали. И у JPA и JDBC существенно разные модели программирования (JPA использует модель огромного графа изменяемых объектов, а JDBC - набор коллекций слабосцепленных неизменяемых агрегатов) из-за различия которых и будут все дальнейшие WTF-ы.

И если в случае JPA мы делали репозитории для каждой сущности. То мы были не правы.

За состоянием наших объектов БД никто следить не будет. Наброс: изменяемое состояние зло и его количество в системе надо минимизировать. И JDBC рекомендует использовать неизменяемые классы для сущностей.

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

К сожалению many-to-many у нас тоже нет. Между сущностями many-to-many на самом деле встречается не так уж и часто. Покрайней мере реже, чем между сущностями и справочниками. А такие связи вполне можно денормализовать в представление и сильно упростить работу с ними.

И N+1 запрос это естественное состояние JDBC. Это не совсем так. Когда вы выбираете 1 агрегат, то запросов будет 1+K, где K - это кол-во связей (а не связанных элементов, как в случае JPA). Кроме того, что эффективнее - K запросов или вытягивание из БД декартова произведения К таблиц - невозможно сказать без замеров на конкретных данных. Но вот если выбрать N агрегатов, то да вы получите 1+N*K запросов.

Если у вас раньше идшник назначался через сиквенс, вам придётся написать немножко кода. Для меня странное утверждение. Я, честно говоря, никогда не генерял ИДы сиквенсами (я сейчас это делаю через IDENTITY, а раньше через SERIAL), но если уж очень хочется погенерять руками - что мешает сделать id bigint default nextval('sequence')?

Поэтому либо явный инсёрт, либо колбэк. Либо @Version - работает как часы. Либо реализовать Persistable. Либо запилить что-то кастомное через EntityInformation.

Генерируем Q-типы при помощи QueryDSL. Во-первых, QueryDSL с JDBC дружит какая-то третья контора. Во-вторых, она немного отстаёт. У меня был забавный случай - давал разработчику задание потыкать QueryDSL. Возвращается, говорит - 1-к-Н не работает. Я говорю - "Да как не работает-то? Вон в доках написано что работает!". Смотрю когда написали - на тот момент (начало сентября 2022 года) написали 8 дней как. И это была последняя фича, которую они запилили - после этого только JDBC 3 поддержали. В общем будьте осторожны с этой либой.

Наши запросы свалятся в рантайме. Тесты - очень рекомендую. У меня ни разу не было такого, чтобы это мне доставляло проблемы. Зато проекты у меня стартуют за 2-3 секунды, а не минуты.

А вот последний оператор вызывает некоторые вопросы. У меня - вообще не вызывает. Для меня репозиторий - это аналог изменяемой мапы неизменяемых деревьев объектов. И если я туда складываю дерево в одном состоянии (без визитов), то я ожидаю, что загружу я его в том же состоянии (без визитов). О том как с такой картиной мира вообще жить и почему так жить на самом деле проще можно посмотреть в двух очень крутых докладах: Are We There Yet и Immutable Relation Data. Сюда же можно подойти и со стороны DDD: агрегат должен быть всегда консистентен. Если вы удалили визиты - значит таково теперь состояние агрегата. Вобщем не надо переезжать с JPA на JDBC, до тех пор, пока у вас картина мира не переключилась на DDD-шную и/или функциональную.

Ровно потому что Batch операции не поддерживаются. В JDBC 3 их завезли.

Мы должны ЯВНО сохранять эти сущности. И это прекрасно. Потому что неявное сохранение - это побочный эффект. А каждый Чистый программист знает, что побочные эффекты это ложь и их надо избегать (Clean Code, Chapter 3, раздел Have No Side Effects).

Может привести к неприятным последствиям в слое бизнес-логики. Если слой бизнес-логики следует функциональной архитектуре - не может. Опять же - не надо переезжать с JPA на JDBC. Надо выполнять полноценный реинжиниринг всей системы. После того, как JPA уже парализовало разработку, естественно. Если [ещё] нет - лучше ничего не трогать:) Ну и вообще там код процедурный странный - зачем лезть в сервис с ИД-ом? Эта логика должна быть внутри сущности/агрегата.

В JPA объектная модель первична. По моему опыту - вообще не так. В тех проектах с JPA, что я видел на практике объектная модель полностью отражала схему БД ещё и с дублированием (двунаправленные связи). И я полагаю, если от этого попытаться отказаться, то потеряется вся простота и удобство JPA.

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

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