Wednesday, May 16, 2007

Creating a CRUD interface using widgets in tapestry

One thing that has now become possible is to create, update and delete items in your app model, all while never leaving the same page. A create can invoke a css-styled div, made to look like a dialog, which asks for a few field values, saves it and invokes an ajax response to update the master page. Same for edit, and for delete. The old way would have been to take you to a new page to do the create, or to pop up a full window and do the job with the aid of a bit of javascript.

I've been experimenting with the dojo dialog provided in tapestry 4.1.2 to do just this. It works very well. Now I want to add a little bit more intelligence to the form (in the dialog - let's call it the crud form). So for instance, if you choose an item from a selection (lets call it billableItem), other fields can react. In tapestry, the simplest way to do this is with the EventListener annotation. Rather than dreaming up some custom javascript to do it, you can do a little ajax request/response to update your form for you. Very cool.

There are a few challenges though. Since the crud form is itself asynchronous, and is told to update some components of the master page, we have a few problems when we try to get the information in our crud form to our app, so that the secondary fields can react. Obviously the billableItem selection needs to get to our app so that we can react, and this is typically done by submitting the form in question, while turning validation off and making sure it is asynchronous.

<component id="addBillableItemEventForm" type="Form">
<binding name="delegate" value="bean:delegate"/>
<binding name="clientValidationEnabled" value="true"/>
<binding name="success"
value="listener:addBillableItemEvent"/>
<binding name="async" value="ognl:true"/>
<binding name="updateComponents"
value="{'weekScheduler_billable_row'}"/>
</component>

<component id="billableItem" type="PropertySelection">
<binding name="displayName" value="literal:Billable Item"/>
<binding name="model"
value="ognl:billableItemSelectionModel"/>
<binding name="value" value="ognl:billableItem"/>
<binding name="validators" value="validators:required"/>
</component>

--

@EventListener(elements = "billableItem", events = "onchange",
submitForm = "addBillableItemEventForm",
async=true, validateForm = false)

The problem is that when our EventListener is invoked, telling that crud form to submit itself so that we can react, it goes and updates the master page and validates on the server side (the crud form is configured to validate on the client side). So then we have validation errors on required fields and such, which is not what we want. So we can't really have our EventListener submit this form, because that form needs to be configured the way it is.

@EventListener(elements = "billableItem", events = "onchange")

So what can we do to add our intelligence, short of developing some custom javascript as per usual?

Thursday, May 3, 2007

Hibernate and link tables

I'm struggling with hibernate and link tables. What I want is a fairly standard link table, to enable a unidirectional many-to-many relationship. While it's fairly trivial to set up in hibernate, there appear to be a few gotchas.

So here's what I have:

AccessLevel
Privilege
AccessLevel_Privilege

An AccessLevel has a collection of Privilege, which can be shared among AccessLevel (and other classes, ultimately). In other words, a Privilege also has a collection of AccessLevel. A typical many-to-many relationship. It doesn't need to be bi-directional, though if that's what it takes to make it work, then okay.


<class name="AccessLevel" table="AccessLevel">
<id name="id" column="id">
<generator class="native"/>
</id>
<set name="privileges" table="AccessLevel_Privilege">
<key column="accessLevelId"/>
<many-to-many class="Privilege" column="privilegeId"/>
</set>
</class>

<class name="Privilege" table="Privilege">
<id name="id" column="id">
<generator class="native"/>
</id>
<property name="privilegeType"/>
<property name="permissions"/>
</class>


This is the same as what they describe in the hibernate docs, section 7.3.4.

So now if we create (in code) an AccessLevel, and add 3 Privileges to that AccessLevel, and then save, all is good. We get some sql like this:


Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into AccessLevel (name, description, creationDate, modificationDate) values (?, ?, ?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)

Nice. Now if we delete that AccessLevel, regardless of any cascade specified, we get this:

Hibernate: delete from AccessLevel_Privilege where accessLevelId=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from AccessLevel where id=?

That's because the join table is made using foreign key constraints. I'm sure you can see the upcoming problem now.

So now make two AccessLevels, which share some of the same Privileges, persist them (all good up to here), and now delete one of them. This will fail with a foreign key constraint violation, because when you delete the AccessLevel, the corresponding relationships in AccessLevel_Privilege are deleted, and then the corresponding Privileges are deleted. The latter operation fails because those Privileges are foreign keys for other relationships!

Now this doesn't seem to me to be an unreasonable design, and is something I've used many times prior to using hibernate. You can partially fix the problem by just removing the foreign key constraints from the link table. Unfortunately you still get hibernate deleting all the privileges, even though you set cascade="none" on the privileges set in AccessLevel. So all we've done is allow the database to get out of sync (since now there are no Privileges for the other AccessLevel). It seems to me if you set cascade to none, it should only delete the AccessLevel. So continuing with this idea, I added an on-delete attribute to the set:


<set name="privileges" table="AccessLevel_Privilege" cascade="none">
<key column="accessLevelId" on-delete="noaction"/>
<many-to-many class="Privilege" column="privilegeId"/>
</set>


but alas, hibernate happily goes and deletes the privileges anyway, screwing up the database.

So I'm thinking this is not possible with hibernate. My next avenue of exploration is to use a mapped relationship with two bidirectional one-to-many associations.