Comments 4
На сколько помню там ещё от стратегии идентификатор тоже зависит если он uuid, не помню как называется и там если идентификатор не указан то будет инсерт а если указан то апдейт
И с энтити граф ещё одна проблема может возникнуть если сортировать по этим сущностям то сортировку хибер начнёт делать в памяти, т. Е. Дёргает всю бд и сортирует, слава богу об этом ворнинг летит... Лучше всего лейзи листы всегда вытягивать подзапросом, а в некоторых командах даже принимают правило защиты от дурака: запрещают указывать onetomany
Ох, парни, что ж вы такое переводите!
Суммируя сказанное
Это была рубрика "вредные советы", ничего из перечисленного в статье делать не надо. В хибернейте разумные дефолты (OSIV не в счёт, это Spring Data JPA, а не сам хибер), что-то tweak-ать надо с полным пониманием.
Кто такой вообще этот Мацей Валковяк и почему его постят?
Поехали разбираться.
Поэтому Spring Data рассуждает следующим образом: “О, это сущность, которая, как утверждает callee, уже существует! Однако мне надо удостовериться, существует ли она на самом деле. Поэтому я сделаю SELECT в базу данных, чтобы убедиться, есть ли там эти данные.”
Чушь! Это не так.
Во-первых, Spring Data навязал репозиторный метод save()
в том числе и для Spring Data JPA (для единообразия), оказав тем самым медвежью услугу. В абсолютном большинстве случаев нужны em.persist()
для новых entities и "ничего" для изменения managed ones.
Хорошо бы понимать, для чего em.merge()
(спойлер -- в нормальных кейсах он не нужен) и какой у него сайд-эффект при вручную установленных ID, вот эта самая лишняя загрузка.
Именно так и появился тот длинный SELECT, который поставил нас в тупик и существенно замедлил работу нашего приложения.
Скорость работы селекта определяется не его длиной)
Лишний селект, это, конечно, никогда не хорошо, но в данном конкретном случае -- join по primary key -- существенно замедлить работу не должен.
Альтернатива состоит в том, чтобы реализовать интерфейс Persistable.
Вот такого точно не надо, пожалуйста.
И @Version
тоже добавляйте для Optimistic Lock, а не для каких-то других целей.
Смущает лишний select при "сохранении" энтити с предустановленными ID -- просто используйте em.persist()
!
добавить аннотацию @Transactional
Ну, хорошо что про это всё таки вспомнили, хорошо бы с этого начать, а заодно понимать, как оно до этого работало.
При использовании getReferenceById(), если такой счет не существует, операция потерпит неудачу только после вызова save(). Однако, всеми этими проблемами можно управлять.
И как управлять-то?
Прежде чем "ускорять" приложение с помощь getReferenceById()
хорошо бы понимать трейд-офф между ним и обычным findById(id).orElseThrow(...)
.
Обычный findById
делает запрос в БД и кидает указанный экспешн, который -- обычная практика -- переоборачивается в 400 или 404.
getReferenceById()
ничего этого не делает, id
используется как есть в надежде, что есть foreign key. Парсить ForeignKeyConstraintViolationException
тоже можно (а возможно и нужно в сильно конкуретной среде), но строить на нём BadRequest/ResourseNotFoundException
несколько сложнее.
Этот код загружает данные, мапит их на DTO, и затем в контроллере это сериализуется в JSON. Не очень хороший код.
Почему это не очень хороший код?! Вот как раз правильно сделано!
> В дальнейшем для каждого элемента в списке найденых переводов будут выполняться еще по два запроса, так что у нас в конце концов появляется ситуация, когда чем больше результат выполнения первоначального запроса, тем больше запросов мы выполняем по сумме. Это называется проблема N+1.
Откуда тут N+1?! Видимо, автор запутался в своих тестах и получил N+1, как раз когда поставил `@ManyToOne(fetch = LAZY)`
Когда у нас есть отношения @ManyToOne или @ManyToMany, необходимо всегда менять FetchType на LAZY
Не нужно.
Во-первых, @ManyToMany
и так LAZY
по дефолту.
А что касается исправления @ManyToOne
на LAZY
то, опять же, надо понимать трейд-офф.
C EAGER
мы `left join`-ом загружаем данные всегда (как, собственно, автор и указал вначале)
from bank_transfer btl_0
left join account r1_0 on r1_0.id = btl_0.receiver_id
left join account s1_0 on s1_0.id = btl_0.sender_id
Они могут нам понадобиться для бизнес-логики/маппинга в ДТО, а могут и не понадобиться. Если у нас инкапсулированная модель, то в общем случае мы этого заранее не знаем, и знать не можем.
JOIN по первичному ключу, естественно, не бесплатен, но, будем считать, относительно дешёв.
В противном случае, с `@ManyToOne(fetch = LAZY)` на любом запросе коллекции энтити как раз-то и есть риск получать N+1 проблему. Обычно это существенно медленнее, и поэтому дефолтное значение как раз EAGER
.
Короче говоря, дефолтные `@ManyToOne(fetch = EAGER)` и `@OneToMany(fetch = LAZY)` -- это нормально и менять их не надо.
Кроме того, SQL запросы лучше писать самим и указывать все нужные нам данные в явном виде, чтобы не загружать лишнего.
Обычно лучше не писать. Но вообще это философский спор между инкапсуляцией и быстродействием.
когда мы выполняем UPDATE, PostgreSQL в любом случае вставляет полностью новую колонку.
СТРОКУ, а не колонку!
Select не будет сделан, если у энтити есть идентификатор, генерируемый на стороне приложения, равный null
А, вижу, добавили. Может, поднимите наверх, где "проблема" обсуждается и спойлер уберите, чтобы сразу в глаза бросалось?
@DynamicUpdate
Мы прогоняем тест и смотрим на UPDATE, который выполняется в базе данных. Почти каждый ORM, включая Hibernate, просто установят значения для каждой колонки, которая есть в этой сущности, даже если значение для этой колонки не поменялось.
Самое время задуматься, чем руководствовались разработчики Хибернейта, и почему сделано именно так. Разобраться с `JDBC PreparedStatement` и кешем запросов.
По мелочи.
Бизнес-конструкторы -- это хорошо и правильно, пустой конструктор нужен хибернейту, но его лучше сделать protected
, чтобы запретить вызывать из кода.
Теперь правильные советы.
Основные причины "тормозящего" хибернейта -- это, конечно, не сам хибернейт, а разработчики вокруг него.
Основные проблемы:
Отсутствие индексов!
Если для
@ManyToOne
primary key забыть невозможно, то вот дляclass Parent { @OneToMany(mappedBy = "parent") List<Child> children; }
...
parent_id
в таблицеchildren
-- вполне. Проверьте!Явно установленный EAGER для `@OneToMany(fetch = EAGER)`. То ли для борьбы с
LazyInitializationException
, то ли для "загрузки" данных, то ли ещё для чего-то. Так делать не надо, источник N+1 проблемы. Правильно: маппить энтити в responseDto.Переопределённый `@ManyToOne(fetch = LAZY)`. То ли после прочтения этой статьи, то ли аналогичных. Тоже потенциальный источник N+1 проблемы. Можно менять только полном понимании всех трейдоффов.
Отдельный комментарий для "зачем нужен это ваш JPA/Hibernate, одни только проблемы, лучше возьмите JOOQ/Spring JDBC/учите чистый SQL".
Hibernate нужен вот для этого, соберусь с силами -- разверну в статью.
Spring Data JPA и Hibernate: ориентируемся на производительность. Часть 2