Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье вы узнаете, почему использование ДОПОЛНИТЕЛЬНЫХ ленивых коллекций в режиме гибернации является плохой идеей, поскольку это может привести к проблемам с запросами N+1 и вызвать проблемы с производительностью.
Причина, по которой я хотел написать эту статью, заключается в том, что я постоянно вижу, как она упоминается в StackOverflow или на форуме Hibernate.
Почему вам следует избегать ДОПОЛНИТЕЛЬНЫХ ленивых коллекций с помощью Hibernate . @vlad_mihalcea https://t.co/gAQL8pYrCg pic.twitter.com/AsJyieBWgG
Модель предметной области
Предположим, что ваше приложение использует родительскую Публикацию
сущность, которая может иметь несколько Комментариев к публикации
дочерних сущностей.
Объект Post
отображается следующим образом:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) @LazyCollection( LazyCollectionOption.EXTRA ) @OrderColumn(name = "order_id") private Listcomments = new ArrayList<>(); public Long getId() { return id; } public Post setId(Long id) { this.id = id; return this; } public String getTitle() { return title; } public Post setTitle(String title) { this.title = title; return this; } public List getComments() { return comments; } public Post addComment( PostComment comment) { comments.add(comment); comment.setPost(this); return this; } public Post removeComment( PostComment comment) { comments.remove(comment); comment.setPost(null); return this; } }
Первое, что вы можете заметить, это то, что сеттеры используют свободный стиль API .
Второе, на что следует обратить внимание, – это то, что в двунаправленной коллекции комментариев
используется @LazyCollection
аннотация с ДОПОЛНИТЕЛЬНЫМ
| параметром LazyCollectionOption . Опция
@LazyCollectionOption.ДОПОЛНИТЕЛЬНАЯ опция учитывается только для индексированных
Списков коллекций, поэтому нам необходимо использовать аннотацию
@OrderColumn .
Третье, что следует отметить, это то, что мы определили методы addcommand
и removecommand
, потому что мы хотим убедиться, что обе стороны двунаправленной связи синхронизированы. Для получения более подробной информации о том, почему вы всегда должны синхронизировать обе стороны двунаправленных отношений JPA, ознакомьтесь с этой статьей .
Объект Комментарий к сообщению
отображается следующим образом:
@Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; public Long getId() { return id; } public PostComment setId(Long id) { this.id = id; return this; } public Post getPost() { return post; } public PostComment setPost(Post post) { this.post = post; return this; } public String getReview() { return review; } public PostComment setReview(String review) { this.review = review; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; return id != null && id.equals(((PostComment) o).getId()); } @Override public int hashCode() { return getClass().hashCode(); } }
Как и в Post
сущности, Комментарий к публикации
использует API в стиле fluent, который упрощает процесс создания экземпляра сущности.
В ассоциации @ManyToOne
используется Тип выборки.Стратегия ленивой
выборки, потому что по умолчанию Тип выборки.НЕТЕРПЕЛИВЫЙ
– это очень плохая идея с точки зрения производительности .
Обратите внимание, что хэш-код
использует постоянное значение, а реализация равна
учитывает идентификатор сущности только в том случае, если он не null
. Причина, по которой методы hashCode
и equals
реализованы подобным образом, заключается в том, что в противном случае равенство не было бы согласованным во всех переходах состояния сущности . Для получения более подробной информации об использовании идентификатора сущности для равенства ознакомьтесь с этой статьей .
Теперь, когда сохраняется одна Запись
сущность с тремя связанными Комментарием к записи
дочерними сущностями:
entityManager.persist( new Post() .setId(1L) .setTitle( "High-Performance Java Persistence" ) .addComment( new PostComment() .setId(1L) .setReview( "Excellent book to understand Java persistence ") ) .addComment( new PostComment() .setId(2L) .setReview( "The best JPA ORM book out there" ) ) .addComment( new PostComment() .setId(3L) .setReview( "Must-read for Java developers" ) ) );
Hibernate выполняет следующие инструкции SQL INSERT и UPDATE:
INSERT INTO post ( title, id ) VALUES ( 'High-Performance Java Persistence', 1 ) INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Excellent book to understand Java persistence', 1 ) INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'The best JPA ORM book out there', 2 ) INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Must-read for Java developers', 3 ) UPDATE post_comment SET order_id = 0 WHERE id = 1 UPDATE post_comment SET order_id = 1 WHERE id = 2 UPDATE post_comment SET order_id = 2 WHERE id = 3
Инструкции UPDATE выполняются для того, чтобы задать Список
индекс записи. Причина, по которой ОБНОВЛЕНИЕ выполняется отдельно, заключается в том, что сначала выполняется действие ВСТАВКА
, а действия на основе сбора выполняются на более поздней стадии очистки. Для получения более подробной информации о порядке операции промывки ознакомьтесь с этой статьей .
Повторение ДОПОЛНИТЕЛЬНОЙ коллекции @LazyCollection с использованием цикла для каждого
Предполагая, что у нас есть объект Post
, связанный с текущим текущим контекстом сохранения, если мы хотим получить доступ к его дочерним сущностям
, используя цикл для каждого, как показано в следующем фрагменте кода:
for (PostComment comment: post.getComments()) { LOGGER.info("{} book review: {}", post.getTitle(), comment.getReview() ); }
Hibernate собирается выполнить одну инструкцию SELECT:
SELECT pc.post_id as post_id3_1_0_, pc.id as id1_1_0_, pc.order_id as order_id4_0_, pc.review as review2_1_1_ FROM post_comment pc WHERE pc.post_id = 1 -- High-Performance Java Persistence book review: Excellent book to understand Java persistence -- High-Performance Java Persistence book review: The best JPA ORM book out there -- High-Performance Java Persistence book review: Must-read for Java developers
Повторение ДОПОЛНИТЕЛЬНОЙ коллекции @LazyCollection с использованием цикла for
Однако, если мы повторим Публикацию комментария
коллекции, используя цикл for:
int commentCount = post.getComments().size(); for(int i = 0; i < commentCount; i++ ) { PostComment comment = post.getComments().get(i); LOGGER.info("{} book review: {}", post.getTitle(), comment.getReview() ); }
Hibernate создаст 4 запроса ВЫБОРА:
SELECT MAX(order_id) + 1 FROM post_comment WHERE post_id = 1 SELECT pc.id as id1_1_0_, pc.post_id as post_id3_1_0_, pc.review as review2_1_0_ FROM post_comment pc WHERE pc.post_id = 1 AND pc.order_id = 0 -- High-Performance Java Persistence book review: Excellent book to understand Java persistence SELECT pc.id as id1_1_0_, pc.post_id as post_id3_1_0_, pc.review as review2_1_0_ FROM post_comment pc WHERE pc.post_id = 1 AND pc.order_id = 1 -- High-Performance Java Persistence book review: The best JPA ORM book out there SELECT pc.id as id1_1_0_, pc.post_id as post_id3_1_0_, pc.review as review2_1_0_ FROM post_comment pc WHERE pc.post_id = 1 AND pc.order_id = 2 -- High-Performance Java Persistence book review: Must-read for Java developers
Первый запрос SELECT предназначен для размера коллекции, в то время как остальные запросы SELECT будут извлекать каждую отдельную запись Список
.
Вывод
Доступ к Списку
, использующему как @OrderColumn
, так и ДОПОЛНИТЕЛЬНЫЙ
| @LazyCollection по позиции ввода, может привести к N+1 проблемам с запросами, что, в свою очередь, может вызвать проблемы с производительностью.
Поэтому лучше вообще избегать упорядоченных Списков
коллекций, поскольку порядок ввода задается с помощью операторов вторичного ОБНОВЛЕНИЯ. И, используя тип выборки по умолчанию .Стратегия извлечения ленивой
коллекции достаточна, так как вам не нужна функция EXTRA
lazy.
Если ваша коллекция слишком велика, и вы считаете, что используете ДОПОЛНИТЕЛЬНУЮ
ленивую выборку, то вам лучше заменить коллекцию запросом JPQL, который может использовать разбиение на страницы. Для получения более подробной информации о том, как лучше всего использовать ассоциацию @OneToMany
, ознакомьтесь с этой статьей|/.