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

Как работает стратегия Hibernate только для чтения CacheConcurrencyStrategy

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

Как я ранее объяснял , корпоративное кэширование требует тщательности. Поскольку данные дублируются между базой данных ( системой записи ) и уровнем кэширования, нам нужно убедиться, что два отдельных источника данных не расходятся.

Если кэшированные данные неизменяемы (ни база данных, ни кэш не могут их изменить), мы можем безопасно кэшировать их, не беспокоясь о каких-либо проблемах с согласованностью. Данные, доступные только для чтения, всегда являются хорошим кандидатом для кэширования на уровне приложений, повышая производительность чтения без необходимости ослаблять гарантии согласованности.

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

Репозиторий является корневой сущностью, являющейся родительской для любого Фиксация сущность. Каждая Фиксация содержит список Изменений компонентов (встраиваемых типов значений).

Все сущности кэшируются как элементы, доступные только для чтения:

@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_ONLY
)

Сохраняющиеся сущности

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

doInTransaction(session -> {
    Repository repository = 
        new Repository("Hibernate-Master-Class");
    session.persist(repository);
});

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

@Test
public void testRepositoryEntityLoad() {
    LOGGER.info("Read-only entities are read-through");

    doInTransaction(session -> {
        Repository repository = (Repository) 
            session.get(Repository.class, 1L);
        assertNotNull(repository);
    });

    doInTransaction(session -> {
        LOGGER.info("Load Repository from cache");
        session.get(Repository.class, 1L);
    });
}

Этот тест генерирует выходные данные:

--Read-only entities are read-through

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1 

--JdbcTransaction - committed JDBC Connection

--Load Repository from cache

--JdbcTransaction - committed JDBC Connection

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

В режиме гибернации 5 объекты ТОЛЬКО для ЧТЕНИЯ доступны для записи при использовании генератора ПОСЛЕДОВАТЕЛЬНОСТЕЙ или ТАБЛИЦ, в то время как они доступны для чтения для генератора идентификаторов.

Обновление сущностей

Записи кэша, доступные только для чтения, не подлежат обновлению. Любая такая попытка заканчивается тем, что создается исключение:

@Test
public void testReadOnlyEntityUpdate() {
    try {
        LOGGER.info("Read-only cache entries cannot be updated");
        doInTransaction(session -> {
            Repository repository = (Repository) 
                session.get(Repository.class, 1L);
            repository.setName(
                "High-Performance Hibernate"
            );
        });
    } catch (Exception e) {
        LOGGER.error("Expected", e);
    }
}

Выполнение этого теста приводит к следующим выводам:

--Read-only cache entries cannot be updated

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1 

UPDATE repository
SET    NAME = 'High-Performance Hibernate'
WHERE  id = 1 

--JdbcTransaction - rolled JDBC Connection

--ERROR Expected
--java.lang.UnsupportedOperationException: Can't write to a read-only object

Поскольку объекты кэша, доступные только для чтения, практически неизменяемы, рекомендуется приписывать им специфику гибернации @Неизменяемая аннотация.

Удаление сущностей

Записи кэша, доступные только для чтения, удаляются также при удалении связанного объекта:

@Test
public void testReadOnlyEntityDelete() {
    LOGGER.info("Read-only cache entries can be deleted");
    doInTransaction(session -> {
        Repository repository = (Repository) 
            session.get(Repository.class, 1L);
        assertNotNull(repository);
        session.delete(repository);
    });
    doInTransaction(session -> {
        Repository repository = (Repository) 
            session.get(Repository.class, 1L);
        assertNull(repository);
    });
}

Создание следующего вывода:

--Read-only cache entries can be deleted

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;

DELETE FROM repository
WHERE  id = 1

--JdbcTransaction - committed JDBC Connection

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1; 

--JdbcTransaction - committed JDBC Connection

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

Кэширование коллекции

Сущность Commit содержит набор компонентов Change .

@ElementCollection
@CollectionTable(
    name="commit_change",
    joinColumns=@JoinColumn(name="commit_id")
)
private List changes = new ArrayList<>();

Хотя сущность Commit кэшируется как элемент, доступный только для чтения, коллекция Change игнорируется кэшем второго уровня.

@Test
public void testCollectionCache() {
    LOGGER.info("Collections require separate caching");
    doInTransaction(session -> {
        Repository repository = (Repository) 
            session.get(Repository.class, 1L);
        Commit commit = new Commit(repository);
        commit.getChanges().add(
            new Change("README.txt", "0a1,5...")
        );
        commit.getChanges().add(
            new Change("web.xml", "17c17...")
        );
        session.persist(commit);
    });
    doInTransaction(session -> {
        LOGGER.info("Load Commit from database");
        Commit commit = (Commit) 
            session.get(Commit.class, 1L);
        assertEquals(2, commit.getChanges().size());
    });
    doInTransaction(session -> {
        LOGGER.info("Load Commit from cache");
        Commit commit = (Commit) 
            session.get(Commit.class, 1L);
        assertEquals(2, commit.getChanges().size());
    });
}

Выполнение этого теста приводит к следующим выводам:

--Collections require separate caching

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;


INSERT INTO commit
            (id, repository_id)
VALUES      (DEFAULT, 1);

INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '0a1,5...', 'README.txt');       

INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '17c17...', 'web.xml');

--JdbcTransaction - committed JDBC Connection

--Load Commit from database

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;


SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1 

--JdbcTransaction - committed JDBC Connection

--Load Commit from cache

SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1 

--JdbcTransaction - committed JDBC Connection

Хотя сущность Commit извлекается из кэша, коллекция Change всегда извлекается из базы данных. Поскольку Изменения также неизменяемы, мы хотели бы также кэшировать их, чтобы избежать ненужных обходов базы данных.

Включение поддержки кэша коллекций

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

@ElementCollection
@CollectionTable(
    name="commit_change",
    joinColumns=@JoinColumn(name="commit_id")
)
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_ONLY
)
private List changes = new ArrayList<>();

Повторный запуск предыдущего теста приведет к следующему результату:

--Collections require separate caching

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;


INSERT INTO commit
            (id, repository_id)
VALUES      (DEFAULT, 1);

INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '0a1,5...', 'README.txt');       

INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '17c17...', 'web.xml');

--JdbcTransaction - committed JDBC Connection

--Load Commit from database

SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;


SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1 

--JdbcTransaction - committed JDBC Connection

--Load Commit from cache

--JdbcTransaction - committed JDBC Connection

Как только коллекция будет кэширована, мы сможем извлечь объект Commit вместе со всеми его Изменениями , не попадая в базу данных.

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

Код доступен на GitHub .