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

Как избежать проблемы с кэшированием запросов в режиме гибернации N+1

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

Автор оригинала: Vlad Mihalcea.

Недавно я ответил на этот вопрос на форуме Hibernate, и, поскольку он очень хороший, я решил превратить его в статью.

В этом посте мы опишем, как возникает проблема с запросом N+1 при использовании кэша запросов гибернации второго уровня.

Как возникает проблема с запросом N+1 при использовании второго уровня #Hibernate Кэш запросов – @vlad_mihalcea https://t.co/ysel1ZBYU3 pic.twitter.com/Dg8gzlO6ST

Предполагая, что у нас есть следующие классы моделей предметной области:

Которые отображаются следующим образом:

@Entity(name = "Post")
@Table(name = "post")
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE
)
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE
)
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

Таким образом, как Сообщение , так и Комментарий к сообщению сущности можно кэшировать и использовать READ_WRITE CacheConcurrencyStrategy .

Чтобы включить кэш запросов, нам также необходимо включить кэш второго уровня. Поэтому необходимо указать следующие свойства конфигурации:






Хотя мы включили кэш запросов, он автоматически не применяется ни к одному запросу, и нам нужно явно указать Hibernate, какие запросы должны быть кэшированы. Для этого вам необходимо использовать org.hibernate.cacheable подсказка запроса, как показано в следующем примере:

public List getLatestPostComments(
        EntityManager entityManager) {
    return entityManager.createQuery(
        "select pc " +
        "from PostComment pc " +
        "order by pc.post.id desc", PostComment.class)
    .setMaxResults(10)
    .setHint(QueryHints.HINT_CACHEABLE, true)
    .getResultList();
}

Теперь, если мы дважды вызовем получить последние комментарии к сообщению , мы увидим, что результат извлекается из кэша при втором выполнении этого метода.

Следовательно, при выполнении этого тестового случая:

printCacheRegionStatistics(
    StandardQueryCache.class.getName()
);
assertEquals(
    3, 
    getLatestPostComments(entityManager).size()
);

printCacheRegionStatistics(
    StandardQueryCache.class.getName()
);
assertEquals(
    3, 
    getLatestPostComments(entityManager).size()
);

Hibernate генерирует следующие выходные данные:

Region: org.hibernate.cache.internal.StandardQueryCache,
Statistics: SecondLevelCacheStatistics[
    hitCount=0,
    missCount=0,
    putCount=0,
    elementCountInMemory=0,
    elementCountOnDisk=0,
    sizeInMemory=0
],
Entries: {}

-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
-- Query results were not found in cache

SELECT pc.id AS id1_1_,
       pc.post_id AS post_id3_1_,
       pc.review AS review2_1_
FROM post_comment pc
ORDER BY pc.post_id DESC
LIMIT 10

-- Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=6244549098291200

Region: org.hibernate.cache.internal.StandardQueryCache,
Statistics: SecondLevelCacheStatistics[
    hitCount=0,
    missCount=1,
    putCount=1,
    elementCountInMemory=1,
    elementCountOnDisk=0,
    sizeInMemory=776
],
Entries: {
sql: select pc.id as id1_1_, pc.post_id as post_id3_1_, pc.review as review2_1_ from post_comment pc order by pc.post_id desc; parameters: ; 
named parameters: {}; 
max rows: 10; 
transformer: org.hibernate.transform.CacheableResultTransformer@110f2=[
    6244549098291200, 
    4, 
    3, 
    2
]}

-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
-- Checking query spaces are up-to-date: [post_comment]
-- [post_comment] last update timestamp: 6244549098266628, result set timestamp: 6244549098291200
-- Returning cached query results

Как вы можете видеть в журнале, только первый вызов выполнил SQL-запрос, так как второй использовал кэшированный набор результатов.

Теперь давайте посмотрим, что произойдет, если мы удалим все объекты PostComment до запуска второго вызова метода getlatestpostcomm .

doInJPA(entityManager -> {
    entityManager
    .getEntityManagerFactory()
    .getCache()
    .evict(PostComment.class);
});

doInJPA(entityManager -> {
    assertEquals(
        3, 
        getLatestPostComments(entityManager).size()
    );
});

При запуске приведенного выше тестового случая Hibernate генерирует следующие выходные данные:

-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
-- Checking query spaces are up-to-date: [post_comment]
-- [post_comment] last update timestamp: 6244574473195524, result set timestamp: 6244574473207808
-- Returning cached query results

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 = 4

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 = 3

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 = 2

Как вы можете видеть в журналах, даже если идентификаторы сущностей были извлечены из кэша запросов, поскольку сущности не найдены в кэше второго уровня, сущности PostComment извлекаются с помощью SQL-запросов.

Если результат кэша запросов содержит N идентификаторов сущностей, будет выполнено N вторичных запросов, что может быть на самом деле хуже, чем выполнение запроса, который мы ранее кэшировали.

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

Как я уже объяснял ранее, вы можете обнаружить все проблемы с запросами N+1 с помощью моего механизма утверждения модульного теста db-util и устранить эту проблему задолго до развертывания в рабочей среде.

Чтобы избежать этой проблемы, вы должны убедиться, что кэшированная сущность хранится в кэше второго уровня.

. Убедитесь, что объект Комментарий к сообщению доступен для кэширования, что означает, что вы снабдили его аннотацией, специфичной для режима гибернации @Cache . Хотя JPA определяет аннотацию @Cacheable , этого недостаточно, так как Hibernate должен знать, что Cacheconcurrencystrategy вы хотите использовать для рассматриваемой сущности. . Кроме того, убедитесь, что параметр Ehcache timeToIdleSeconds или эквивалентный TTL(Время жизни) других поставщиков кэша второго уровня больше для сущностей, чем для кэша запросов. Это гарантирует, что сущности останутся в кэше дольше, чем результирующий набор кэша запросов, в котором хранятся только идентификаторы сущностей.

Хотя кэш запросов Hibernate может помочь вам разгрузить сервер базы данных, вы должны быть очень осторожны при кэшировании сущностей, поскольку , в отличие от прогнозов DTO, результирующий набор не обслуживается полностью из кэша запросов, поскольку он также зависит от областей сущностей кэша второго уровня.

Вот почему мониторинг очень важен, когда вы включаете кэш второго уровня Hibernate и связанный с ним кэш запросов.