Принудительный откат транзакции при ошибках валидации в Seam - PullRequest
6 голосов
/ 13 мая 2010

Быстрая версия: Мы ищем способ принудительного отката транзакции, когда возникают определенные ситуации во время выполнения метода на компоненте поддержки, но мы хотели бы, чтобы откат происходил без необходимости показывать пользователю общую страницу ошибки 500. Вместо этого мы хотели бы, чтобы пользователь увидел форму, которую она только что отправила, и FacesMessage, указывающее, в чем заключалась проблема.

Длинная версия: У нас есть несколько компонентов поддержки, которые используют компоненты для выполнения нескольких связанных операций с базой данных (используя JPA / Hibernate). Во время процесса может произойти ошибка после выполнения некоторых операций с базой данных. Это может быть по нескольким причинам, но для этого вопроса, давайте предположим, что произошла ошибка проверки, обнаруженная после того, как произошли некоторые записи в БД, которые не были обнаружены до того, как произошли записи. Когда это произойдет, мы хотим убедиться, что все изменения в БД до этого момента будут отменены. Seam может с этим справиться, потому что если вы выбросите RuntimeException из текущего FacesRequest, Seam откатит текущую транзакцию.

Проблема в том, что пользователю показывается страница с общей ошибкой. В нашем случае нам бы хотелось, чтобы пользователю показывали страницу, на которой он находился, с описательным сообщением о том, что пошло не так, и имели возможность исправить неверный ввод, вызвавший проблему. Решение, которое мы придумали, заключается в создании исключения из компонента, который обнаруживает проблему проверки с помощью аннотации:

@ApplicationException( rollback = true )

Тогда наш компонент поддержки может поймать это исключение, предположить, что компонент, который его выдал, опубликовал соответствующий FacesMessage, и просто вернуть null, чтобы вернуть пользователя на страницу ввода с отображенной ошибкой. Аннотация ApplicationException указывает Seam откатить транзакцию, и мы не показываем пользователю страницу с общей ошибкой.

Это хорошо сработало для первого места, где мы его использовали, и оказалось, что он делал только вставки Второе место, где мы пытались его использовать, мы должны что-то удалить во время процесса. Во втором случае все работает, если нет ошибки проверки. Если возникает ошибка проверки, возникает исключение отката, и транзакция помечается для отката. Даже если откат базы данных не был отменен, когда пользователь исправляет неверные данные и повторно отправляет страницу, мы получаем:

java.lang.IllegalArgumentException: Removing a detached instance 

Отсоединенный экземпляр лениво загружается из другого объекта (существует отношение многие к одному). Этот родительский объект загружается при создании экземпляра компонента поддержки. Поскольку транзакция была откатана после ошибки проверки, объект теперь отсоединен.

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

Итак, мой вопрос: как другие люди справляются с обработкой ошибок одновременно и правильно управляют транзакциями? А еще лучше, если бы я мог использовать все, что у нас есть сейчас, если кто-то обнаружит то, что я делаю неправильно, это было бы относительно легко исправить.

Я прочитал статью Seam Framework на Унифицированная страница ошибок и обработка исключений , но это больше ориентировано на более общие ошибки, с которыми может столкнуться ваше приложение.

Обновление : вот некоторый псевдокод и детали потока страниц.

В этом случае предположим, что мы редактируем некоторую информацию о пользователе (в данном случае мы фактически не имеем дело с пользователем, но я не собираюсь публиковать фактический код).

Файл edit.page.xml функциональности редактирования содержит простой шаблон перезаписи для URL-адреса RESTful и два правила навигации:

  1. Если результат был успешным, отредактируйте пользователя на соответствующую страницу просмотра, чтобы увидеть обновленную информацию.
  2. Если пользователь нажал кнопку отмены, перенаправьте пользователя на соответствующую страницу просмотра.

Файл edit.xhtml довольно прост с полями для всех частей пользователя, которые можно редактировать.

Поддерживающий компонент имеет следующие аннотации:

@Name( "editUser" )
@Scope( ScopeType.PAGE )

Есть некоторые внедренные компоненты, такие как пользователь:

@In
@Out( scope = ScopeType.CONVERSATION ) // outjected so the view page knows what to display
protected User user;

У нас есть метод save для компонента поддержки, который делегирует работу для сохранения пользователя:

public String save()
{
    try
    {
        userManager.modifyUser( user, newFName, newLName, newType, newOrgName );
    }
    catch ( GuaranteedRollbackException grbe )
    {
        log.debug( "Got GuaranteedRollbackException while modifying a user." );
        return null;
    }

    return USER_EDITED;
}

Наше исключение GuaranteedRollbackException выглядит так:

@ApplicationException( rollback = true )
public class GuaranteedRollbackException extends RuntimeException
{
    public GuaranteedRollbackException(String message) {
        super(message);
    }
}

UserManager.modifyUser выглядит примерно так:

public void modifyUser( User user, String newFName, String newLName, String type, String newOrgName )
{
    // change the user - org relationship
    modifyUser.modifyOrg( user, newOrgName );

    modifyUser.modifyUser( user, newFName, newLName, type );
}

ModifyUser.modifyOrg делает что-то вроде

public void modifyOrg( User user, String newOrgName )
{
    if (!userValidator.validateUserOrg( user, newOrgName ))
    {
        // maybe the org doesn't exist something. we don't care, the validator
        // will add the appropriate error message for us
        throw new GauaranteedRollbackException( "couldn't validate org" );
    }

    // do stuff here to change the user stuff
    ...
}

ModifyUser.modifyUser похож на modifyOrg.

Теперь (вам придется совершить этот прыжок со мной, потому что это не обязательно звучит так, как будто это проблема в этом сценарии пользователя, но это из-за того, что мы делаем) предположим, что изменение org вызывает modifyUser не в состоянии проверить, но это невозможно проверить этот провал заранее. Мы уже записали обновление org в базу данных в нашем текущем txn, но, поскольку пользовательское изменение не может выполнить проверку, исключение GuaranteedRollbackException помечает транзакцию для отката. В этой реализации мы не можем использовать БД в текущей области, когда мы снова визуализируем страницу редактирования, чтобы отобразить сообщение об ошибке, добавленное средством проверки. Во время рендеринга мы нажимаем на БД, чтобы получить что-то для отображения на странице, а это невозможно, потому что сессия недействительна:

Вызвано org.hibernate.LazyInitializationException с сообщением: «не удалось инициализировать прокси - нет сеанса»

Ответы [ 3 ]

1 голос
/ 15 мая 2010

Я должен согласиться с @duffymo о проверке до начала транзакции. Довольно сложно обработать исключения из базы данных и представить их пользователю.

Причина, по которой вы получаете отсоединенное исключение, наиболее вероятна, потому что вы думаете, что что-то записали в базу данных, а затем вызываете команду remove on или refresh для объекта, а затем вы пытаетесь что-то записать снова.

Вместо этого вам нужно создать long-running conversation с flushMode, установленным на MANUAL. Затем вы начинаете сохранять вещи, а затем можете выполнять проверку, и если это нормально, вы сохраняете снова. После того, как вы закончите и все будет хорошо, вы звоните entityManager.flush(). Который сохранит все в базе данных.

И если что-то не получилось, вы не смываете. Вы просто return null или "error" с некоторым сообщением. Позвольте мне показать вам какой-нибудь псевдокод.

Допустим, у вас есть лицо и организация. Теперь вам нужно сохранить Person, прежде чем вы сможете поместить человека в Организацию.

private Person person;
private Organization org;

@Begin(join=true,FlushMode=MANUAL) //yes syntax is wrong, but you get the point
public String savePerson() {
//Inside some save method, and person contains some data that user has filled through a form

//Now you want to save person if they have name filled in (yes I know this example should be done from the view, but this is only an example
try {
  if("".equals(person.getName()) {
    StatusMessages.instance().add("User needs name");
    return "error"; //or null
  }
  entityManager.save(person);
  return "success";
} catch(Exception ex) {
  //handle error
  return "failure";
}
}

Обратите внимание, что мы теперь спасем человека, но не сбросили транзакцию. Тем не менее, он проверит ограничения, которые вы установили для вашего объекта. (@NotNull, @NotEmpty и т. Д.). Таким образом, он будет только симулировать сохранение.

Теперь вы сохраняете организацию для человека.

@End(FlushMode=MANUAL) //yes syntax is wrong, but you get the point
public String saveOrganization() {
//Inside some save method, and organization contains some data that user has filled through a form, or chosen from combobox

org.setPerson(person); //Yes this is only demonstration and should have been collection (OneToMany)
//Do some constraint or validation check
entityManager.save(org);
//Simulate saving org
//if everything went ok
entityManager.flush() //Now person and organization is finally stored in the database
return "success";
}

Здесь вы даже можете поместить материал в try catch и возвращать успех только в том случае, если исключений не было, так что вы не попадете на страницу ошибки.

Обновление

Вы можете попробовать это:

@PersistenceContext(type=EXTENDED)
EntityManager em;

Это позволит компоненту с состоянием иметь расширенный контекст персистентности EJB3. Сообщения, полученные в запросе, остаются в управляемом состоянии до тех пор, пока существует компонент, поэтому любые последующие вызовы метода для компонента с состоянием могут обновлять их без необходимости явного вызова EntityManager. Это может избежать вашего LazyInitializationException. Теперь вы можете использовать em.refresh(user);

0 голосов
/ 27 мая 2010

Я сталкивался с этой ситуацией в последнее время в различных формах.

Лучшее, что я нашел, - это рассматривать объект, который у вас есть, как объект значения (что в основном и происходит после отката). Чтобы удалить его из базы данных, найдите его «прикрепленного» близнеца, используя поиск по его идентификатору (который не попадет в базу данных, поскольку он почти наверняка кэширован), а затем удалите возвращенный объект.

Аналогично для обновлений: получите новую копию и обновите ее.

Это немного хлопотно, но позволяет избежать длительных транзакций и всех связанных с этим проблем блокировки.

0 голосов
/ 13 мая 2010

Я думаю, что проверка должна быть выполнена до того, как транзакция когда-либо будет инициирована.

...