Недавно в нашем проекте мы сообщили о странной ошибке. В одном отчете, где мы отображаем исторические данные, предоставленные Hibernate Envers , пользователи обнаружили дублирующиеся записи в раскрывающемся списке , используемом для фильтрации. Мы попытались найти источник этой ошибки, но, потратив несколько часов на просмотр кода, ответственного за эту функциональность, нам пришлось отказаться и запросить дамп из производственной базы данных, чтобы проверить, что на самом деле хранится в одной таблице. И когда мы получили его и начали расследование, оказалось, что в Hibernate Envers 3.6 есть ошибка, которая является причиной наших проблем. Но, к счастью, после некоторого расследования и неоценимой помощи от Адама Варски (автора Envers) нам удалось решить эту проблему.
Ошибка сама
Давайте рассмотрим следующий сценарий:
- Транзакция начата. Мы вставляем некоторые проверенные объекты во время этого, а затем он откатывается.
- Тот же EntityManager используется повторно для запуска другой транзакции
- Вторая транзакция совершена
Но когда мы проверим таблицы аудита для сущностей, которые были созданы, а затем откатаны на первом этапе, мы заметим, что они все еще там и не были откачены, как мы ожидали. Мы смогли воспроизвести его в тесте на провал в нашем проекте, поэтому следующим шагом была подготовка теста на провал в Envers, чтобы мы могли проверить, работает ли наше исправление.
Провал тест
Простейшие тестовые примеры, уже присутствующие в Envers, находятся в классе Simple.java, и они выглядят довольно просто:
public class Simple extends AbstractEntityTest { private Integer id1; public void configure(Ejb3Configuration cfg) { cfg.addAnnotatedClass(IntTestEntity.class); } @Test public void initData() { EntityManager em = getEntityManager(); em.getTransaction().begin(); IntTestEntity ite = new IntTestEntity(10); em.persist(ite); id1 = ite.getId(); em.getTransaction().commit(); em.getTransaction().begin(); ite = em.find(IntTestEntity.class, id1); ite.setNumber(20); em.getTransaction().commit(); } @Test(dependsOnMethods = "initData") public void testRevisionsCounts() { assert Arrays.asList(1, 2).equals(getAuditReader().getRevisions(IntTestEntity.class, id1)); } @Test(dependsOnMethods = "initData") public void testHistoryOfId1() { IntTestEntity ver1 = new IntTestEntity(10, id1); IntTestEntity ver2 = new IntTestEntity(20, id1); assert getAuditReader().find(IntTestEntity.class, id1, 1).equals(ver1); assert getAuditReader().find(IntTestEntity.class, id1, 2).equals(ver2); } }
поэтому подготовка моего сценария неудачного выполнения теста, описанного выше, не была ракетостроением:
/** * @author Tomasz Dziurko (tdziurko at gmail dot com) */ public class TransactionRollbackBehaviour extends AbstractEntityTest { public void configure(Ejb3Configuration cfg) { cfg.addAnnotatedClass(IntTestEntity.class); } @Test public void testAuditRecordsRollback() { // Given EntityManager em = getEntityManager(); em.getTransaction().begin(); IntTestEntity iteToRollback = new IntTestEntity(30); em.persist(iteToRollback); Integer rollbackedIteId = iteToRollback.getId(); em.getTransaction().rollback(); // When em.getTransaction().begin(); IntTestEntity ite2 = new IntTestEntity(50); em.persist(ite2); Integer ite2Id = ite2.getId(); em.getTransaction().commit(); // Then List<Number> revisionsForSavedClass = getAuditReader().getRevisions(IntTestEntity.class, ite2Id); assertEquals(revisionsForSavedClass.size(), 1, "There should be one revision for inserted entity"); List<Number> revisionsForRolledbackClass = getAuditReader().getRevisions(IntTestEntity.class, rollbackedIteId); assertEquals(revisionsForRolledbackClass.size(), 0, "There should be no revisions for insert that was rolled back"); } }
Теперь я мог убедиться, что тесты не пройдены на разветвленной ветке 3.6, и проверить, что исправление, которое у нас было, делает этот тест зеленым.
Исправление
После написания неудачного теста в нашем проекте я поместил несколько точек останова в код Envers, чтобы лучше понять, что там не так. Но представьте, что вас бросают в проект, разработанный несколькими годами умнее, чем вы, программистов. Я чувствовал себя подавленным и понятия не имел, где следует применить исправление, а что именно не работает, как ожидалось. К счастью, в моей компании у нас на борту Адам Варски. Он является первым автором Envers, и на самом деле он указал нам решение.
Само исправление содержит только одну проверку, которая регистрирует процессы аудита, которые будут выполняться при завершении транзакции, только когда такие процессы все еще находятся в Map <Transaction, AuditProcess> для данной транзакции. Это звучит сложно, но если вы посмотрите на класс AuditProcessManager в этом коммите, должно быть более понятно, что там происходит.
Официальный путь
Помимо обнаружения проблемы и ее устранения, есть еще несколько официальных шагов, которые необходимо выполнить, чтобы включить исправление в Envers.
Шаг 1. Создание проблемы JIRA с ошибкой — https://hibernate.onjira.com/browse/HHH-7682
Шаг 2: Создайте локальную ветку Envers-bugfix-HHH-7682 из разветвленного Hibernate 3.6
Шаг 3. Подтвердите и отправьте неудачный тест и исправьте его в локальном и удаленном репозитории на Github.
Шаг 4. Создание запроса извлечения — https://github.com/hibernate/hibernate-orm/pull/393
Шаг 5: дождаться слияния
И это все. Теперь исправление объединено с основным репозиторием, и у нас меньше одной ошибки в мире открытого исходного кода.