cfNotate – CFC Persistence Based on Inline Code Annotations – Take that XML and shove it!
There seems to be a trend in the Java world to move from XML based configuration to the new Java annotation approach. Its a good move. It keeps everything in one place, and its clean and aesthetic. So although I can hardly say CFML is an aesthetic language (that’s a personal opinion, so lower your flame throwers), I thought it would be interesting to see how annotations and CFML can work together. It started as an innocent experiment to see if annotations and a certain level of CFML reflections can make object relational mapping a bit easier to maintain. It then evolved into a proof of concept, and from that to a small ORM mini framework that actually works quite well and fast.
Lets start from the very top: when we’re dealing with a lot of objects that essentially represent a database row each, it’s nice if we can abstract the logic to save and load that data and automate that whole process. In most cases its a good idea. This is not a new concept. There are a good deal of object persistence frameworks around from EJB to Hibernate in Java, SqlObject for Python, ActiveRecord for Ruby etc.
In Hibernate for example, the mapping of object properties to database fields was done via XML files until not very long ago. The latest version of Hibernate supports mapping those fields via Java Annotations. Special markers in the code denote “configuration” blocks that would have normally resided in XML files. I wanted to do the same thing in ColdFusion.
Since CFML has no such concept of annotations, I’ll piggy back the comments syntax and denote that /*{ }*/ is an annotated block. Due to that syntax decision, annotations can be used in only in CFSCRIPT tags, but that is not a bad thing. CFSCRIPT works better and faster to code in (for me anyway) and you should use it as much as possible (I know sometimes tags are required but usually it can all be done with script blocks). Besides CFSCRIPT looks a little bit prettier so its a nice try to redeem the tag-ugliness of ColdFusion syntax.
The idea is this: we can embed those annotation blocks directly in our code. They contain a set of key:value pairs in a javascript object format. These blocks are embedded exactly where they are meaningful for the code reader. If an annotation block describes the database mapping for a particular object property it will exist right next to that property. For example:
this.keys.name = "John"; /*{ PROPERTY:name; DATATYPE:char; MAPTO:contact_name; REQUIRED:true; }*/
This defines that the object property ‘name’ of a character type relates to the database column ‘contact_name’ in the table that object is bound to. That table is also defined via an annotated block, usually at the beginning of the file:
/*{ TABLE:contacts; PRIMARYKEY:id; }*/
Each of our data objects extend a new annotatedobject object. That object will obtain the file name for the data object, read the source and convert all the existing annotations into more meaningful data map. These are then cached for quick access later (into the application scope). That data map contains everything in our annotated blocks which is the property’s name, which database column it bounds to, if its a required value, what is the data type of this property and how it should be validated. We can then construct all the boring queries for select, update, create and delete automatically as well as validate the object’s properties to make sure they contain the right data types etc.
You can actually embed any type of attribute into the annotated blocks. These will get picked up and added to the object’s definitions (as this.cfnObjectMap).
The master object annotatedobject.cfc offers the following methods below for extending objects. Note that only the major functions are described here at this stage, and at this point all is in public access:
- init();
Called by extending objects to initializes the object from annotations or cached version. - load(int id); create(); update();
Respectively load an object for a given primary key, creates a new one or updates existing one. - save();
Will execute update() if the getId() method returns a numeric value or a create() if it does not. - getProperty(String key);
Returns a given property, or empty string if none exists. - setProperty(String key, Object value);
Sets a given property value. - getAttribute(String key, String attr);
Returns a given property attribute for a key. For example:
getAttribute(“NAME”, “REQUIRED”) will return true or false if the property NAME is a required field. - getTableName();
Returns the database table name this object is bound to. - getPrimaryKey(); and getDBPrimaryKey();
Returns the primary key property of the object and the name of the database primary key for the object’s table respectively. - getId();
Returns the object’s Id. - loadInput(Struct);
Takes a struct and loads its data as the object’s property values. Makes a copy of the old values replaced. - diffKeys();
Usually can be called after loadInput to return the difference between newly loaded data and what was previously there. - validate();
You can call validate any time to validate that each of the object’s properties matches its designated data type and required value state. Returns a struct of errors if any are found. - getSelectSql() getUpdateSql() getInsertSql() getDeleteSql()
Returns a constructed sql statement for this operation respectively. - executeSql(String sql);
Runs a query and returns it.
An example for a vanilla extending object can be something like:
< cfcomponent name="object" extends="annotatedobject" >
< cfscript >
/*{ CONTEXT:META; TABLE:contacts; PRIMARYKEY:id; }*/
this.properties.id = "";
/*{ PROPERTY:id; DATATYPE: numeric; MAPTO:contact_id; TITLE:Contact Id; VALIDATE:numeric; }*/
this.properties.name = "";
/*{ PROPERTY:name; DATATYPE: char; TITLE:Full Name; REQUIRED:true; }*/
this.properties.address = "";
/*{ PROPERTY:address; DATATYPE: char; TITLE:Complete Address; }*/
this.properties.notes = "";
/*{ PROPERTY:notes; DATATYPE: char; TITLE:Notes and Description; }*/
this.properties.created_at = "";
/*{ PROPERTY:created_at; DATATYPE:datetime; TITLE:Created at; VALIDATE:date; ONINSERT:NOW(); }*/
this.properties.last_modified = "";
/*{ PROPERTY:last_modified; DATATYPE: datetime; TITLE:Last modified; ONUPDATE:NOW(); }*/
init();
< /cfscript >
< /cfcomponent >
The above examples shows how setting a few properties to the object in the this.properties scope and adding the right annotations can generate a full on object with all the database persistence goodness built in.This project is at early stages and will progress slowly. It was done as a proof of concept more than anything else, but I found that it actually works quite well and is lean and maintainable.
You can download a zip file containing the annotatedobject.cfc discussed above and the annotate.cfc object which handles the actual parsing of the code files. Also in the zip is an example extending object and a sample .cfm file that shows how all this works. The demo demonstrates how the sql queries are retrieved but does not actually execute them against a database. I leave that to you.
Unzip the file in your server root and hit the cfnotate directory with your browser to see the demo.Future enhancements that I’ll be working on include automatic (simple) form generation from the object data, more advanced validation rules, new data types, support for databases other than mySql etc.
As well, any ideas, requests or complaints will be happily accepted.
Have fun with it (and fun in general), and as always, ANY comment is well appreciated.
April 7th, 2007 at 4:01 pm
Interesting post. I have taken a different approach- using custom attributes on cfcomponent,function,argument and property tags to accomplish similar annotation ends. This allows you to use the built-in reflection api (getMetaData). See my CFDJ article for more details: http://coldfusion.sys-con.com/read/311292.htm
May 17th, 2007 at 1:40 am
Hi,
I think this is a really good base for a good ORM.
* On contrary to Reactor/Transfer/ObjectBreeze, this starts at the object level and then down to the database.
* Leaves your business object model completely intact
* Have your mappings to the database or another place to persist close to your object while not putting storage dependent stuff in them
* It doesnt generate mappings automatically, when mapping objects to a relational database, sometimes there is more ways then 1. And maybe part of your properties you dont even want to store. This can all be dealt with.
Did you have a look at what Chip said? I had a look and that might also be an option, but i wonder if its a good idea to depend on the getmetadata function always returning those extra custom attributes, it might be tricky. Might also get problems when object/method/properties dont map one-to-one to the database.
If i have time i want to make an ORM this way, also want to make it more general so you can persist not only into a database, but for example into a file system (for example in case of photo files which you dont want to store as BLOB’s in a database)
What your missing in your proof of concept is managing of relations to other objects.. right? Did you by any chance already put some work in that?
greets,
klaas
May 17th, 2007 at 6:35 am
Hi Klaas,
Yes I did look at chips note and have toyed with that idea as well. I chose the annotation style because potentially this can include a lot of data and the attribute style can potentially lead to clutter – comments can be folded in most IDEs, while attributes cannot. Also attributes are too restrictive. Comments can be what I want them to be. Attributes must be name=value pairs in double quotes. As well, as you said – I’m not sure we can rely on getMetaData to ALWAYS return same data, even in 2-3 years time in the future.
My idea was to start with database mapping but progress it to gui representations and validation rules. I’ve done this before with XML files but inline code annotations keeps everything closer to home. I like your idea of adding other modes of persistence like the file system – sounds very useful.
I’ve just recently completed a small photo blog application using cfNotate. It was done for a friend’s project and will go online soon. Nothing fancy but it turned out quite well and made me tweak the framework a tiny bit. I did add something in regards of pulling data from other tables though I don’t think its as elegant as it can be. Perhaps we can come up with something that’s got style and sleekness to it ;o)
This is an initial draft/proof of concept, and I didn’t even have time to integrate my tweaks from the photoblog into it. I intend to post the entire source for it as a sample application once it goes up online.
My schedule in the next 2 weeks is going to be hectic, but I’ll try to find some time to post the source for the photoblog and integrate the changes made to the framework.
Hmm… this could be a start of an interesting project ;o).
Harel