2010-05-12 15 views
6

Versión rápida: Estamos buscando una forma de forzar una reversión de una transacción cuando se producen situaciones específicas durante la ejecución de un método en un bean de respaldo, pero nos gustaría que revertir para que ocurra sin tener que mostrarle al usuario una página de error genérica de 500. En su lugar, nos gustaría que el usuario vea el formulario que acaba de enviar y un FacesMessage que indica cuál fue el problema.Forzar una transacción para deshacer errores de validación en Seam

Versión larga: Tenemos algunos respaldos que usan componentes para realizar algunas operaciones relacionadas en la base de datos (usando JPA/Hibernate). Durante el proceso, puede producirse un error después de que se hayan producido algunas de las operaciones de la base de datos. Esto podría deberse a varias razones diferentes, pero para esta pregunta, supongamos que ha habido un error de validación que se detecta después de que se produjeron algunas grabaciones de DB que no fueron detectables antes de que se produjeran las escrituras. Cuando esto suceda, nos gustaría asegurarnos de que todos los cambios de db hasta este punto se retrotraerán. Seam puede manejar esto porque si arrojas una RuntimeException de la FacesRequest actual, Seam revertirá la transacción actual.

El problema es que al usuario se le muestra una página de error genérica. En nuestro caso, nos gustaría que se muestre al usuario la página en la que se encontraba con un mensaje descriptivo sobre lo que salió mal, y que tenga la oportunidad de corregir la información incorrecta que causó el problema. La solución que hemos llegado con es lanzar una excepción de la componente que descubre el problema de validación con la anotación:

@ApplicationException(rollback = true) 

Entonces nuestro bean de respaldo puede detectar esta excepción, asumir el componente que arrojó ha publicado la FacesMessage apropiado, y simplemente devuelve null para llevar al usuario a la página de entrada con el error mostrado. La anotación ApplicationException le dice a Seam que deshaga la transacción y no mostramos al usuario una página de error genérica.

Esto funcionó bien para el primer lugar en el que lo usamos, que pasaba a ser solo insertos. El segundo lugar donde tratamos de usarlo, tenemos que eliminar algo durante el proceso. En este segundo caso, todo funciona si no hay un error de validación. Si se produce un error de validación, se lanza la excepción de retrotracción y la transacción se marca para deshacer. Incluso si han ocurrido ninguna modificación de bases de datos que revertirse, cuando el usuario corrige los malos datos y vuelve a presentar la página, que van a obtener:

java.lang.IllegalArgumentException: Removing a detached instance 

La instancia separada se carga con pereza de otro objeto (hay una relación de muchos a uno). Ese objeto principal se carga cuando se crea una instancia del bean de respaldo. Debido a que la transacción se retrotrajo después del error de validación, el objeto ahora está separado.

Nuestro próximo paso fue cambiar esta página del alcance de la conversación al alcance de la página. Cuando hicimos esto, Seam ni siquiera puede renderizar la página después del error de validación porque nuestra página debe presionar DB para representar y la transacción se ha marcado para deshacer.

Así que mi pregunta es: ¿cómo se manejan otras personas que manejan los errores de manera limpia y manejan las transacciones al mismo tiempo? Mejor aún, me encantaría poder utilizar todo lo que tenemos ahora si alguien puede detectar algo que estoy haciendo mal que sería relativamente fácil de solucionar.

He leído el artículo de Seam Framework en Unified error page and exception handling pero esto se dirige más hacia errores más genéricos que su aplicación pueda encontrar.

Actualización: aquí hay un código de psudo y detalles del flujo de la página.

En este caso, supongamos que estamos editando la información de algunos usuarios (en este caso no estamos tratando con un usuario, pero no vamos a publicar el código real).

archivo edit.page.xml de la funcionalidad de edición contiene un patrón re-escritura simple para una URL REST y dos reglas de navegación:

  1. Si el resultado fue un éxito de edición, redirigir al usuario a la página de vista correspondiente a ver la información actualizada.
  2. Si el usuario presiona el botón cancelar, redirige al usuario a la página de vista correspondiente.

El edit.xhtml es bastante básico con campos para todas las partes de un usuario que se pueden editar.

El bean de respaldo tiene las siguientes anotaciones:

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

Hay algunos componentes inyectados como el usuario:

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

Tenemos un método de ahorrar en el bean de respaldo que delega el trabajo para el 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; 
} 

Nuestra GuaranteedRollbackException se parece a:

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

UserManager.modifyUser ve algo como esto:

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 hace algo como

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 es similar a modifyOrg.

Ahora (vas a tener que dar este paso porque no necesariamente suena como un problema con este escenario de usuario pero es por lo que estamos haciendo) asumiendo que cambiar la organización causa la modifyUser no se puede validar, pero es imposible validar este error antes de tiempo. Ya hemos escrito la actualización de la organización en el db en nuestro txn actual, pero como el usuario no puede validar la modificación, GuaranteedRollbackException marcará la transacción que se revertirá. Con esta implementación, no podemos usar el DB en el ámbito actual cuando estamos visualizando la página de edición nuevamente para mostrar el mensaje de error agregado por el validador. Mientras renderizamos, pulsamos el archivo db para que aparezca algo en la página y eso no es posible porque la sesión no es válida:

Causado por org.hibernate.LazyInitializationException con el mensaje: "no se pudo inicializar el servidor proxy - no Session"

+0

Te puedo ayudar pero ** puedes mostrarlo en código simple ** El primer escenario y El segundo: Componentes, tu página, reglas de navegación y el controlador de excepciones –

+0

@Arthur Ronald FD Garcia, agregué más información junto con algunos fragmentos de código. Esto golpea demasiado código para publicar todo, pero espero que esto te brinde lo que necesitas saber. –

Respuesta

1

Debo estar de acuerdo con @duffymo sobre la validación antes de que se inicie la transacción. Es bastante difícil manejar excepciones de bases de datos y presentarlas al usuario.

La razón por la que obtiene la excepción separada es muy probable porque cree que ha escrito algo en la base de datos, y luego llama a quitar o actualizar el objeto, y luego intenta escribir algo de nuevo.

Lo que debe hacer en su lugar es crear un long-running conversation con flushMode establecido en MANUAL. Luego comienzas a persistir cosas, y luego puedes realizar tu validación, y si eso está bien, persistes nuevamente. Cuando haya terminado y todo esté listo, llame al entityManager.flush(). Que guardará todo en la base de datos.

Y si algo falla, no enjuaga. Usted acaba de return null o "error" con algún mensaje. Déjame mostrarte con un pseudo código.

Supongamos que tiene una entidad Persona y Organización. Ahora necesita almacenar Persona antes de poder poner a la persona en Organización.

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"; 
} 
} 

Tenga en cuenta que ahora guardamos persona, pero no hemos limpiado la transacción. Sin embargo, verificará las restricciones que haya establecido en su entitybean. (@NotNull, @NotEmpty y así sucesivamente). Por lo tanto, solo simulará un guardado.

Ahora guarda la organización por persona.

@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"; 
} 

Aquí incluso se puede poner cosas en try catch y sólo volver éxito si se ha producido no es una excepción, por lo que no son arrojados a la página de error.

actualización

Puede probar esto:

@PersistenceContext(type=EXTENDED) 
EntityManager em; 

Esto hará que un bean con estado tiene un contexto de persistencia EJB3 extendida. Los mensajes recuperados en la consulta permanecen en el estado administrado, siempre que el bean exista, por lo que cualquier método posterior de llamadas al bean con estado puede actualizarlos sin necesidad de realizar ninguna llamada explícita al EntityManager. Esto podría evitar su LazyInitializationException. Ahora puede usar em.refresh(user);

+0

gracias por la respuesta. Estamos pensando en avanzar en esta dirección, pero parece mucho más limpio utilizar la anotación @ApplicationException; solo debe lanzar la excepción y capturarla en su bean de respaldo. Agregué un código y más de una explicación de lo que estamos tratando de hacer arriba. Básicamente estamos tratando de descubrir cómo otras personas están manejando esto. El simple caso de poder validar primero es fácil. Créanme, nos encantaría hacer la validación antes de tocar el DB pero eso no es práctico en nuestro caso. –

+0

terminamos yendo con algo similar a esta solución (las cosas antes de su actualización). Gracias por la sugerencia. –

+0

El problema con Manual Flush es que solo funciona si no tiene que hacer ninguna consulta de base de datos. Si utiliza ajax con algunas sugerencias que requieren consultas en la base de datos o realiza una validación que involucra consultas en la base de datos, no funciona, ya que Hibernate llamará automáticamente cuando haga consultas a la base de datos. En este caso, necesita deshacer la transacción. Tengo el mismo problema que Chris cuando lanzo una ApplicationException (rollback = true) y lo redirecciono a otra página obtengo un LIE con "could not initialize proxy". Curiosamente, esto no sucede en todas las páginas a las que estoy redirigiendo. – Ben

0

Creo que la validación debe hacerse antes de que se inicie la transacción.

+0

Ese es un patrón optimista que sería simple de implementar. ¿Qué sucede cuando tiene un escenario más complicado: puede escribir en la base de datos que podría causar un error de validación en el paso 2 que no pudo validar hasta que finalizó el paso 1?Además, cuando intenta consolidar todo el código relacionado con la tarea 1 en un lugar y la tarea 2 en otro, las cosas se vuelven más difíciles de leer y mantener cuando la validación asociada con 1 y 2 se extrae y se coloca en otro lugar. También complica la validación de la tarea 2 cuando tiene que escribir una lógica especial para que actúe como si la tarea 1 hubiera finalizado. –

+0

@Chris He añadido algunas actualizaciones. Podría tratar de usar un contexto de persistencia extendido. Podría ayudarlo –

0

Me he enfrentado a esta situación últimamente en una variedad de formas.

Lo mejor que encontré es tratar el objeto que tiene como objeto de valor (que básicamente es después de la reversión). Para eliminarlo de la base de datos, busque su gemela 'adjunta' utilizando un buscar por su id (que no irá a la base de datos ya que es casi seguro que está en caché) y luego elimine el objeto que se devolvió.

Similar para actualizaciones: obtenga una copia nueva y actualícela.

Es un poco molesto, pero evita largas transacciones y todos los problemas de bloqueo relacionados con eso.

Cuestiones relacionadas