How to control JPA persistence in Wicket forms?
I'm building an application using JPA 2.0 (Hibernate implementation), Spring, and Wicket开发者_StackOverflow. Everything works, but I'm concerned that my form behaviour is based around side effects.
As a first step, I'm using the OpenEntityManagerInViewFilter
. My domain objects are fetched by a LoadableDetachableModel
which performs entityManager.find()
in its load
method. In my forms, I wrap a CompoundPropertyModel
around this model to bind the data fields.
My concern is the form submit actions. Currently my form submits pass the result of form.getModelObject()
into a service method annotated with @Transactional
. Because the entity inside the model is still attached to the entity manager, the @Transactional
annotation is sufficient to commit the changes.
This is fine, until I have multiple forms that operate on the same entity, each of which changes a subset of the fields. And yes, they may be accessed simultaneously. I've thought of a few options, but I'd like to know any ideas I've missed and recommendations on managing this for long-term maintainability:
- Fragment my entity into sub-components corresponding to the edit forms, and create a master entity linking these together into a
@OneToOne
relationship. Causes an ugly table design, and makes it hard to change forms later. - Detach the entity immediately it's loaded by the
LoadableDetachableModel
, and manually merge the correct fields in the service layer. Hard to manage lazy loading, may need specialised versions of the model for each form to ensure correct sub-entities are loaded. - Clone the entity into a local copy when creating the model for the form, then manually merge the correct fields in the service layer. Requires implementation of a lot of copy constructors / clone methods.
- Use Hibernate's
dynamicUpdate
option to only update changed fields of the entity. Causes non-standard JPA behaviour throughout the application. Not visible in the affected code, and causes a strong tie to Hibernate implementation.
EDIT
The obvious solution is to lock the entity (i.e. row) when you load it for form binding. This would ensure that the lock-owning request reads/binds/writes cleanly, with no concurrent writes taking place in the background. It's not ideal, so you'd need to weigh up the potential performance issues (level of concurrent writes).
Beyond that, assuming you're happy with "last write wins" on your property sub-groups, then Hibernate's 'dynamicUpdate' would seem like the most sensible solution, unless your thinking of switching ORMs anytime soon. I find it strange that JPA seemingly doesn't offer anything that allows you to only update the dirty fields, and find it likely that it will in the future.
Additional (my original answer)
Orthogonal to this is how to ensure you have a transaction open when when your Model loads an entity for form binding. The concern being that the entities properties are updated at that point and outside of transaction this leaves a JPA entity in an uncertain state.
The obvious answer, as Adrian says in his comment, is to use a traditional transaction-per-request filter. This guarantees that all operations within the request occur in single transaction. It will, however, definitely use a DB connection on every request.
There's a more elegant solution, with code, here. The technique is to lazily instantiate the entitymanager and begin the transaction only when required (i.e. when the first EntityModel.getObject() call happens). If there is a transaction open at the end of the request cycle, it is committed. The benefit of this is that there are never any wasted DB connections.
The implementation given uses the wicket RequestCycle object (note this is slightly different in v1.5 onwards), but the whole implementation is in fact fairly general, so and you could use it (for example) outwith wicket via a servlet Filter.
After some experiments I've come up with an answer. Thanks to @artbristol, who pointed me in the right direction.
- I have set a rule in my architecture: DAO save methods must only be called to save detached entities. If the entity is attached, the DAO throws an
IllegalStateException
. This helped track down any code that was modifying entities outside a transaction. - Next, I modified my
LoadableDetachableModel
to have two variants. The classic variant, for use in read-only data views, returns the entity from JPA, which will support lazy loading. The second variant, for use in form binding, uses Dozer to create a local copy. - I have extended my base DAO to have two save variants. One saves the entire object using
merge
, and the other uses Apache Beanutils to copy a list of properties.
This at least avoids repetitive code. The downsides are the requirement to configure Dozer so that it doesn't pull in the entire database by following lazy loaded references, and having yet more code that refers to properties by name, throwing away type safety.
精彩评论