A lot of Wicket applications use an ORM framework to work with the database. Because ORMs provide a generic mechanism for loading entities we can create a generic Wicket model that can simplify binding between ORM and Wicket. In this article I will walk you through creating an EntityModel that will make using Wicket and ORM fun, read inside…
In order to load an entity from the database we need two pieces of information: entity’s class and id. Since there is no standard way to get an id from an object we will create an interface that will standardize the process:
1 2 3 4 | public interface Identifiable<T extends Serializable> { T getId(); } |
The simplest entity model can then look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public abstract class AbstractEntityModel<T extends Identifiable< ? >> extends LoadableDetachableModel<T> { private Class clazz; private Serializable id; public AbstractEntityModel(T entity) { clazz = entity.getClass(); id = entity.getId(); } public AbstractEntityModel(Class< ? extends T> clazz, Serializable id) { this.clazz = clazz; this.id = id; } @Override protected T load() { return load(clazz, id); } protected abstract T load(Class clazz, Serializable id); } |
- 26: the method used to perform the actual loading of entity from the ORM
This model can load any entity given an instance or a class and id, and will detach itself at the end of request. The model is abstract because the way to retrieve a handle to the ORM session varies from application to application. For example, in my project that uses Salve's dependency injection and Hibernate a concrete implementation will look like this:
1 2 3 4 5 6 7 8 9 10 11 | public class EntityModel<T> extends AbstractEntityModel<T> { @Dependency private Session session; @Override protected Identifiable load(Class clazz, Serializable id) { return session.get(clazz, id); } } |
- 3: Salve annotation that will make sure field session is injected with the current hibernate session
- 7-10: concrete implementation of the #load() method using hibernate Session
Handling Errors
There are cases when a user deletes an object from the database that another user is currently viewing in the UI. In this case when the UI tries to render and calls getObject() on our EntityModel it will return null. There is nothing worse in the log then a cryptic NullPointerExcepton. A much better approach is to check for this condition and throw a type of exception that can be intercepted and processed appropriately by the UI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public abstract class AbstractEntityModel<T extends Identifiable< ? >> extends LoadableDetachableModel<T> { @Override protected T load() { T entity = load(clazz, id); if (entity == null) { throw new EntityNotFoundException(clazz, id); } return entity; } } |
- 10-13: built-in null check for better error reporting
Then in our RequestCycle subclass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class MyRequestCycle extends WebRequestCycle { @Override public Page onRuntimeException(Page page, RuntimeException e) { if (e instanceof EntityNotFoundException) { return new EntityNotFoundErrorPage((EntityNotFoundException)e); } else { return super.onRuntimeException(page, e); } } } |
EntityNotFoundErrorPage can present a user-friendly message explaining why the error happened.
Using EntityModel to bind to Forms
Suppose the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class EditPersonPage extends WebPage { public EditPersonPage(Long personId) { this(new EntityModel<Person>(Person.class, personId)); } private EditPersonPage(IModel<Person> model) { Form<Person> form = new Form<Person>("form", new CompoundPropertyModel<Person>(model)) { @Override public boolean process() { beginTransaction(); if (super.process()) { commitTransaction(); return true; } else { rollbackTransaction(); return false; } } }; add(form); form.add(new TextField<String>("name")); } } |
When the form is submitted the following series of calls will be made:
- Form#process()
- beginTransaction()
- super.proces()
- Person p=EntityModel.getObject()
- p.setName(name);
- Form#onSubmit()
- commitTransaction();
- EntityModel#detach();
This is interesting because the Person entity is loaded and the TextField sets the value on the loaded Person inside the same transaction, which means this value will be persisted automatically by the ORM when the transaction ends. Any changes done to this form’s data are automatically reflected in the database without any extra code.
Our AbstractEntityModel is already good enough to handle this scenario, but what if we wanted to reuse our form to create a new entity instance instead of just editing one? If we had a way to do this we can then reuse any Form to create or edit an entity. To achieve this we need to add two things to our model:
- Ability to hold on to a transient entity instance (disable detaching while the entity is transient)
- Allow the model to intelligently begin detaching if a transient entity instance has become persistent between getObject() and detach()
Since it is not possible to override LoadableDetachableModel’s detach() we will have to convert our model to implement IModel directly – giving us full control:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | public abstract class AbstractEntityModel<T extends Identifiable< ? >> implements IModel<T> { private final Class clazz; private Serializable id; private T entity; public AbstractEntityModel(T entity) { clazz = entity.getClass(); id = entity.getId(); this.entity = entity; } public AbstractEntityModel(Class< ? extends T> clazz, Serializable id) { this.clazz = clazz; this.id = id; } public T getObject() { if (entity == null) { if (id != null) { entity = load(clazz, id); if (entity == null) { throw new EntityNotFoundException(clazz, id); } } } return entity; } public void detach() { if (entity != null) { if (entity.getId() != null) { id = entity.getId(); entity = null; } } } protected abstract T load(Class clazz, Serializable id); public void setObject(T object) { throw new UnsupportedOperationException(getClass() + " does not support #setObject(T entity)"); } } |
- 6: cache for the entity instance that we keep for the request, only the first #getObject() call will populate it
- 12: notice we initialize the cache if we have the instance
- 25: guard so we only call #load() if we have a valid id
- 39: guard from preventing detaching a detached model
- 41: guard to make sure we do not null out a transient entity instance, we want to keep it until it is persisted
- 43: if a transient instance was persisted keep its id so it can be reloaded
- 44: now that we have a valid id we no longer need to keep the cache
From now on our EntityModel will hold on to the transient entity until it is persisted. If we subclass the Form in the previous example and override its onSubmit() to persist the entity the model will switch into persistent mode after the form is submitted. So, it doesn’t matter if the EntityModel given to that form contains a transient or a persistent instance of the entity, the model and therefore also the form will simply do the right thing.
That’s it for now. Perhaps if there is more interest in such models I will revisit this and add a couple of other useful features.
[...] It is a great day: Igor Vaynberg, the most prominent committer for Apache Wicket (he has the most commits I think) has written up an article about building a smart EntityModel. You can read it at the official Wicket blog: wicketinaction.com [...]
Pingback by First Wicket article by Igor Vaynberg | A Wicket Diary — September 30, 2008 @ 8:57 am
Hi,
Great article…
Small improvement… It is possible to deduct the class of T by using this expression: (Class) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]. By using this trick, you don’t need to include the clazz parameters in the constructor and in the load method.
Cheers,
Bernard.
Comment by Bernard — September 30, 2008 @ 10:46 am
Nice article. Shouldn’t Identifiable extends Serializable?
Comment by Marcell — September 30, 2008 @ 11:06 am
Thanks Igor, some useful tricks here, especially for someone like myself new to both Wicket & Hibernate. I’d love to see a followup article to see how you handle more complex situations, eg collections of entities filtered by certain criteria.
Comment by Chris — September 30, 2008 @ 12:08 pm
Bernard: that works in the simple case however if you have a deeper class hierarchy where the type argument is declared elsewhere (on either a superclass or an interface) the code gets a more complex. You have to recurse up via getSuperclass() and check both the class and any interfaces for the appropriate param. With multiple type params it’s even messier, you have to recurse to the top of the hierarchy then search downwards, filling in the type params as you find them (otherwise you don’t know which actual type param corresponds to which parameter).
Comment by Chris — September 30, 2008 @ 12:22 pm
@Marcell: There are two things here: 1) Identifiable should not extend Serializable because serialization of the object is not Identifiable’s concern. It simply represents something with a serializable id. If you expect all your identifiables to also be serializable you should probably create a new interface that extends both. 2) Serializable is only required if you give the model a transient entity, so only those entities need to be serializable.
My original code had the serialization check built into detach(), but the article was getting a little too long - thats why I hinted at a second part if there was interest.
Comment by ivaynberg — September 30, 2008 @ 4:20 pm
@Chris: I dont think a follow up to this particular article would include querying, but you can definitely take a look at wicket-phonebook project in wicket-stuff svn. It demonstrates filtering resultsets.
Comment by ivaynberg — September 30, 2008 @ 4:24 pm
@Bernard: I agree with Chris, deducing would be nice but hairy. Are there any utility libraries out there that have the entire tracing built into a convenient function?
Comment by ivaynberg — September 30, 2008 @ 4:26 pm
Igor I have a similar model already in my app. But, there’s a gotcha regarding to using it with the DiskStore. Imagine I have a non-persistent entity, where the getId() would return null. Now the model would keep the entity in memory instead of the id. I start editing the model, pass it to another page, in that page i edit it, and then return to the previous page instance. This instance would fetch the model from the DiskStore, which would have the previous version of my entity. So all my changes when I get back is lost. Do you think we can provide a solution for this ?
Comment by Iman Rahmatizadeh — October 1, 2008 @ 1:12 pm
@Iman: Unfortunately passing objects between pages is a known limitation with the diskstore. Currently the stored data would look like this [page1 [model]] [page2 [model']], in order to make this work we would have to rewrite the store to be smarter and store the objects like so [page1] [page2] [model], but this is a pretty big rewrite. You should open a JIRA issue and provide a quickstart, I dont think we will get to it within 1.3 or even 1.4 timeframe, but we will definitely try. In the meanwhile I tend to use Panels instead of Pages for situations like these; its not ideal but at least its a feasible workaround.
Comment by ivaynberg — October 2, 2008 @ 2:29 am
@Igor: Then at detach you check if the entity has no id and is Serializable. What do you do if the entity does not implement Serializable? I mean besides raising a error :P
@Iman: I am using Seam with Wicket and I can say that your problem is a good use case to use conversational context provided by Seam.
Comment by Marcell — October 3, 2008 @ 1:41 pm
@Marcell: There isn’t much you can do if it is not Serializable, that is the contract of the EntityModel if you choose to put in a transient entity. In my actual model I have checks that enforce the contract by trying to serialize the entity in dev mode. Perhaps I will revisit this article and add those checks.
I also do not think seam’s convesational context would be much help to Iman. The problem is that the IModel that goes from page A to page B is cloned and now page A is no longer aware of the changes to the model that page B made. Its a much more deeply rooted problem then just ensuring that the Session instance is the same.
Comment by ivaynberg — October 3, 2008 @ 6:12 pm
@Igor: I wasn’t aware of any workaround with panels. As I use panels extensively in my app, would you care to elaborate how it would help in this scenario ?
Comment by Iman Rahmatizadeh — October 4, 2008 @ 6:22 am
@Iman: its not really a workaround, more of a way you build your app. The problem here is that the model is shared between multiple pages, so the solution is to eliminate multiple pages. Instead of navigation consisting of going from page to page you can instead keep the user on the same page and accomplish the same navigation by swapping out the “content” panel.
eg
So, instead of calling setResponsePage(new EditUserPage()) you swap the panel…
Comment by ivaynberg — October 4, 2008 @ 6:31 am
@Igor: When I mean to use the conversational context you wouldn’t store the transient Model in the EntityModel. You probably would do at detach and attach:
if (entity != null) {
if (if (entity.getId() != null) {
…
} else {
Context.getConversationContext().set(this.getClass().getName()+”#entity”,entity);
}
}
And at getObject():
if (entity == null) {
if (id != null) {
…
} else {
entity = (T) Context.getConversationContext().get(this.getClass().getName()+”#entity”);
}
}
Comment by Marcell — October 4, 2008 @ 11:36 am
@Marcell: Sure. I can do the same thing with the Session and a uuid token stored in the model. The big advantage of just using the model is that it is its own conversational scope. Eg, the user does whatever they need and the conversation ends, then the user presses back button 4 times and tries to do something - error because conversation has already been closed. This wouldnt happen with the model because it would be stored with the page and retrieved again.
Comment by ivaynberg — October 4, 2008 @ 6:27 pm
Whats the adavantage of overriding process and doing a rollback when super.process returns false, instead of just overriding submit? Submit gets called only when the submit is valid, so no transaction gets started unless the submit is actually valid.
Comment by Jörn Zaefferer — October 5, 2008 @ 11:38 pm
@Jörn: We want the entity loaded, updated, and saved all in the same transaction. onSubmit() is called after wicket has already applied user inputs to the model object, at that point you would have to session.merge() your changes instead of having them transparently persisted.
Comment by ivaynberg — October 6, 2008 @ 6:19 am
Okay, that makes sense. Thanks for clarifying.
Comment by Jörn Zaefferer — October 6, 2008 @ 3:33 pm
[...] Das NewsModel ist nicht von LoadableDetachableModel abgeleitet, da wir keinen schreibenden Zugriff auf das zu speichernde Objekt hätten, sondern dieses immer über load() geladen werden müsste. Dennoch ist die Model-Klasse recht einfach und übersichtlich. Es speichert die ID und die News selbst, beim Speichern in der Session wird die News aber nicht serialisiert. Werden die Daten wieder aus der Session geholt (z.B. durch ein Neu Laden der Seite), so wird die News anhand der ID erneut aus der Datenbank geladen. Eine allgemein gehaltenere Version eines solchen Models beschreibt Igor Vaynberg, einer der Wicket-Mitentwickler, im Blogeintrag “Building a smart EntityModel”. [...]
Pingback by Persistenz für den Feedreader - rattlab.net — October 28, 2008 @ 10:01 pm