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

Почему вам следует избегать дополнительных ленивых коллекций с помощью Hibernate

Узнайте, почему вам следует избегать использования дополнительных ленивых коллекций с Hibernate, поскольку это может вызвать проблемы с запросами N+1 и проблемы с производительностью.

Автор оригинала: 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 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 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 , ознакомьтесь с этой статьей|/.