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 longermerge
(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
- (Spring
@Transactional
advice around UI controller: begin) - Save
- Process 1
- Process 2
- (
@Transactional
: commit)
- (Spring
- Any service might throw a validation exception (JSR 303), which rolls back as expected (messages are displayed in the UI)
EntityManager.merge()
is dangerous. See JPA 2.1, section "3.3.3 Transaction Rollback". It says: "[..] the state of version attributes and generated state (e.g., generated primary keys) may be inconsistent." Passing such a detached entity to the merge operation "may fail" as this section points out. Section "3.2.7.1 Merging Detached Entity State" has the explanation: "Any Version columns used by the entity must be checked by the persistence runtime implementation during the merge operation [..]". – Martin Andersson