Статьи

JPA и CMT — почему ловли исключений постоянства недостаточно?

Находиться в мире EJB и JPA, используя CMT ( Container Managed Transactions ), очень удобно. Просто определите несколько аннотаций, чтобы разграничить границу транзакции (или использовать значения по умолчанию), и все — не нужно возиться с ручными операциями начала, фиксации или отката. Один из способов отката вашей транзакции состоит в том, чтобы вызвать исключение, не относящееся к приложению (или исключение приложения с откатом = true), из бизнес-метода вашего EJB. Это кажется простым: если во время какой-либо операции есть вероятность, что будет сгенерировано исключение, и вы не хотите откатывать свой tx, тогда вам просто нужно перехватить это исключение, и вы в порядке. Теперь вы можете повторить энергозависимую операцию еще раз в той же активной транзакции.

Теперь все верно для исключений приложений, генерируемых компонентами пользователя . Вопрос в том — что с исключениями выбрасывать из других компонентов? Как EntityManager JPA, выбрасывающий EntityManager PersistenceException ? И вот тут начинается история.

Чего мы хотим достичь

Представьте себе следующий сценарий: у вас есть объект с именем E. Он состоит из:

  • id — это первичный ключ,
  • имя — это какое-то понятное человеку имя сущности,
  • content — какое-то произвольное поле, содержащее строку — оно имитирует «расширенный атрибут», который, например, вычисляется во время сохранения / слияния и может привести к ошибкам.
  • code — содержит строки OK или ERROR — определяет, успешно ли сохранены расширенные атрибуты, или нет,

Вы хотите сохранить E. Вы предполагаете, что основные атрибуты E всегда будут успешно сохраняться. Однако для расширенных атрибутов требуются некоторые дополнительные вычисления или операции, которые могут привести, например, к нарушению ограничения из базы данных. Если такая ситуация возникает, вы все равно хотите, чтобы E сохранялся в базе данных (но только с заполненными базовыми атрибутами и атрибутом кода, установленным в «ОШИБКА»).

Другими словами, это то, что вы могли бы подумать:

  1. Сохраняйте E с его основными атрибутами,
  2. Попробуйте обновить его хрупкими расширенными атрибутами,
  3. Если PersistenceException был сгенерирован с шага 2. — поймайте его, установите для атрибута «code» значение «ERROR» и очистите все дополнительные атрибуты (они вызвали исключение),
  4. Обновление Е.

Наивное решение

Переходя к EJB-коду, вы можете попытаться это сделать (предположим, по умолчанию TransactionAttributes):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public void mergeEntity() {
    MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');
 
    em.persist(entity);
 
    // This will raise DB constraint violation
    entity.setContent('tooLongContentValue');
 
    // We don't need em.merge(entity) - our entity is in managed mode.
 
    try {
        em.flush();  // Force the flushing to occur now, not during method commit.
    } catch (PersistenceException e) { 
        // Clear the properties to be able to persist the entity.
        entity.setContent('');
        entity.setCode('ERROR');
 
       // We don't need em.merge(entity) - our entity is in managed mode.
    }
}

Что не так с этим примером?

Перехват исключительной EntityManager генерируемой EntityManager , не препятствует откату транзакции . Это не значит, что отсутствие кэширования исключения в вашем EJB сделает отметку tx для отката. Это исключение не-приложения из EntityManager помечающего tx для отката. Не говоря уже о том, что ресурс может сам по себе пометить tx для отката во внутренних органах. Это фактически означает, что ваше приложение на самом деле не контролирует такое поведение tx. Более того, в результате отката транзакции наша сущность была переведена в отдельное состояние. Следовательно, в конце этого метода потребуются некоторые em.merge(entity) .

Рабочий раствор

Итак, как вы можете справиться с этим автоматическим откатом транзакции? Поскольку мы используем CMT, наш единственный способ — определить другой бизнес-метод, который запустит новую транзакцию и выполнит там все хрупкие операции . Таким образом, даже если PersistenceException будет выброшено (и перехвачено), оно будет помечать только новую транзакцию для отката. Наша главная передача будет нетронутой. Ниже вы можете увидеть пример кода здесь (с краткими инструкциями регистрации):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void mergeEntity() {
    MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');
 
    em.persist(entity);
 
    try {
        self.tryMergingEntity(entity);
    } catch (UpdateException ex) {
        entity.setContent('');
        entity.setCode('ERROR');
    }
}
 
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void tryMergingEntity(final MyEntity entity) throws UpdateException {
    entity.setContent('tooLongContentValue');
 
    em.merge(entity);
 
    try {
        em.flush();
    } catch (PersistenceException e) {
        throw new UpdateException();
    }
}

Имейте в виду, что:

  • UpdateException — это @ApplicationException которая расширяет Exception (поэтому по умолчанию это rollback=false ). Он используется для информирования о сбое операции обновления. В качестве альтернативы вы можете изменить сигнатуру метода tryMergingEntity(-) чтобы она возвращала логическое значение вместо void. Это логическое значение может описывать, было ли обновление успешным или нет.
  • self — это ссылка на наш собственный EJB. Это обязательный шаг для использования EJB Container proxy, который заставляет @TransactionAttribute вызванного метода работать. В качестве альтернативы вы можете использовать SessionContext#getBusinessObject(clazz).tryMergingEntity(entity) .
  • em.merge(entity) имеет решающее значение. Мы tryMergingEntity(-) новую транзакцию в tryMergingEntity(-) поэтому объект не находится в контексте постоянства.
  • В этом методе нет необходимости в каком-либо другом слиянии или очистке. Передача tx не была отменена, поэтому обычные функции CMT подтверждают это, что означает, что все изменения в сущности будут автоматически сбрасываться при фиксации tx.

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

JTA BMT Solution

Все время мы говорили о CMT. Что насчет JTA BMT? Ну, а в качестве бонуса найдите приведенный ниже код, который показывает, как бороться с этой проблемой с помощью BMT (доступно также и здесь ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void mergeEntity() throws Exception {
    utx.begin();
    MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');
    em.persist(entity);
    utx.commit();
 
    utx.begin();
    entity.setContent('tooLongContentValue');
 
    em.merge(entity);
 
    try {
        em.flush();
    } catch (PersistenceException e) {
        utx.rollback();
 
        utx.begin();
        entity.setContent('');
        entity.setCode('ERROR');
 
        em.merge(entity);
        utx.commit();
    }
}

С JTA BMT мы можем сделать все это одним способом. Это потому, что мы контролируем, когда начинается наш tx, и фиксирует / откатывает (взгляните на эти utx.begin () / commit () / rollback (). Тем не менее, результат тот же — после выброса PersistenceException наша tx помечается для отката). и вы можете проверить это с помощью UserTransaction#getStatus() и сравнить его с одной из констант, таких как Status.STATUS_MARKED_ROLLBACK . Вы можете проверить весь код в моей учетной записи GitHub .

Справка: JPA и CMT — почему ловли исключений постоянства недостаточно? от нашего партнера JCG Петра Новицки в блоге Петра Новицкого .