Create or Update from Web Page - Mini Recipe
3 PM
April 19, 2004
Here is a mini-recipe for handling HTML forms that map to domain objects. This has cropped up in my work a few times lately, so I thought I’d write it down.
Scenario: An application has a web page for editting a domain object. The processing for the page can be called to do any one of:
- display an existing domain object,
- submit data for a new domain object,
- submit form data to update an existing domain object.
Steps:
- Examine the incoming request for an ID argument. If you find one, retrieve the domain object with that ID from the database. If you the request does not contain an ID argument, create a new domain object in memory.
- For each attribute of the domain object, examine the request for a corresponding argument. If the argument is present, validate it. If the argument is present and valid, update the object with the value of the argument.
- If there were no errors, and the request was a ‘save’ request, then INSERT or UPDATE the domain object to the database.
- Generate an HTML form to display the object’s data. If there was an ID argument in the request, include it in the generated form.
Commentary:
- Don’t be tempted to skip retrieving the domain object in the case of a save. This step ensures that any attributes of the domain object that are not also on the form are preserved. Bugs caused by disappearing attributes are often subtle and difficult to track down.
- The same request handler is used to display the initial form and to save the domain object. It is appropriate to use the HTTP method to distinguish between these two cases: use POST to save an object, and GET to save it.
- It is an error to have a save request without the attribute data. In a public web application, check for this condition.
- According to this recipe, the form is redisplayed to this user after the object has been successfully saved. In this case, ensure that the user receives clear feedback that their changes have been applied. Alternatively, you may choose to redirect the user’s browser or display a different page after the object has been saved.
- Handling invalid data can be tricky. Typically, if invalid data was entered, then the data the user entered (rather than data from the object) should be redisplayed to the user, along with an error message. Fortunately, most web frameworks provide some assistance with this process.
Comments
how do you handle overwriting data? I presume you have some sort of optimistic locking strategy (not the "Oh I'm optimistic that won't be much a of a problem" locking strategy ;-)
that is, what if I retrieve a domain object with fieldA:X and fieldB:Y - then someone else updates fieldA:Z before I submit a change to fieldB:W; how do I either (1) refuse the update with a lock conflict (row level lock) or (2) accept the change to fieldB while preserving the other user's changes to fieldA (column level lock).
*cough* Read uncommitted? */cough*
The "Oh, I'm optimistic that won't be much of a problem" strategy works surprisingly often. Either people are modifying their own data and thus are unlikely to clobber themselves, or the consequences of overlapping changes are negligible anyway.
Column-level locking is probably a bad thing, because you don't know if the change to fieldA invalidates the change to fieldB. It's also pretty hard to accomplish in ORM without some rather deep voodoo. Object-level optimistic locking could be easy to implement in the scheme above by making a magic 'version' field that the framework is aware of.
Incidentally, the above is very similar to the "Rails" Ruby web framework. I got into an argument with said framework's author because I don't like the direct mapping between the web parameters and the domain object's attributes. It seems a security problem waiting to happen if you have to remember to specifically deny the web tier access to certain domain object properties. (In very early pre-releases of Confluence, a related programming error meant that anyone could get superuser access to an installation by putting something like "?superuser=true" on the end of a URL.)
My approach (well, for the project I'm working on right now) has a some complication around validation, and I took a look at it to figure out why. Basically, I have a requirement that the database may contain INVALID data, and if so, then we leave it alone. So if the data I receive in a form field is invalid, I have to check to see whether it was ALSO the same as what's already in the DB, in which case we leave it alone rather than reporting an error to the user.
-- Michael Chermside
Charles,
It sounds like "Rails" uses reflection to do Step 2 automagically. This recipe can also be usd done 'manually'; e.g moving attributes one-by-one out of a Struts View bean into a DTO.
cough, cough, hack, hack, [keels over] who the hell uses ReadUncommited???
version numbers are all very well, but they do pollute your physical schema with the secondary concern of update tracking (although an autogenerated hash will usually suffice too). Column level locking can be implemented with provision for your validation framework to validate the final result, and certain concurrent changes can be configured to automatically throw row-level type lock conflict exceptions anyhow.
Whilst I reckon the "hugely optimistic" solution does indeed work 99% of the time, the other 1% is also rarely reported as it is either (a) not noticed until later if at all or (b) an unproducable system glitch that leaves the non-tech users scratching their heads.