开发者

JPA transaction rollback retry and recovery: merging entity with auto-incremented @Version

I'd like to recover after a failed transaction.

Now, of course after any rollback, all entities become detached and the entity manager is closed. However, the UI still holds the detached entities. Obviously we can't just throw away the user's changes, so we'd like to let them retry (fix the highlighted validation error, then click the button again).

Following the Java Persistence WikiBook,

One method of error handling is to call merge for each managed object after the commit fails into a new EntityManager, then try to commit the new EntityManager. One issue may be that any ids that were assigned, or optimistic lock versions that were assigned or incremented may need to be reset. Also, if the original EntityManager was EXTENDED, any objects that were in use would still become detached, and need to be reset.

This option开发者_如何学编程 seems straightforward at first, until we inevitably encounter exactly those anticipated issues. Some services might trigger a flush for various reasons, which increments @Version fields in both the DB (which is rolled back) and Java entities (which are not). The next "save" calls merge, which throws an unexpected OptimisticLockException.

Is there a reliable way to "rollback" version fields in the Java entity beans?

OK, that seams hard. We have cascaded entities with their own @Versions, so doing it manually seems fragile. (How can we reliably know the original (persisted) versions anyway? Can't query, because some other user might successfully update the entity in the mean time; querying the current version could break oplocking!)

Another more involved method to error handling is to always work with a non-transactional EntityManager. When it's time to commit, a new EntityManager is created, the non-transactional objects are merged into it, and the new EntityManager is committed. If the commit fails, only the state of the new EntityManager may be inconsistent, the original EntityManager will be unaffected. This can allow the problem to be corrected, and the EntityManager re-merged into another new EntityManager. If the commit is successful any commit changes can be merged back into the original EntityManager, which can then continue to be used as normal. This solution requires a fair bit of overhead, so should only be used if error handling is really required, and the JPA provider provides no alternatives.

This seems logical. Does anyone have any experience implementing this kind of recovery with two EntityManagers (especially with Spring)? Any pitfalls I should be aware of before attempting it? It seems like every service and DAO would now have to become aware of the two entity managers (with Spring, today they are almost persistence-layer agnostic). DAO 'find' operations use one EM; 'update' uses another. Or have separate 'read' and 'write' DAOs. Ouch.

Other options I've considered include:

  • Use DTOs in the UI, so auto-incrementing does not affect anything. Ugly.
  • Move call to merge to the end of any composed operation. Entity is only attached after all validation and state updates have succeeded. Seems strange that the "save" service would no longer merge (only validate). In effect, the UI would take responsibility for calling the DAO! Is this as unusual as it sounds?

Advice? Thanks :-)

Update My architecture includes:

  • Detached entities updated by UI (JSF)
  • Entity IDs are NOT autogenerated (pre-assigned UUIDs and/or business keys)
  • Entities have auto-incremented @Version fields for oplocking
  • "Save" service validates, calls em.merge (JPA over Hibernate)
  • "Process" services validate, apply business logic, update entity state
  • Services can be composed. One UI button might do
    1. (Spring @Transactional advice around UI controller: begin)
    2. Save
    3. Process 1
    4. Process 2
    5. (@Transactional: commit)
  • Any service might throw a validation exception (JSR 303), which rolls back as expected (messages are displayed in the UI)


You ask two different questions and then a general call for advice. I'm going with the advice (would have put this in a comment but SO won't let me comment yet...)

Using DTO's (or any UI-only data structure) in the UI is not ugly - it's architecturally sound. I agree with JB Nizet that the non-transactional EM approach essentially turns your entities into DTO's. If you must use entities in your UI (and I realize Spring encourages this), this is the way to go.

In my experience the main reason people want to use entities as UI scratch storage is to get access to the complex validation logic therein. You might also want to consider refactoring towards making that logic accessible without invoking your full domain model.


Can only support JB Nizet's comment: merge only copies the state of the detached entities.


Obviously we can't just throw away the user's changes

if the user is trying to update, and someone else already changed it, the user better refresh the copy and make changes again no?


Since EntityManager.merge() creates a copy of the passed entity, the solution seems pretty straightforward: maintain a reference to the passed entity in UI (JSF bean) until the transaction completes successfully, and only then update the reference with the copied entity:

@ManagedBean
@SomeScoped
public class EntityBean
{
    @EJB
    private PersistenceService service;

    private Object entity;

    public void update()
    {
        try
        {
            // service.update() is a CMT method
            Object updatedEntity = service.update(entity);

            // if we are here transaction has completed successfully, so update the reference 
            entity = updatedEntity;
        }
        catch(Exception e)
        {
            // if catched, entity has not been modified (just like transaction never happened)
            // now handle exception (log, add a FacesMessage, rethrow, ...)
        }
    }

    ...
}

You can re-render the UI, and the UIComponents will use the very same entity user has filled before submitting the previous request.

This way you don't have to worry about resetting @Ids, @Versions, nor any other changed field or property, including cascading associations, nor using multiple EntityManagers, nor using extended persistence context.

Nevertheless, this simple example has some limitations:

  1. never use EntityManager.persist on a "passed" entity, since persist does not perform the copy. You have to always use EntityManager.merge, but there are cases where this simply is not possible.

  2. the passed entities have to be in "TRANSIENT" or "DETACHED" state for EntityManager.merge to create a copy (otherwise the passed entity is returned an no copy is performed). This implies that using extended persistence context or OpenSessionInViewFilter and derivates is strictly forbidden.

  3. Special care should be take when implementing service composition with sub-transactions: you may end with screwed entities when a sub-transaction completes successfully but the parent rollbacks.

Note that I don't know how Spring works, so all of this simply may be not applicable.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜