Почему следует избегать использования JPA/Hibernate в продакшене +12


AliExpress RU&CIS

Этот материал является кросс-постом
Следить за обновлениями блога можно в моём канале:
Эргономичный код

Дисклеймер - я люто ненавижу JPA/Hibernate

Мои отношения с Hibernate (JPA тогда ещё не было) не сложились с самого начала - в далёком то ли 2005, то ли 2007, на собеседовании у меня спросили как замапить отношение 1-N в Hibernate. А я ответил "Я не знаю, что такое Hibernate".

Затем в чуть менее далёком 2008 году я устроился в Софтэйдж на какой-то проект на Swing и Hibernate. Коммерческого опыта ни с тем ни с другим у меня на тот момент не было, поэтому мне казалось, что работал я плохо. Я сильно парился на эту тему недели две-три, а потом уволился.

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

В этом посте я изложил факты и свой опыт, а как их интерпретировать - решайте сами.

Философия JPA

Справедливости ради, 90% моего опыта с JPA - это Hibernate (есть ещё немного EclipseLink, но там все те же проблемы). Поэтому в этом посте я буду использовать JPA и Hibernate, практически как синонимы. Но изменяемые сущности и ленивая загрузка - корень причин основных проблем JPA - зашиты в спеку, и я не думаю что можно написать реализацию без приведённых проблем.

Моя вольная интерпретация философии JPA: "Забудьте про базу данных - просто объявите свою объектную модель. Работайте с ней, как будто она вся в памяти. Мы позаботимся о сохранении объектов в БД".

Возможно истинная философия JPA какая-то другая (не могу нагуглить), но эта - точно самая распространенная "в народе".

Упрощённая модель работы Hibernate

Упрощённая модель работы Hibernate
Упрощённая модель работы Hibernate

Для обеспечения обещания "работайте как будто у вас все объекты в памяти" Hibernate работает примерно так:

  1. Приложение начинает транзакцию через entityManager.getTransaction().begin() (транзакции бывают и в памяти и это не противоречит философии JPA);

  2. приложение загружает данные через entityManager:

    1. entityManager формирует запросы и получает строки таблиц через JDBC-Driver;

    2. ORM на основе строк формирует прокси объектов сущностей;

    3. перед тем как отдать приложению, entityManager сохраняет все объекты в Persistence Context;

  3. приложение каким-то образом изменяет объекты через сеттеры;

    1. но т.к. это прокси, то сеттеры заодно помечают объекты "грязными";

  4. приложение коммитит транзакцию через entityManager.getTransaction().commit();

    1. в этот момент entityManager просматривает Persistence Context, сохраняет/обновляет в БД все "грязные" и новые объекты;

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

Достоинства JPA

У всего есть свои плюсы и минусы. Надо признать, что плюсы есть даже у JPA.

Это безусловно самая распространённая технология работы с БД на платформе Java. Из этого вытекает ещё три достоинства:

  • По JPA огромное количество материалов всех видов и на любой вкус. Если использовать JPA идиоматично, то любые проблемы и решения гуглятся моментально;

  • нанять разработчика, знающего JPA не проблема - берёте с любого рынка и с вероятностью 99% он имеет хоть какой-то опыт работы с JPA;

  • JPA поддержано везде, где его можно поддержать. В Котлине, например, сделали специальный плагин для совместимости с JPA.

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

С изолированной задачей сохранения и загрузки графов объектов в БД JPA справляется без каких-либо нареканий.

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

Наконец, именно с точки зрения реализации, это надёжное решение - я не помню чтобы сталкивался с багами в реализации Hibernate.

Но у всего есть и свои минусы и в JPA их тоже хватает.

Проблемы JPA

Корень проблем JPA лежит не в технической, а парадигмальной плоскости. JPA пытается создать иллюзию отсутствия базы данных, в частности спрятать от программиста необходимость отражения изменений в БД. Поэтому, в силу природы баз данных (управление изменяемым состоянием), у JPA нет другого выбора, кроме как использовать императивную модель программирования. Это единственный способ отдать программе "POJO", а потом отследить изменения его состояния. И в погоне за этой химерой JPA исключает более эргономичную декларативную модель программирования.

JPA наносит удар по двум фронтам - дизайн и производительность. Сначала рассмотрим, как JPA подрывает дизайн программ.

Конструктор по умолчанию

JPA требует включения в классы сущностей конструкторов по умолчанию:

The entity class must have a no-arg constructor.

— JSR 338: JavaTM Persistence API; Version 2.2; "2.1 The Entity Class", https://github.com/javaee/jpa-spec/blob/master/jsr338-MR/JavaPersistence.pdf

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

Эту проблему можно частично обойти, сделав конструктор по умолчанию package private и пометив его @Deprecated. Правда я не видел, чтобы кто-то кроме меня следовал этой практике.

Классы должны быть открытыми для наследования

JPA требует, чтобы классы сущностей были открытыми для наследования:

The entity class must not be final

— JSR 338: JavaTM Persistence API; Version 2.2; "2.1 The Entity Class"

А классы должны быть либо спроектированы и задокументированы для наследования, либо запрещать его. Тут сошлюсь на классику: Effective Java, глава "Item 19: Design and document for inheritance or else prohibit it".

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

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

Объекты должны быть изменяемыми

JPA не может работать с неизменяемым объектами "By Design", и мутабельность так же зашита в спецификацию:

An update to the state of an entity includes both the assignment of a new value to a persistent property or field of the entity as well as the modification of a mutable value of a persistent property or field

— JSR 338: JavaTM Persistence API; Version 2.2; "3.2.4 Synchronization to the Database"

Если же у вас вся модель изменяемая, то вы получаете все проблемы с:

  1. временной связанностью;

  2. нелокальностью рассуждений;

  3. конкурентным программированием.

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

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

Весь код становится кодом с побочными эффектами

При использовании JPA буквально весь код становится кодом с побочными эффектами.

Каждый геттер может привести к выполнению запроса. Или завтра начать приводить к выполнению запроса. Каждый вызов функции может мутировать ваш объект. И добавить новый UPDATE в транзакцию.

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


Теперь рассмотрим проблемы с производительностью, которые несёт использование JPA

Ленивая загрузка

JPA активно продвигает ленивую загрузку. Это вариант по умолчанию для отношений OneToMany и ManyToMany и ленивая загрузка считается "лучшей практикой" в мире JPA.

Я не удивлюсь, если ленивая загрузка ответственна за 1% мирового потребления электроэнергии. Ленивая загрузка была причиной 90% проблем с производительностью, которые мне приходилось решать в проектах с JPA.

Я много раз на порядки увеличивал производительность частей, системы использующих JPA, по следующему алгоритму:

  1. посчитать количество запросов, выполняемых кодом;

  2. пригладить волосы, вставшие дыбом от сотен запросов вместо несколько штук;

  3. выкинуть старый код, написать несколько запросов руками, написать на этой базе новый код;

  4. готово.

Тут я могу только предполагать, но думаю рецепт создания подобных проблем такой:

  1. разработчику нужно срочно реализовать новую функциональность;

  2. в месте, куда разработчик собирается добавлять новую функциональность, у него уже есть объект с геттером, возвращающим список с нужными данными;

  3. разработчик вызывает этот геттер и пробегается по нему циклом;

  4. примерно в 60% случаев, разработчик не осознаёт, что вызвав геттер он добавляет новый запрос. А пробежавшись по нему циклом - ещё N.

    Ещё в 30% осознаёт, но решает что "преждевременная оптимизация - корень всех зол".

    Ещё в 7% случаев добавляет задачу на кладбище техдолга.

    И наконец только в 3% случаях, берёт на себя ответственность, двигает сроки и решает задачу эффективно.

    По моим наблюдениям у меня в проектах с JPA процентовка примерно такая же, в лучшем случае - 60, 0, 30, 10 соотвественно.

  5. разработчик повторяет шаг 3 несколько раз, лучше сделать 2-3 вложенных цикла с ленивой загрузкой, чтобы получить экспоненциальный рост количества запросов;

  6. разработчик тестирует на демо-данных с двумя строками в таблице и не видит никаких проблем;

  7. готово.

С ленивой загрузкой надо быть постоянно начеку. Каждый раз, написав что-то в духе entity.getXXXs, задумываться - не случится ли здесь N+1 запрос. Лично мне не хватает дисциплины на это.

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

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

Дополнительный запрос для обновления сущности

Та же проблема, что и с неизменяемыми объектами , возникает, если вы хотите обновить сущность на основании DTO, полученном извне (в HTTP-запросе, например). В JPA есть два способа сделать это:

  1. Идиоматичный - выполнить дополнительный SELECT для того чтобы поместить объект в PersistenceContext, и обновить его;

  2. Эффективный - снова воспользоваться UPDATE-ом.

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

Теоретически есть ещё вариант хранить сущности в HTTP сессии, но в эпоху горизонтального масштабирования это вариант исключительно теоретический.

Дополнительный запрос для вставки ссылки

Третья проблема из той же серии - вставка новой сущности, которая ссылается на существующую с известным ИДом. И снова есть всё те же два варианта: либо делать дополнительный запрос, жертвуя производительностью, или бороться с JPA.

Кэширование

Кэшировать JPA сущности нельзя.

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

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

Наконец, если у сущности есть ленивые поля, то рано или поздно стрельнет LazyInitializationException.


Я уверен, что этот список будет и дальше расти. Сейчас я выписал только то, что лежит на поверхности.

Получается, что теоретически JPA можно использовать, не жертвуя качеством дизайна и производительностью. Однако придётся пожертвовать идиоматичностью использования JPA. А вслед за ней уходят и все остальные достоинства JPA - материалов по такому подходу уже практически нет, в поддержке проявляются острые углы, разработчики этот подход не знают и т.п.

Возникает вопрос - стоит ли игра свеч, если качество дизайна и производительность являются приоритетными качественными атрибутами системы? И чем воспользоваться, если ответ - "нет"?

Альтернативы JPA

Все вышеперечисленные проблемы не присущи объектно-реляционному маппингу как таковому. Это проблемы одного конкретного подхода к ОРМу, вызванные его стремлением сэмулировать работу с объектами в памяти. Поэтому существуют другие решения, даже похожие на JPA, в которых идиоматичное использование не вынуждает жертвовать дизайном и производительностью.

Spring Data Jdbc/R2dbc

docs.spring.io/spring-data/jdbc

Сейчас я предпочитаю работать с БД посредствам Spring Data Jdbc/R2dbc (далее - SDJ).

Эта технология обладает частью достоинств, которые считают уникальными для JPA:

  1. программисты знакомые со Spring Data JPA уже знают большую часть SDJ;

  2. это всё та же всеми любимая технология Spring Data, которая "автомагически" генерирует реализации методов вида findByName(name: String);

  3. это "надёжное решение от проверенного вендора" - его намного легче "продать" заказчику или СТО, чем другие альтернативы.

При всём при этом SDJ имеет эргономичную философию:

Spring Data JDBC aims to be much simpler conceptually, by embracing the following design decisions:

* If you load an entity, SQL statements get run. Once this is done, you have a completely loaded entity. No lazy loading or caching is done.

* If you save an entity, it gets saved. If you do not, it does not. There is no dirty tracking and no session.

* There is a simple model of how to map entities to tables. It probably only works for rather simple cases. If you do not like that, you should code your own strategy. Spring Data JDBC offers only very limited support for customizing the strategy with annotations.

— Spring Data JDBC Reference Documentation, https://docs.spring.io/spring-data/jdbc/docs/2.1.7/reference/html/#jdbc.why

И чуть ниже:

* Try to stick to immutable objects — Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only. Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state. If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types. Constructor-only materialization is up to 30% faster than properties population.

* Provide an all-args constructor — Even if you cannot or don’t want to model your entities as immutable values, there’s still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones, as this allows the object mapping to skip the property population for optimal performance.

— Spring Data JDBC Reference Documentation, https://docs.spring.io/spring-data/jdbc/docs/2.1.7/reference/html/#mapping.general-recommendations

Более того, хотя

All Spring Data modules are inspired by the concepts of “repository”, “aggregate”, and “aggregate root” from Domain Driven Design.

— Spring Data JDBC Reference Documentation, https://docs.spring.io/spring-data/jdbc/docs/2.1.7/reference/html/#jdbc.domain-driven-design

Все проекты на Spring Data JPA, с которыми я сталкивался на практике, игнорируют DDD, создают по репозиторию на таблицу и строят полносвязный двунаправленный граф всех сущностей. Кажется, с этим согласны и авторы SDJ:

These are possibly even more important for Spring Data JDBC, because they are, to some extent, contrary to normal practice when working with relational databases.

— Spring Data JDBC Reference Documentation

Эта так называемая "normal practice" - просто кошмар с точки зрения дизайна, поддержки и производительности, хотя и позволяет быстро решить задачу в моменте.

С SDJ "normal practice" не пройдёт из-за отсутствия ленивой загрузки, и команде всё-таки придётся озадачиться дизайном модели данных и разбиением её на агрегаты.

Пока что я попробовал эти технологии (JDBC и R2DBC) только в двух небольших проектах, но результатами очень доволен.

jooq

jooq.org

jooq - первая альтернативная технология, с которой у меня есть успешный коммерческий опыт.

В основе jooq-а лежит Java DSL для написания SQL запросов. Но автор так же сделал мощную инфраструктуру исполнения запросов и генерации DAO для CRUD операций.

Основных недостатка два - генерация исходного кода отдельным шагом и платная лицензия для работы с платными базами данных.

Ebean

ebean.io

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

Эта технология наиболее близка к JPA и является полноценным ОРМом. Но в отличие от JPA, Ebean не накладывает таких ограничений на дизайн и по умолчанию намного более производительная.

Однако по Ebean мало информации помимо официальной документации, а некоторые особенности в поведении всё-таки встречались. Плюс Ebean использует препроцессор аннотаций, который заметно тормозит сборку и не всегда корректно работает в Идее.

Тем не менее проект сдан, сдан в срок и седых волос прибавилось не больше, чем обычно.

MyBatis

mybatis.org

MyBatis я сам в коммерческих проектах не трогал, но насколько мне известно, это тоже популярная альтернатива JPA.

Заключение

По моему мнению, применение JPA уместно, когда важно сделать быстро, дёшево и плохо. То есть применение JPA уместно в двух случаях:

  1. быстрое прототипирование;

  2. разработка небольших внутренних информационных систем на пару десятков таблиц и столько же пользователей.

И в этих случаях, вариант с сохранением сущностей в HTTP сессии становится уже вполне практическим.

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

Теги:




Комментарии (24):

  1. welovelain
    /#22929924 / +1

    Jpa прекрасен для простых круд-сервисов, в остальных мы переписали на mybatis, полет прекрасный, времени ушло примерно х2.
    Jpa еще подкладывал свинью с автосейвом сущностей при коммите транзакции — в итоге пришел к выводу, чем меньше магии, тем лучше.


    P.S еще так как сущности теперь прокси, внутри них делать логику не рекомендуется, что убивает весь DDD и весь смысл ORM

    • furic
      /#22932392

      Улыбнуло про свинью. JPA тем и занимается что автосохраняет все изменения в контексте. Не хотите — detach.

      • welovelain
        /#22933994

        Подложил — в смысле во время переписывания, в паре мест пропустили добавить в бизнес-слое явный вызов сохранения сущностей, хотя, казалось бы, меняли слой работы с БД.
        Что выразительно ещё раз подчеркнуло, как JPA лезет куда не надо (в бизнес-слой).

  2. centralhardware
    /#22930236

    Отказ от jpa ничего не поменяет, для изменений надо менять квалификацию разработчиков

  3. cyber_ua
    /#22930344

    jooq — первая альтернативная технология, с которой у меня есть успешный коммерческий опыт.

    jooq оч классная штука и после него вообще не хочется на hibernate/ Spring Data

  4. mais
    /#22930392

    Spring Data JPA пытается решать какие-то из перечисленных проблем, например c Projection. На хабре уже были статьи по тому как избавиться от некоторых стандартных проблем, но соглашусь статья актуальна. Бездумное использование фреймворка приводит к большим проблемам, видел примеры как в одном приложении одним запросом вся база данных вытагивалась, просто потому что не было понимания что такое EagerLoading.

    • mmMike
      /#22930510

      Когда поверх Нibernate навешивается еще и особенности Spring, то бывают неожиданные чудеса, когда «магия» Spring умножается на «магию» Hibernate.

  5. nehaev
    /#22930494

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


    Возможно истинная философия JPA какая-то другая (не могу нагуглить)

    Вы передали все верно. Этот анти-паттерн называется "active record".

  6. mmMike
    /#22930498

    Я ни разу не видел JPA Entity, спроектированную для наследования.
    Хотя возможность наследования сущностей JPA создаёт потенциал для проблем, на практике я с ними не сталкивался.

    Сплошь и рядом использую.
    Просто для примера, не более:
    • toString в базовом классе (он не JPA, но как пример)
    • Куча таблиц имеющих стандартный шаблон 'ID' поле и 'WTIME' поле (например).


    А вообще, лично мне Hibernatе нравится. Но все для своих целей.
    Если в проекте активно используется слой логики в встроенных процедурах (типа PL/SQL Oracle), то уж лучше сразу отказаться от Hibernate иначе получается микс.
    Если нужно активно забирать из БД данные таблиц/вьюшек, но забирать из них меньшую часть полей, то то же лучше без Hibernate.
    Да и вообще, не знаю тонкостей работы Hibernate лучше использовать полный контроль через jdbc. Иначе можно наступить на хитрые грабли (с той же ленивой загрузкой, например)

    Как в любом инструменте есть тонкости, плюсы и минусы…
    Серебряной пули не бывает.

  7. atamur
    /#22930602

    Третья проблема из той же серии — вставка новой сущности, которая ссылается на существующую с известным ИДом.

    EntityManager.getReference или я не понял проблемы?


    Кэшировать JPA сущности нельзя.

    Second-Level Cache?


    В целом я согласен, JPA хороша для простых приложений. ORM это в целом сложная пролема

  8. tonhead
    /#22930610

    Есть ли эта или подобные статьи на английском, коллегам дать почитать?

  9. furic
    /#22930758 / +3

    Не надо писать ерунду. Через entity manager можно добраться до low level API и работать с базой данных через jdbc API. Никто вас ни в чем не ограничивает. После этого утверждения о недостатках перестал читать дальше.

    • aleksandy
      /#22930886

      Справедливости ради, стоит уточнить, что не через EntityManager, а через его конкретную имплементацию Session, если говорить о Hibernate. Вот у неё есть замечательный метод.

      • furic
        /#22931002

        Я имею ввиду спецификацию jpa. EntitManager в ней — entry point. Кстати, тот кто делал интервью автору статьи тоже далёк от понимания разницы между jpa и Hibernate, иначе он бы спросил о one to many declaration в jpa а не в Hibernate

  10. gkislin
    /#22931064

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

    • sshikov
      /#22931644

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

      select
      expr(col1), expr(col2), expr(col3)
      from ${schema}.${table}
      where ${predicate}

      при этом выражения для выборки колонк динамически генерируются, предикат тоже, таблицы перебираются все в пределах схемы, и т.п. Ну т.е. специфичный такой инструмент для определенных целей, который даже bind variables нормально использовать не может по ряду причин. Никаких мыслей применить тут JPA даже не возникало никогда. При этом в других проектах активно применяли, и даже до появления JPA Hibernate 2 пользовались — и никаких таких серьезных проблем не испытывали. Т.е. от типа проекта, от квалификации людей, и много от чего еще все и зависит. А личные вот такие вот отношения очень часто говорят, что человек не разобрался.

  11. mad_nazgul
    /#22931292

    Ну как бы -да. ORM — ЗЛО!

    Правда я не понял, насчет, того, что сущности не могут быть закрытыми для наследования.
    Т.к. в Kotlin я для сущностей использую Data Class'ы.
    Так что «если нельзя, но очень хочется — то можно»
    Аналогично с LazyInitializationException. Отказываемся от транзакционности (hibernate.enable_lazy_load_no_trans=true) и всё не особо беспокоимся о LazyInitializationException.

    Но с основным посылом, что ОРМ создает проблемы, чем их решает — согласен.

    Но Spring Data Jpa ну очень удобен. :-)

    • poxvuibr
      /#22932504

      Т.к. в Kotlin я для сущностей использую Data Class'ы.

      А зачем? equals всё равно переопределять, hashCode всё равно переопределять, конструктор без параметров всё равно нужен


      Аналогично с LazyInitializationException. Отказываемся от транзакционности

      Мне кажется, лучше словить экспешн и поправить код. Дешевле выйдет. ))

  12. poxvuibr
    /#22932610 / -1

    JPA пытается создать иллюзию отсутствия базы данных, в частности спрятать от программиста необходимость отражения изменений в БД.

    JPA не пытается. Это разработчики пытаются использовать JPA таким способом и мне кажется большинство экспертов прямо говорят не делать так.


    JPA используется для того, чтобы вытащить данные из базы, поправить и скинуть обратно. Или, чтобы просто вытащить. Ещё JPA помогает программисту строить запросы. Ключевое тут помогает. Отдавать построение запросов на откуп JPA — нежелательно.


    Конструктор по умолчанию
    Классы должны быть открытыми для наследования

    Тут я хотел бы сказать, что не нужно пытаться работать со Entity как с объектами. Это не объекты, это структуры. И если пытаться пользоваться одной вещью, так, как будто это другая вещь — ничего хорошего, конечно не выйдет. Наличие конструктора без параметров — для структуры штука закономерная.


    Объекты должны быть изменяемыми

    Повторюсь, что Entity сделаны для того, чтобы выгрузить данные, поправить и скинуть обратно. Если у вас какой-то другой воркфлоу, то не используйте Entity, JPA в этом случае всё равно будет вполне себе полезной штукой.


    Весь код становится кодом с побочными эффектами

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


    С ленивой загрузкой надо быть постоянно начеку. Каждый раз, написав что-то в духе entity.getXXXs, задумываться — не случится ли здесь N+1 запрос.

    Да, надо. Если обходиться без JPA — придётся писать запрос руками. Если с JPA — придётся руками добавить настройки, чтобы ленивой загрузки не было. Выбор индивидуален, но с JPA работы, наверное меньше. Особенно учитывая, что писать запросы руками JPA не мешает.


    Я уверен, что этот список будет и дальше расти. Сейчас я выписал только то, что лежит на поверхности.

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


    Получается, что теоретически JPA можно использовать, не жертвуя качеством дизайна и производительностью. Однако придётся пожертвовать идиоматичностью использования JPA.

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


    По моему мнению, применение JPA уместно, когда важно сделать быстро, дёшево и плохо.

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


    Основной недостаток JPA в том, что разработчики не хотят его осваивать )).

    • nehaev
      /#22932798

      Основной недостаток JPA в том, что разработчики не хотят его осваивать )).

      Допустим, передо мной выбор: изучить JDBCTemplate, Jooq или JPA. Почему я должен инвестировать свое время именно в JPA?

      • poxvuibr
        /#22933598

        Почему я должен инвестировать свое время именно в JPA?

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

  13. asm0dey
    /#22932628 / -2

    Статья автора, не осилившего SELECT NEW и RntityGrpaphs.

  14. Throwable
    /#22932982

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

    Корень проблем в табличной модели RDB, которая вцелом плохо ложится на объектную.


    Объекты должны быть изменяемыми

    Сделайте сеттеры protected и получите неизменяемую entity. Кроме того, в некоторых JPA объект можно пометить ReadOnly ну или поставить на поле updatable=false.


    Весь код становится кодом с побочными эффектами

    JPA использует паттерн Active Object. Вся работа с объектами заключена внутри текущего UnitOfWork. Вне его объекты становятся detached но с вполне детерминированным состоянием.


    Ленивая загрузка

    Это как раз то почему объектная модель плохо совместима с RDB, где все есть таблица. Проблема n+1 запроса — это не проблема JPA как таковой, а вообще всех ORM, причем концептуальная. Тем не менее никто не запрещает добавить JOIN FETCH для массивного запроса. Многие JPA позволяют сократить загрузку с дочерними сущностями до двух запросов, используя для дочернего либо IN(родительские PKs), либо IN(родительский SELECT). В большинстве же ситуаций запросы, которые делает JPA — это загрузить объект по ID, которые выполняются очень быстро.


    Дополнительный запрос для обновления сущности

    Есть еще кеш и extended persistence context. Но вцелом первый вариант полностью оправдан. При массовом апдейте объектов их лучше сначала вытащить одним запросом. А JPA потом сгенерит один batch update. Кроме того, саму entity можно при желании использовать вкачестве DTO, а для обновления использовать простой merge().


    Дополнительный запрос для вставки ссылки

    Про EntityManager.getReference(Class<T> entityClass, Object primaryKey); не слышали?


    Кэшировать JPA сущности нельзя.

    Можно, но осторожно. Есть разные стратегии поведения кеша. Кроме того, при желании можно даже вручную управлять кешем: EntityManagerFactory.getCache().


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


    Из реальных проблем JPA отметил бы отсутствие нормального type-safe DSL для запросов (Criteria API это просто ужас), достаточно убогий API и достаточно страшные аннотации для ORM. Поэтому в своих проектах часто приходится многое допиливать и делать различные надстройки и врапперы. Для упрощения разработки могу порекомендовать замечательные библиотеки QueryDSL и JINQ.

  15. furic
    /#22933584

    Статья должна называться "Почему следует избегать использования JPA/Hibernate в продакшене тем кто не понимает что такое JPA и как работает Hibernate"