Pull to refresh

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, чтобы запретить вызывать из кода.

Теперь правильные советы.

Основные причины "тормозящего" хибернейта -- это, конечно, не сам хибернейт, а разработчики вокруг него.

Основные проблемы:

  1. Отсутствие индексов!

    Если для @ManyToOne primary key забыть невозможно, то вот для

    class Parent {
      @OneToMany(mappedBy = "parent")
      List<Child> children;
    }

    ... parent_id в таблице children -- вполне. Проверьте!

  2. Явно установленный EAGER для `@OneToMany(fetch = EAGER)`. То ли для борьбы с LazyInitializationException, то ли для "загрузки" данных, то ли ещё для чего-то. Так делать не надо, источник N+1 проблемы. Правильно: маппить энтити в responseDto.

  3. Переопределённый `@ManyToOne(fetch = LAZY)`. То ли после прочтения этой статьи, то ли аналогичных. Тоже потенциальный источник N+1 проблемы. Можно менять только полном понимании всех трейдоффов.

Отдельный комментарий для "зачем нужен это ваш JPA/Hibernate, одни только проблемы, лучше возьмите JOOQ/Spring JDBC/учите чистый SQL".

Hibernate нужен вот для этого, соберусь с силами -- разверну в статью.

Sign up to leave a comment.