Автор оригинала: Vlad Mihalcea.
Как ранее объяснялось , каждая инструкция SQL должна выполняться в контексте транзакции базы данных. Для изменения инструкций (например, ВСТАВКА
, ОБНОВЛЕНИЕ
, УДАЛЕНИЕ
) необходимо использовать блокировки на уровне строк, чтобы обеспечить возможность восстановления и избежать аномалий данных.
Далее я продемонстрирую, что может произойти, если транзакция базы данных не будет завершена должным образом.
Чтобы упростить модульные тесты, я использовал следующую утилиту модульного тестирования для регистрации заданного набора операций в транзакции базы данных:
protected void doInJPA(JPATransactionVoidFunction function) { EntityManager entityManager = null; EntityTransaction txn = null; try { entityManager = emf.createEntityManager(); function.beforeTransactionCompletion(); txn = entityManager.getTransaction(); txn.begin(); function.accept(entityManager); txn.commit(); } catch (RuntimeException e) { if ( txn != null && txn.isActive()) txn.rollback(); throw e; } finally { function.afterTransactionCompletion(); if (entityManager != null) { entityManager.close(); } } }
Учитывая, что у нас есть следующая организация JPA:
@Entity(name = "DateEvent") public class DateEvent { @Id @GeneratedValue private Long id; @Temporal(TemporalType.TIMESTAMP) @Column(name = "`timestamp`") private Calendar timestamp; public DateEvent() { } public DateEvent(Calendar timestamp) { this.timestamp = timestamp; } public Long getId() { return id; } public Calendar getTimestamp() { return timestamp; } }
Модульный тест выглядит следующим образом:
@Test public void testEqualityAfterPersisting() { final Calendar calendar = new GregorianCalendar(); doInJPA(entityManager -> { entityManager.persist( new DateEvent( calendar ) ); } ); doInJPA(entityManager -> { DateEvent dateEvent = entityManager.createQuery( "select e from DateEvent e", DateEvent.class ) .getSingleResult(); assertEquals( calendar, dateEvent.getTimestamp() ); } ); }
Этот тест успешно выполняется на HSQLDB, H2, Oracle и PostgreSQL. В MySQL тест застрял, и журнал выглядит так:
DEBUG [main]: o.h.SQL - select next_val as id_val from hibernate_sequence for update DEBUG [main]: o.h.SQL - update hibernate_sequence set next_val= ? where next_val=? DEBUG [main]: o.h.SQL - insert into DateEvent (`timestamp`, id) values (?, ?) INFO [main]: o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory DEBUG [main]: o.h.SQL - select calendarwi0_.id as id1_0_, calendarwi0_.`timestamp` as timestam2_0_ from DateEvent calendarwi0_ INFO [main]: o.h.t.s.i.SchemaDropperImpl$DelayedDropActionImpl - HHH000477: Starting delayed drop of schema as part of SessionFactory shut-down' DEBUG [main]: o.h.SQL - drop table if exists DateEvent
Последняя строка журнала указывает, где застрял тест. По какой-то причине MySQL не может удалить таблицу Дата события
.
Если вы посмотрите на утилиту обработки транзакций, вы обнаружите, что транзакция фиксируется, когда функция JPATransactionVoidFunction
обратный вызов выполняется без каких-либо исключений и выполняется откат Исключение времени выполнения
.
В этом случае следующее утверждение не выполняется:
assertEquals( calendar, dateEvent.getTimestamp() );
Но ComparisonFailure
расширяет Ошибку утверждения
, которая, в свою очередь, расширяет Ошибку
. Таким образом, ошибка утверждения не будет поймана блоком catch
, и транзакция не получит возможности отката.
наконец
блок всегда выполняется, даже если возникает Ошибка
. В наконец
блоке EntityManager
закрыт, и, поскольку я использую hibernate.hbm2ddl.auto
установлен в create-drop
, Hibernate пытается удалить текущую схему.
Но почему он блокируется? Это можно было бы ожидать в реализации двухфазной блокировки с возможностью повторного чтения, когда операция чтения выполняет общую блокировку результирующего набора. Но механизм хранения InnoDB использует MVCC , поэтому читатели не должны блокировать записи, и здесь нет параллельных транзакций.
В этом примере блокировка происходит при использовании одного сеанса базы данных. Даже если оператор select не получает блокировку на уровне строк, MySQL все равно использует блокировку метаданных для каждой транзакции базы данных, и в соответствии с документацией MySQL:
Чтобы обеспечить сериализуемость транзакций, сервер не должен разрешать одному сеансу выполнять инструкцию языка определения данных (DDL) для таблицы, которая используется в незавершенной явно или неявно запущенной транзакции в другом сеансе.
Поскольку транзакция не завершена должным образом (либо фиксация, либо откат), оператор drop table
застрял. Исправить это очень просто. Нам просто нужно оформить откат по любому Выбрасывается
, чтобы мы могли охватить любое Исключение времени выполнения
, Ошибку
или другие Выбрасываемые
ситуации, в которых у нас все еще может быть шанс выполнить откат транзакции:
catch (Throwable e) { if ( txn != null && txn.isActive()) txn.rollback(); throw e; }
Теперь при выполнении теста мы получаем следующий журнал:
DEBUG [main]: o.h.SQL - update hibernate_sequence set next_val= ? where next_val=? DEBUG [main]: o.h.SQL - insert into DateEvent (`timestamp`, id) values (?, ?) INFO [main]: o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory DEBUG [main]: o.h.SQL - select calendarwi0_.id as id1_0_, calendarwi0_.`timestamp` as timestam2_0_ from DateEvent calendarwi0_ INFO [main]: o.h.t.s.i.SchemaDropperImpl$DelayedDropActionImpl - HHH000477: Starting delayed drop of schema as part of SessionFactory shut-down' DEBUG [main]: o.h.SQL - drop table if exists DateEvent DEBUG [main]: o.h.SQL - drop table if exists hibernate_sequence java.lang.AssertionError: Expected :java.util.GregorianCalendar[time=1455774800060,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Athens",offset=7200000,dstSavings=3600000,useDaylight=true,transitions=138,lastRule=java.util.SimpleTimeZone[id=Europe/Athens,offset=7200000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2016,MONTH=1,WEEK_OF_YEAR=8,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=49,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=7,HOUR_OF_DAY=7,MINUTE=53,SECOND=20,MILLISECOND=60,ZONE_OFFSET=7200000,DST_OFFSET=0] Actual :java.util.GregorianCalendar[time=1455774800000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Athens",offset=7200000,dstSavings=3600000,useDaylight=true,transitions=138,lastRule=java.util.SimpleTimeZone[id=Europe/Athens,offset=7200000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2016,MONTH=1,WEEK_OF_YEAR=8,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=49,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=7,HOUR_OF_DAY=7,MINUTE=53,SECOND=20,MILLISECOND=0,ZONE_OFFSET=7200000,DST_OFFSET=0]
Транзакция должным образом завершена, и, следовательно, ошибка подтверждения получает возможность быть зарегистрированной.
Как правило, транзакция базы данных всегда должна быть завершена (либо зафиксирована, либо откатана), независимо от результата данной операции уровня доступа к данным (успешной или создающей исключение).