Рубрики
Без рубрики

Лучший способ инициализации ЛЕНИВЫХ прокси-серверов сущностей и коллекций с помощью JPA и гибернации

Автор оригинала: 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 List comments = 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
);

List comments = 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 .