Автор оригинала: Vlad Mihalcea.
В этой статье мы рассмотрим лучший способ инициализации ЛЕНИВЫХ прокси-серверов и коллекций при использовании JPA и Hibernate.
Я решил написать эту статью, потому что в Интернете слишком много ресурсов, которые вводят читателя в заблуждение, заставляя использовать неудобные и неэффективные методы.
Лучший способ инициализации ЛЕНИВЫХ прокси и коллекций при использовании JPA и #Hibernate . @vlad_mihalcea https://t.co/kWpi3etBAZ pic.twitter.com/sVqeMgFSLu
Давайте предположим, что у нас есть родительская Запись
сущность, которая имеет двунаправленную @OneToMany
связь с Комментарием к сообщению
дочерней сущностью.
Объект Post
отображается следующим образом:
@Entity(name = "Post") @Table(name = "post") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Post { @Id private Long id; private String title; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) 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 void addComment(PostComment comment) { comments.add(comment); comment.setPost(this); } public void removeComment(PostComment comment) { comments.remove(comment); comment.setPost(null); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Post)) return false; return id != null && id.equals(((Post) o).getId()); } @Override public int hashCode() { return getClass().hashCode(); } }
Есть несколько аспектов сопоставления Post
сущностей, которые стоит объяснить:
- Сущность
Post
использует стратегию параллелизма кэша второго уровня READ_WRITE , которая работает в режиме сквозной записи . - Сеттеры следуют за API в стиле Fluent , который поддерживается Hibernate.
- Поскольку ассоциация
@OneToMany
является двунаправленной, мы предоставляем служебные методы добавления/удаления, чтобы обеспечить синхронизацию обеих сторон ассоциации . Неспособность синхронизировать оба конца двунаправленной связи может привести к очень трудным для отслеживания проблемам. - Метод
hashCode
возвращает постоянное значение, поскольку идентификатор сущности используется для проверки на равенство. Это метод, который я ввел 2 года назад так как ранее считалось, что вы не можете использовать идентификатор сущности при сравнении логической эквивалентности сущности JPQ.
Объект Комментарий к сообщению
отображается следующим образом:
@Entity(name = "PostComment") @Table(name = "post_comment") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class PostComment { @Id private Long id; private String review; @ManyToOne(fetch = FetchType.LAZY) private Post post; public Long getId() { return id; } public PostComment setId(Long id) { this.id = id; return this; } public String getReview() { return review; } public PostComment setReview(String review) { this.review = review; return this; } public Post getPost() { return post; } public PostComment setPost(Post post) { this.post = post; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PostComment)) return false; return id != null && id.equals(((PostComment) o).id); } @Override public int hashCode() { return getClass().hashCode(); } @Override public String toString() { return "PostComment{" + "id=" + id + ", review='" + review + ''' + '}'; } }
Обратите внимание, что стратегия выборки ассоциации @ManyToOne
имеет значение FetchType.ЛЕНИВЫЙ
потому что по умолчанию @ManyToOne
и @OneToOne
ассоциации извлекаются с нетерпением, и это может привести к N+1 проблемам с запросами среди других проблем с производительностью. Для получения более подробной информации ознакомьтесь с этой статьей .
Объект или коллекция с отложенной загрузкой заменяется прокси-сервером перед извлечением объекта или коллекции. Прокси-сервер может быть инициализирован путем доступа к любому свойству сущности или элементу коллекции или с помощью метода Hibernate.initialize
/.
Теперь давайте рассмотрим следующий пример:
LOGGER.info("Clear the second-level cache"); entityManager.getEntityManagerFactory().getCache().evictAll(); LOGGER.info("Loading a PostComment"); PostComment comment = entityManager.find( PostComment.class, 1L ); assertEquals( "A must read!", comment.getReview() ); Post post = comment.getPost(); LOGGER.info("Post entity class: {}", post.getClass().getName()); Hibernate.initialize(post); assertEquals( "High-Performance Java Persistence", post.getTitle() );
Во-первых, мы собираемся очистить кэш второго уровня, поскольку, если вы явно не включите кэш второго уровня и не настроите поставщика, Hibernate не будет использовать кэш второго уровня.
При запуске этого тестового набора Hibernate выполняет следующие инструкции SQL:
-- Clear the second-level cache -- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post -- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment -- Loading a PostComment 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.id=1 -- Post entity class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post$HibernateProxy$5LVxadxF SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=1
Мы видим, что кэш второго уровня был правильно удален и что после извлечения сущности PostComment
сущность post
представлена Экземпляр HibernateProxy
, который содержит только идентификатор сущности Post
, полученный из столбца post_id
строки таблицы базы данных post_comment//.
Теперь из-за вызова метода Hibernate.initialize
выполняется вторичный SQL-запрос для извлечения сущности Post
, что не очень эффективно и может привести к N+1 проблемам с запросами .
Поэтому, если вы не используете кэш второго уровня, не рекомендуется извлекать ленивые ассоциации с помощью вторичных SQL-запросов либо путем их обхода, либо с помощью метода Hibernate.initialize
.
В предыдущем случае комментарий Post
должен быть извлечен вместе с его ассоциацией post
с помощью директивы JOIN FETCH
JPQL.
LOGGER.info("Clear the second-level cache"); entityManager.getEntityManagerFactory().getCache().evictAll(); LOGGER.info("Loading a PostComment"); PostComment comment = entityManager.createQuery( "select pc " + "from PostComment pc " + "join fetch pc.post " + "where pc.id = :id", PostComment.class) .setParameter("id", 1L) .getSingleResult(); assertEquals( "A must read!", comment.getReview() ); Post post = comment.getPost(); LOGGER.info("Post entity class: {}", post.getClass().getName()); assertEquals( "High-Performance Java Persistence", post.getTitle() );
На этот раз Hibernate выполняет одну инструкцию SQL, и мы больше не рискуем столкнуться с проблемами с запросами N+1:
-- Clear the second-level cache -- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post -- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment -- Loading a PostComment SELECT pc.id AS id1_1_0_, p.id AS id1_0_1_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_, p.title AS title2_0_1_ FROM post_comment pc INNER JOIN post p ON pc.post_id=p.id WHERE pc.id=1 -- Post entity class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post
Обратите внимание, что класс Post
entity не является Прокси-сервер гибернации
больше не работает, потому что ассоциация post
извлекается во время запроса и инициализируется как POJO.
Итак, чтобы увидеть, когда Hibernate.initialize
действительно стоит использовать, вам нужно использовать кэш второго уровня:
LOGGER.info("Loading a PostComment"); PostComment comment = entityManager.find( PostComment.class, 1L ); assertEquals( "A must read!", comment.getReview() ); Post post = comment.getPost(); LOGGER.info("Post entity class: {}", post.getClass().getName()); Hibernate.initialize(post); assertEquals( "High-Performance Java Persistence", post.getTitle() );
На этот раз мы больше не удаляем области кэша второго уровня, и, поскольку мы используем стратегию параллелизма READ_WRITE
cache, сущности кэшируются сразу после их сохранения, поэтому при выполнении приведенного выше тестового случая не требуется выполнение SQL-запроса:
-- Loading a PostComment -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#1` -- Proxy class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post$HibernateProxy$rnxGtvMK -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1`
Как Комментарий к сообщению
, так и сообщение
ассоциация извлекаются из кэша второго уровня, как показано в сообщениях Попадание в кэш
журнал.
Поэтому, если вы используете кэш второго уровня, можно использовать Hibernate.initialize
для извлечения дополнительных ассоциаций, необходимых для выполнения вашего бизнес-варианта использования. В этом случае, даже если у вас есть N+1 вызовов кэша, каждый вызов должен выполняться очень быстро, так как кэш второго уровня настроен правильно и данные возвращаются из памяти.
Hibernate.initialize
также может использоваться для коллекций. Теперь, поскольку коллекции кэша второго уровня доступны для чтения, это означает, что они сохраняются в кэше при первой загрузке при выполнении следующего тестового случая:
LOGGER.info("Loading a Post"); Post post = entityManager.find( Post.class, 1L ); Listcomments = post.getComments(); LOGGER.info("Collection class: {}", comments.getClass().getName()); Hibernate.initialize(comments); LOGGER.info("Post comments: {}", comments);
Hibernate выполняет SQL-запрос для загрузки Комментария к сообщению
коллекции:
-- Loading a Post -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1` -- Collection class: org.hibernate.collection.internal.PersistentBag - Cache hit, but item is unreadable/invalid : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments#1` SELECT pc.post_id AS post_id3_1_0_, pc.id AS id1_1_0_, pc.id AS id1_1_1_, pc.post_id AS post_id3_1_1_, pc.review AS review2_1_1_ FROM post_comment pc WHERE pc.post_id=1 -- Post comments: [ PostComment{id=1, review='A must read!'}, PostComment{id=2, review='Awesome!'}, PostComment{id=3, review='5 stars'} ]
Однако, если Комментарий к сообщению
коллекция уже кэширована:
doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); assertEquals(3, post.getComments().size()); });
При запуске предыдущего тестового случая Hibernate может извлекать все данные только из кэша:
-- Loading a Post -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1` -- Collection class: org.hibernate.collection.internal.PersistentBag -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments#1` -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#1` -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#2` -- Cache hit : region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`, key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#3`
Метод Hibernate.initialize
полезен при загрузке прокси-объекта или коллекции, хранящейся в кэше второго уровня. Если базовая сущность или коллекция не кэшируются, то использование загрузки Прокси-сервера с помощью вторичного SQL-запроса менее эффективно, чем загрузка отложенной ассоциации с самого начала с использованием директивы JOIN FETCH
.