"Я всегда прав, на этот раз прав как никогда". Linus Torvalds
Стоит сразу сказать, что задача этой статьи не в том чтобы кого-то обидеть, а в том чтобы развенчать миф “нормально делай – нормально будет” в контексте Spring Data JPA. Неконтролируемый паровоз движется в случайном направлении. Можете считать это криком души, моим “хватит”!
@Entity это плохо
Энтити это чудовищное, неполноценное изобретение. Энтити просто не позволяют реализовать все необходимые сценарии. Энтити фиксируют схему, что фактически означает монопольный доступ к СУБД. Кстати, как насчет запроса к промежуточной таблице в случае n:n связи? Чтобы сделать миграции, вам придется использовать sql-мигратор, a.k.a flyway. Также энтити не дадут вам sql-проекций из коробки. Если учесть все эти минусы напрашивается закономерный вопрос, а зачем нам @Entity. Ответ прост, вы добавляете энтити потому что без них не запустится Spring Data JPA.
Сложность получения проекций базы данных
Вообще, думаю мало кто поспорит (пишите в комментарии), что сделать проекцию с базы данных должно быть просто, даже структурированную. Но постойте, вам придется использовать Spring Data JPA! Выбирайте на любой вкус: JPQL (весьма ограниченный), EntityGraph (ну это вообще шутка), Criteria API, программирование на строках в аннотациях Spring, ваше любимое программирование на интерфейсах, программирование на именах методов, возможно еще XML или квери-билдер. Часто это кончается тем, что люди забивают на все это и просто отправляют полный EntityGraph на фронтенд, пример: https://github.com/spring-petclinic/spring-petclinic-rest/tree/master.
Тем временем когда аналитики пишут 100кк проекций в секунду на превосходном SQL с complete набором функций, попивая куба-либре на Мальдивах, нам приходиться с этим возиться.
Невероятное количество документации и технологий
Hibernate это база Spring Data JPA, он один вносит 600+ страниц демеджа документации и миллионы строк кода. Думаю если бы кто-то решил распечатать полную документацию на Spring Data JPA и его друзей, лесам Амазонки пришлось бы тяжко. Функциональность этих библиотек постоянно пересекается, то есть надо тратить мозго-часы на решение задачи выбора. И всё вот это вот ради того чтобы делать тривиальные вещи, как мы сюда попали? Сравните это с JDBC + SQL, на которых можно сделать всё, почему мы так много платим?
Отвратительное дебагабилити
Абсолютно неясно что там Spring сгенерировал и как всё это отлаживать в общем случае. Вся валидация исключительно в Runtime, всё работает на Reflection API. Вставляются специальные костыли чтобы это работало с AOT Java Compiler. Вам нужно прожать все кнопки в приложении чтобы убедиться, что вы где-то неправы, это называется руками жар загребать (стакан водки и за станок).
Названия методов как язык запросов
Нарушает Java naming convention. (в принципе дальше можно не читать)
Write-only.
Ограничено одной таблицей.
И зачем было начинать?
Пример в студию (https://vladmihalcea.com/spring-data-query-methods/):
List<PostComment> findAllByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn(
Post post,
PostComment.Status status,
String reviewPattern,
int votes
);
Автора статьи заботливо указывает:
TL;DR, Don’t write query methods that cannot even fit on the screen.
Лично на мой экран не влезло.
20 лет нерешенных проблем с оптимизацией
Сколько нужно еще времени чтобы решить проблемы с оптимизацией? Может ещё 20? Не говоря о том что у этой штуки жуткий code-base, на самом деле получилось так, что проблемы с оптимизацией решать придется нам, контролировать форму запросов, профиль нагрузки и т.д. Это помимо того, что нам еще и в СУБД эти запросы придется оптимизировать. Умножаем сложность. А вот если просто писать native-query в СУБД, этой проблемы нет. Лично по моему мнению вообще все эти затеи с хранением какого-то состояния базы данных на Java-сервере, это химера, но об этом дальше.
Ортогональность приложения. Контроллер, сервис, репозиторий, дао, энтити, мапстракт
На типовом проекте вам нужно поменять порядка 10 файлов для того чтобы добавить/изменить хоть какую-то функциональность. Не зря люди стараются убежать от этой реальности в low-code решения. Переиспользование энтити в разных контроллерах/сервисах уменьшает ортогональность (single-responsibility). Даже самим java-программистам в лом писать эти груды кода, что приводит к экономии на декомпозиции и появлению вот таких вот карликовых монстров, сервисных классов с 24 методами: https://github.com/spring-petclinic/spring-petclinic-rest/blob/master/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java . И при этом мы, не замечая бабайки, продолжаем рассказывать про SOLID принципы на собеседованиях.
Стейтфул глина. Транзакции
Современное программирование стремится к большей декларативности кода, явности и минимизации количества состояний системы. Здесь же мы получаем полный букет в виде: Entity Life Cycle, Implicit границ транзакций, мутных propagation.requires_new, потенциальных неявных savepoint’ов, detached коллекции, кеша второго уровня (который думает что он один в системе). Видя ваши циклы в которых вы месите коллекции , “Senior”-разработчики думают, что они не зря писали свои лабы на паскале именно в таком стиле.
Вывод
Паровозик прицепил уже слишком много вагонов и мосты (спины программистов) уже вышли из проектного режима эксплуатации. Надо что-то с этим делать. Давайте думать, _, подсказывайте. Что вы мозги _, подскажите как _ сделать _, по красоте! (Паша Техник)
Комментарии (22)
Kill_Voice
20.11.2024 13:48Spring Data это реально швейцарский нож, просто проблема, что ножом то как бы тоже можно неправильно пользоваться. Если у вас MSA с компактными сервисами то почему бы и нет, быстро и надежно, если монолит то возможно к использованию есть вопросы, но опять же очень много аспектов, не всё не так однозначно
bugy
20.11.2024 13:48Это старый добрый холивар: ORM vs no ORM?
С очень слабыми аргументами и отсутствием альтернатив.
Я так понимаю, вы против ORM (или hibernate/JPA) в целом, поскольку spring data это всего лишь обертка поверх. Тот же
@Entity
вообще в JPA спеке.Написание SQL ручками? И вы утверждаете, что проблем:
нужно поменять порядка 10 файлов для того чтобы добавить/изменить хоть какую-то функциональность
Вся валидация исключительно в Runtime,
с чистым SQL нет?
Если у вас есть 10 запросов, и вы добавили поле, будьте добры все 10 запросов обновить, даже если вы поле всего лишь читаете, чтобы положить в POJO. А если забыли ещё запятую в конце добавить, то о проблеме вы тоже узнаете только в рантайме.
Кстати, как насчет запроса к промежуточной таблице в случае n:n связи?
А как насчёт этого запроса, если у вас промежуточная таблица - другой (микро)сервис? Или такую проблему уже решить нереально? А если реально, то почему такой же подход нельзя использовать для другой таблицы, но в том же приложении?
В общем: "нормально делай, нормально будет"
ПС я главный хейтер спринга в компании. И не то, чтобы фанат hibernate, но как говорится "
ДемократияORM – наихудшаяформа правлениятехнология для работы с БД, если не считать всех остальных."BugM
20.11.2024 13:48ДемократияORM – наихудшаяформа правлениятехнология для работы с БД, если не считать всех остальныхJooq же. Отличная промежуточная технология.
bugy
20.11.2024 13:48Может быть, не работал с ней, не могу дать практический отзыв.
Но как будто бы, количество бойлерплейта там всё равно слишком велико для меня. Но безусловно, она решает многие недостатки sql и выбор между jooq/orm сложнее, чем sql/orm.
Serge1001
20.11.2024 13:48Да что тут думать, hibernate как и spring data jpa НЕ нужны, это лишь дополнительный слой абстракции замедляющий работу. Видимо их используют те, кто не смог в чистый SQL (но как вы тогда собеседование прошли? Задачки по SQL теперь постоянно спрашивают на лайвкодинге)
Пока что лучший вариант для меня - spring jdbcTemplate, где можно писать нативные запросы
bugy
20.11.2024 13:48Видимо их используют те, кто не смог в чистый SQL
Когда я только начинал карьеру программиста, я слышал похожую фразу: "джаву используют те, кто не сумел в c++". Степень адекватности фразы для меня одинаковая. Продолжая аналогию: "машину используют те, кто не сумел в ходьбу".
Я поработал на продукте, где люди придерживаются вашего мнения и пишут на sql. Проблемы орм мне показались цветочками:
На обновление модели: будь добр обновить все запросы
Если ты их не обновил, то когда-нибудь что нибудь на проде не будет работать. Но это не точно
Кривые и неоптимальные sql запросы. Потому что это так классно писать сложные запросы в бд, вместо нормальной архитектуры
Serge1001
20.11.2024 13:48Продолжая аналогию: "машину используют те, кто не сумел в ходьбу".
Так если бы машину... А то мне тут вместо ходьбы (SQL) предлагают трактор (spring data jpa). Нет уж, я лучше пешочком.
На обновление модели: будь добр обновить все запросы
Кривые и неоптимальные sql запросы.
Так ведь лучше когда сам пишешь, сам оптимизируешь. Если за тебя пишет фреймворк, то потом ещё больше времени на отладку этого всего потратишь, при каждом изменении.
Интересно было бы проверить у тех, кто топит за hibernate/data jpa, как у них со знанием SQL, есть подозрение что люди разучились ручками писать сложные запросы.
ris58h
20.11.2024 13:48Сложные это какие? И как из одного следует другое?
JPA ограничен в выразительности, отсюда следует, что написать на нём некий произвольный запрос будет сложнее.
Serge1001
20.11.2024 13:48Да хотя бы любой select длиннее пяти строчек.
Знаю некоторых программистов, которые давно разучились и такое писать, поэтому используют/пропагандируют ORM (hibernate/spring data jpa)
ris58h
20.11.2024 13:48Да хотя бы любой select длиннее пяти строчек.
Конкретный пример, пожалуйста. И пример кода на Spring Data JPA, который бы делал то же самое. Потом оценим насколько легко написать этот код и насколько сложно забыть как это сделать на чистом SQL.
bugy
20.11.2024 13:48SELECT o.id o.creation_date, o.number, o.contractor_id FROM order o WHERE o.id = :user_id
Пойдёт?
Я пропагандирую
repository.findById(id)
вместо этих 6 строк.
ALexKud
20.11.2024 13:48Вся и проблема имхо что основной массе нативный SQL использовать сложно ввиду того что мозги там надо поворачивать в сторону алгоритмизации работы с наборами данных а не с линейными алгоритмами обработки на фронтенде для бэкенда. Web испортил многих и кроме трех операторов SQL в ограниченном контексте мало кто знает сам SQL хорошо. Отсюда и растут ноги ORM. Для. Web приложений хватает и ладно. Хуже то что все эти web примочки пацаны тянут в приложения , которые работают в чистом локале, напихивают туда докеры, Джанги и проч., без необходимости, а потом тихо сваливают на сторону с указанием в резюме на "опыт", оставляя другим свои проблемы доработок, зависимостей и рефакторинга. Я имею ввиду не продажные приложения, а внутренний софт технологических и производственных компаний
png
20.11.2024 13:48Посыл правильный - Hiber устарел. ему давно пора на свалку истории.
Но с аргументацией проблемы.
Например, я не понял, чем плох Entity (точнее, я знаю, что там не так, но в статье про это не говорится).
Чем плох flyway? Чем плох рефлекшен и рантайм? проверять качество работы c SQL кроме как в рантайм не где.
Про проблемы работы с вложенными сущностями - ничего нет.
Про проблемы работы с кешем 2 уровня - ничего нет.
Про проблемы работы с java классами - тоже ничего нет.
Дальше, критикуются генерируемые методы в интерфейсах, но это не Hiber и не JPA, это spring data. Это другой фреймворк, на базе его есть Spring Data JDBC или Spring Data Mongo. Ну кто-то написал дикий г-код, мы же не критикуем за это язык программирования?
Дальше, чем не угодили слои приложения. Отделяйте мух от котлет, в хорошей крупной системе слои приложения очень даже нужны. Да, пример из спринга - плохой с точки зрения. ну давайте будем честными, многое в спринге написано отвратительно. Он не ОПП, не SOLID, а лишь имитирует их. Ну и что. Странно...
Итого, чтобы не было недомолвок. Я считаю, что на spring data jpa можно собрать приятный достойный проект. Но очень много старой функциональности, которая уже сильно устарела и надо сильно думать, что из JPA стоит использовать, а что пора выкинуть. Лично мне не нравится JPA с точки зрения парадигмы. На своих проектах, где я влияю на выбор технологий, я стараюсь его не использовать.
ris58h
20.11.2024 13:48Посыл правильный - Hiber устарел. ему давно пора на свалку истории.
Даже интересно стало на что его нужно по-вашему заменить. Вдруг мне тоже надо.
kacetal
20.11.2024 13:48Господи, на что люди готовы пойти лишь бы не использовать jooq. Даже на изучение jpa спецификации и всех неявных подводных камней хибера.
А вообще если время тратить некуда то лучше уж изучить sql и спокойно писать на jooq.
Slobodator
20.11.2024 13:48Ок, короткая шпаргалка, что такое JPA, зачем и как с ним работать.
JPA -- это Java Persistence API, фреймворк, который позволяет мапить Java объекты на реляционную базу данных.
Что это значит на практике? Допустим, у нас есть такой класс (и объекты -- экземпляры этого класса)
public class Order { private Map<Item, Integer> itemsAndTheirAmounts = new HashMap<>(); private Set<PromoCode> promoCodes = new HashSet<>(); private Instant createdAt = Instant.now(); private Money totalPrice = Money.of(0, Monetary.getCurrency("RUB")); }
... в
Item
иPromoCode
, в свою очередь, наверчено ещё что-то и т.д.Пока мы работаем с ними в оперативной памяти -- всё ок, у но как только начинаем работать с внешним миром, нужно их как-то различать, для этого добавляется ID.
Entity = доменный класс + ID
Поскольку оперативная память всё ещё ограничена и энергозависима, нужно сохранять (persist-ить) эти объекты где-то, например, в реляционной БД.
Далее, есть такой подход как ООП -- объектно-ориентированное программирование. Одной из его идей является инкапсуляция -- это когда поля закрыты для изменений напрямую, но есть методы, которые гарантируют бизнес-целостность объекта.
В нашем примере это могут быть
addItem(...)
,applyPromoCode(...)
и т.д., которые изменяют состояние объекта, в том числе и пересчитываяtotalPrice
, благодаря чему (и юнит-тестам на них, конечно) мы можем быть уверены, что объект всегда консистентен.Ещё раз, если у нас есть
addItem(...)
иremoveItem(...)
с соответсвующими проверками, есть гарантия, что количество товаров положительное, а итоговая цена пересчитана правильно.Далее, все изменения состояния объекта хорошо бы отобразить обратно в БД. Этим и занимается JPA, что-то вроде
class Service { @Transactional void someMethod(...) { var entity = repo.findById(id); entity.update(...); // нет, repo.save(entity) здесь не нужен } }
Единственный ли это подход? Конечно же нет.
Ещё можно вызывать разные SQL команды, а можно вообще ничего не перегонять на бекэнд, а всё делать сразу в БД с помощью хранимых процедур. У каждого подхода есть свои преимущества и недостатки.
К преимуществам JPA можно отнести, что бизнес-логика пишется на высокоуровневом языке Java, проверяется unit-тестами, а DML операции будут произведены фреймворком.
Ну, а недостатком считается факт, что в частных случаях с помощью SQL можно добиться большей производительности.
Однако, тут есть диллема. Допустим, мы хотим увеличить зарплату (правильнее, конечно, говорить ставку) сотрудников некоторого отдела на 10%. Какой подход лучше
@Transactional void increaseSalary(...) { List<Employee> employees = repo.findAllByDepartment(...); employees.forEach( e -> e.increaseSalaryByPercent(10); ) }
... т.е. "выгрузить данные из БД, изменить, записать обратно по одному" или bulk update
update emloyees set salary = salary * 1.1 where department_id = :department_id;
...?
Очевидно, что вторая команда быстрее.
Но кто сказал, что метод
increaseSalaryByPercent(...)
настолько прост? Там могут быть (сейчас или добавятся потом) проверки на граничные значения, правила округления и т.д.Кроме того, написав sql update мы создали вторую точку изменения
salary
, теперь придётся всегда об этом помнить и держать их в согласованном состоянии.Всё рассуждения выше были про изменение состояния объектов, JPA в первую очередь об этом.
Кроме этого обычное занятие приложения это отображать их состояние AKA "читать данные из базы".
С объектной точки зрения корректный путь здесь -- это загружать объекты из базы и преобразовывать их в DTO.
Однако, очевидно, реляционные отношения и SQL предоставляют больше возможности и производительности. Если необходимо этим можно и нужно пользоваться, плата за это -- как в примере выше с bulk update -- поддержание ДВУХ подходов в синхронизированном состоянии.
P.S. Spring PetClinic -- довольно плохой пример использования JPA. Так делать не надо.
Gremlinquisitor
А в чём посыл статьи? Ну поворчать ТС поворчал. Дальше что? Предложения будут? Кому надо - Query. Кому надо - хранимые процедуры и функции. Но как-то оно с Entity уже привычно и покрывает довольно много задач. Больше бы конкретики к этим "фи", а так какой-то вброс, а не статья.
sshikov
Да просто статья начинается вот с этого. Кем считается, почему считается? Непонятно, откуда взят данный постулат. Это раз.
И второе, наверное даже более важное: а автор вообще швейцарским ножом пытался когда-нибудь делать серьезную работу? Швейцарский нож - это карманный универсальный инструмент, который плохо делает почти все, что умеет. Хлеб им порезать проблематично, пассатижи там если и бывают - то гавно, отвертка - тоже. Поэтому само сравнение не совсем корректное - если предмет данной статьи делает хорошо хоть что-нибудь - то просто определите для себя, в каких задачах вы его употребляете, а для других задач найдите инструмент более подходящий, благо их существует достаточно много. Даже голый JDBC может быть осмысленным вариантом где-то (хотя я лично остановился на Spring Jdbc Template).