Автор оригинала: 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 Listchanges = 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 Listchanges = 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 .