Simple Base Persistent (ORM) Object for CF9 Now Available
Posted At : September 7, 2009 9:38 PM | Posted By : Bob Silverberg
Related Categories: ColdFusion, CF ORM Integration, BPO
I mentioned in an earlier blog post that I've been working on a Base Persistent Object (BPO) for ColdFusion 9 that can be extended by other components to provide them with certain behaviours. In order to add the kind of flexible behaviour that I desire in my BPO, I compose a number of other objects into it, which it then uses to accomplish things such as automatic validation and returning population and validation failures to whomever called its methods. I realized as I was preparing it for public consumption that it is a much larger task that I first envisioned to document and describe all of its intricacies and dependencies. I have decided, therefore to start by releasing a Simple Base Persistent Object, which can be used as a standalone object. It does not accomplish all that my full-featured BPO does, but it works as is, and will hopefully provide some ideas for what a BPO can do and how a BPO can be used.
For those who want to skip the explanation and just see the code, it can be downloaded from the Base Persistent Object RIAForge Project.
The Methods
The point of the BPO is that it defines methods which then become available to the persistent objects which extend it. The Simple BPO contains the following methods:
- populate()
- save()
- delete()
- configure()
The populate() method
The populate() method is easily the most useful method in the Simple BPO. Simply stated, it can be used to automatically populate an object's properties and many-to-one relationships from user-submitted data. It accepts two arguments:
- data, which is a structure containing data to be used to populate the object. For example, one might pass the contents of the form scope into this argument.
- propList, which is an optional array containing a list of properties to be populated. If this is passed in then only those properties listed in the array are populated.
When called it does the following:
- For each property of the object (or each property in the propList, if provided), it looks for a matching key in the data struct. When a matching key is found:
- If the value of that key is an empty string and the property supports nulls, it calls the corresponding setter method passing in a null. This can be useful if one needs to set a property such as a date or a number to null.
- Otherwise it calls the corresponding setter method passing in the value of the key.
- For each many-to-one relationship of the object:
- It attempts to find a matching key by looking for one of the following keys in the data struct:
- A key that matches the name of the relationship.
- A key that matches the fkcolumn of the relationship.
- If it finds a match it then attempts to load an object for the relationship using the value found in the key.
- If it obtains an object, it calls the corresponding setter method passing in the object.
- If not, and the relationship supports nulls, it calls the corresponding setter method passing in a null.
- It attempts to find a matching key by looking for one of the following keys in the data struct:
For example, let's say we have a User object like this:
2 extends="SimpleBasePersistentObject">
3 <cfproperty name="UserId" fieldtype="id" generator="native" />
4 <cfproperty name="UserName" />
5 <cfproperty name="UserPass" />
6 <cfproperty name="UserGroup" cfc="UserGroup" fieldtype="many-to-one"
7 fkcolumn="UserGroupId" />
8</cfcomponent>
We could then create and populate a User object from a struct like this:
2<cfset data = StructNew() />
3<cfset data.UserName = "Bob" />
4<cfset data.UserPass = "myPass" />
5<cfset data.UserGroup = 19 />
6<cfset User.populate(data) />
7<cfset EntitySave(User) />
Some things to note about the sample above:
- We wouldn't normally be populating the data struct manually as show above - it would usually already be available to us as the result of a form post.
- The value 19 above, which is being set into the UserGroup key of the data struct, corresponds to the id of the UserGroup object that we want to relate to this User.
- Rather than set the value 19 into the UserGroup key, we could set it into a UserGroupId key of the data struct. That will work as well because UserGroupId is the value of the fkcolumn attribute of the relationship.
Updating the example above to show how the populate() method might be called using the form scope, would look something like this:
2<cfset User.populate(form) />
3<cfset EntitySave(User) />
In addition to the ability to populate both properties and many-to-one relationships, the populate() method has one extra feature. I have found that it is common to want to cleanse input from a user via the HTMLEditFormat() function. For this reason, the populate() method supports automatic cleansing of data. This can be specified either for all properties of the component, or for individual properties. To turn it on for all properties, simply add a cleanseInput="true" attribute to your cfcomponent tag, like so:
2 extends="SimpleBasePersistentObject" cleanseInput="true">
3 <cfproperty name="UserId" fieldtype="id" generator="native" />
4 <cfproperty name="UserName" />
5 <cfproperty name="UserPass" />
6 <cfproperty name="UserGroup" cfc="UserGroup" fieldtype="many-to-one"
7 fkcolumn="UserGroupId" />
8</cfcomponent>
To turn it on for an individual property, add the cleanseInput="true" attribute to the cfproperty tag, like so:
2 extends="SimpleBasePersistentObject">
3 <cfproperty name="UserId" fieldtype="id" generator="native" />
4 <cfproperty name="UserName" />
5 <cfproperty name="UserPass" cleanseInput="true" />
6 <cfproperty name="UserGroup" cfc="UserGroup" fieldtype="many-to-one"
7 fkcolumn="UserGroupId" />
8</cfcomponent>
Note that you can also turn off cleansing for individual properties when it has been turned on for the whole component, like so:
2 extends="SimpleBasePersistentObject" cleanseInput="true">
3 <cfproperty name="UserId" fieldtype="id" generator="native" />
4 <cfproperty name="UserName" cleanseInput="false" />
5 <cfproperty name="UserPass" />
6 <cfproperty name="UserGroup" cfc="UserGroup" fieldtype="many-to-one"
7 fkcolumn="UserGroupId" />
8</cfcomponent>
The populate() method relies on certain conventions to work, namely:
- The name of the keys in the data struct must match the name attribute of the corresponding cfproperty tag.
- All properties are considered to allow nulls by default. To specify a property (or relationship) as not allowing nulls, a notnull="true" attribute must be specified. This is a standard attribute of the cfproperty tag.
- The name of the many-to-one relationship must be the same as the entityname attribute of the corresponding object.
The save() method
The save() method in the Simple BPO does nothing more than call EntitySave(), passing in the current object. This means that rather than writing code like so:
2<cfset User.populate(form) />
3<cfset EntitySave(User) />
We can write:
2<cfset User.populate(form) />
3<cfset User.save() />
This doesn't really add any functionality, but it does hide the implementation of the save() method, so one could theoretically change it in the future without having to change the calling routine. In my full BPO the save() method does more, namely validating the object and returning any failure messages to the calling routine.
The delete() method
Like the save() method, the delete() method in the Simple BPO does nothing more than call EntityDelete(), passing in the current object. This means that rather than writing code like so:
2<cfset EntityDelete(User) />
We can write:
2<cfset User.delete() />
As with the save() method, this doesn't add any functionality, but it does hide the implementation of the delete() method. Also, if one is going to use User.save(), for consistency I think one should use User.delete().
I originally included a call to ormFlush() after the EntitySave() and EntityDelete() calls, because I generally run with flushatrequestend="false" in my ormsettings in Application.cfc, because of how I do validations. I removed those calls because this is the Simple BPO. At this point I'm not sure what the overhead is for calling ormFlush() immediately after a save/delete, so maybe it makes sense to put them back in, as a convenience.
The configure() method
The configure() method in the BPO does nothing at all. It is meant to be overridden in the objects that extend it. It allows one to specify some code that will always be executed when an object is instantiated (any time the init() method is called). The reason I like using a configure() method is that it means I don't have to override the init() method in any of my persistent objects. I know that any code that is in the init() method of the BPO will always be executed, and I don't have to worry about the signature of the init() method nor have to remember to call super.init() if I want to change the initialization behaviour of an object.
Summary
This Simple Base Persistent Object is a work in progress. There are certainly other behaviours that could be added to it, most notably validation. It is a common practice to include validation code inside business objects, and you can see an excellent example of that in Paul Marcotte's Metro. I have no issues with Paul's approach, in fact I think what he's developed is quite remarkable, but I prefer to keep all of my validation rules external to the object itself, which is why I developed and use ValidateThis.
My full BPO makes use of ValidateThis, as well as a number of other objects, and I hope to be able to find the time to give it some TLC and release and describe it in the not-too-distant future.
Although I am releasing this via RIAForge, at this point the BPO really this just represents a bunch of ideas, many of which could be improved upon. I already have some ideas for improvement, which I've added to the issue tracker on the RIAForge project. I welcome any discussion, feedback, and suggestions about it. My hope is that we, as a community, can come up with an excellent BPO that the community as a whole can benefit from.
As a side note, I think it would be pretty sweet if they added an EntityPopulate(entity,collection) method to CF9, since I'm guessing most developers will end up implementing it on their own in some way.
Yes, populate() is pretty much a necessity - I cannot imagine anyone calling setters manually for every property of an object. I think perhaps because there are lots of different options for implementing a populate scheme, it is something that will always be left to the developer. Transfer certainly takes that approach. In my full BPO I am doing even more inside my populate method, including interacting with other objects, and that is something that I don't think CF will ever implement natively.
Look for some comments from me on _your_ blog ;-)
Element setStateName is undefined in a Java object of type class coldfusion.orm.PersistentTemplateProxy.
The error occurred in C:\domains\webroot\users\dsanderson\orm\com\SimpleBasePersistentObject.cfc: line 89
Here's the code:
<cfset state = EntityNew("tblState").init() />
<cfset data = StructNew() />
<cfset data.StateName = "FL" />
<cfset data.StateDescription = "Hot" />
<cfset state.populate(data) /> (** error **)
<cfset state.save() />
If I use the normal state.setStateName() it works fine. Thanks!
I notice with the populate function, looping over variables.Metadata.properties, you will not have included the propertied from inherited classes.
Playing around with it, looped from the starting object ("this") up the object chain via "extends" until I was at SimpleBasePersistentObject.
For each loop, I took each property array and added it to a combined-array - As long at the property :
A)Had not been found already
B)Matched a property returned by ORMSessionFactory.getClassMetaData(x).getPropertyNames())
I'm guessing similar has been played around with and I was wondering if there are any gotchas or bad side effects from doing this?
Thanks!!
Charlie
Your approach makes sense, and I imagine it would work. Would you care to share the code with me via a comment or email?
Heres the code I ended up with for proof-of-principle...
These functions were added based on John Whish's blog posting http://www.aliaspooryorik.com/blog/index.cfm/e/pos... :
<cfscript>
/**
* returns an array of properties that make up the identifier
* @output false
*/
public array function getIdentifierColumnNames()
{
return ORMGetSessionFactory().getClassMetadata( getClassName() ).getIdentifierColumnNames() ;
}
/**
* returns the name of the Entity
* @output false
*/
public string function getEntityName()
{
return ORMGetSessionFactory().getClassMetadata( getClassName() ).getEntityName();
}
/**
* returns an array of persisted properties
* @output false
*/
public array function getPersistedProperties()
{
return ORMGetSessionFactory().getAllClassMetadata()[ getClassName() ].getPropertyNames();
}
/**
* returns the subclass class name of this object
* @output false
*/
public string function getClassName()
{
return ListLast( GetMetaData( this ).fullname, "." );
}
</cfscript>
Then I added the following to the configure method:
<cfset variables.completePropertyArray = arrayNew(1)>
<cfset local.curObject = getmetadata(this)>
<cfset local.alreadyFound = arrayNew()>
<cfset local.c = 1>
<cfloop condition="20 gt #local.c#">
<!--- condition there to prevent an endless loop just in case. --->
<cfif listLast(local.curObject.FULLNAME,".") eq "SimpleBasePersistentObject">
<cfbreak>
</cfif>
<cfloop array="#local.curObject.PROPERTIES#" index="local.p">
<cfif arrayContains(getPersistedProperties(),local.p.name) and NOT arrayContains(local.alreadyFound,local.p.name)>
<cfset local.p.declaredBy = local.curObject.entityName>
<cfset arrayAppend(variables.completePropertyArray,local.p)>
<cfset arrayAppend(local.alreadyFound,local.p.name)>
</cfif>
</cfloop>
<cfset local.curObject = local.curObject.extends>
<cfset local.c = local.c+1>
</cfloop>
Last, I changed the loop in populate to array="#variables.completePropertyArray#"
Hope this is useful!
-Charlie
It's a bit different from what Charlie has done, so I just wrote about it in a separate blog post:
http://silverwareconsulting.com/index.cfm/2009...
Am in process of doing a social platform. this next version will be using orm with your bpo.
/**
* @output false
* @hint this code was refactored to work in cfscript originally created by Bob Silverberg http://silverwareconsulting.com/index.cfm/2009...
*/
component {
/**
* @output false
* @hint I build a new object
*/
public any function init(){
variables.Metadata = getMetadata(this);
cf_param('variables.Metadata.cleanseInput','false');
configure();
return this;
}
public any function cf_param(var_name, default_value) {
if(not isDefined(var_name))
SetVariable(var_name, default_value);
}
/**
* @output false
* @hint I do setup specific to an object
*/
private void function configure(){
}
/**
* @output false
* @hint Populates the object with values from the argument
*/
public any function populate(required any data, any propList=ArrayNew(1)){
local.mdp = variables.Metadata.properties;
for (local.i=1;i LTE ArrayLen(mdp);i=i+1) {
if (!arraylen(arguments.propList) || arraycontains(arguments.propList,mdp[i].name)){
if (!structkeyexists(mdp[i],"fieldType") || mdp[i].fieldType == "column") {
if (structkeyexists(arguments.data,mdp[i].name)) {
local.varValue = arguments.data[mdp[i].name];
if ((!structkeyexists(mdp[i],"notNull") || !mdp[i].notNull) && !len(varValue)){
_setPropertyNull(mdp[i].name);
}
else {
if(!structkeyexists(mdp[i],"cleanseInput")){
mdp[i].cleanseInput = false;
}
if(mdp[i].cleanseInput){
varValue = _cleanse(varValue);
}
_setProperty(mdp[i].name,varValue);
}
}
}
else if(mdp[i].fieldType == "many-to-one"){
if (structkeyexists(arguments.data,mdp[i].fkcolumn)){
local.fkValue = arguments.data[mdp[i].fkcolumn];
}
else if (structkeyexists(arguments.data,mdp[i].name)){
local.fkValue = arguments.data[mdp[i].name];
}
if (structkeyexists(local,"fkValue")){
local.varValue = EntityLoadByPK(mdp[i].name,fkValue);
if (isnull(varValue)){
if (!structkeyexists(mdp[i],"notNull") || !mdp[i].notNull){
_setPropertyNull(mdp[i].name);
}
else {
throw "Trying to load a null into the #mdp[i].name#, but it doesn't accept nulls.";
}
}
else {
_setProperty(mdp[i].name,varValue);
}
}
}
}
}
}
/**
* @output false
* @hint Persists the object to the database.
*/
public any function save(){
entitySave(this);
}
/**
* @output false
* @hint Deletes an object from the database.
*/
public void function delete(){
this.setStatusID(0);
save(this);
// entityDelete(this)
}
/**
* @output false
* @hint I set a dynamically named property
*/
private void function _setProperty(required any name, any value){
local.theMethod = this["set" & arguments.name];
if (isNull(arguments.value))
theMethod(javacast('NULL', ''));
else
theMethod(arguments.value);
}
/**
* @output false
* @hint I set a dynamically named property to null
*/
private void function _setPropertyNull(required any name){
_setProperty(arguments.name);
}
/**
* @output false
* @hint I cleanse input via HTMLEditFormat. My implementation can be changed to support other cleansing methods.
*/
private any function _cleanse(required any data){
return HTMLEditFormat(arguments.data);
}
}
Thanks for putting this out on RIAForge. This was exactly what I was looking for. I've been using FW/1 which has a native populate method but was giving me errors with empty strings on numeric fields. I needed a way to JavaCast it to Null but couldn't figure out the best way to handle it. This was perfect.
Great work!
Thanks,
Joe
Does the populate method also support one-to-one relationships?
Thanks.
Nolan
Just wanted to thank you for this awesome Base object that I've been using in my ORM application. I've made one change to your existing implementation that I thought I'd share and maybe get feedback on whether you think it's a good idea or not.
Basically, with the many-to-one properties you match on the name or fkcolumn value and then instantiate an entity using the matched value as the entity name, like so:
local.varValue = EntityLoadByPK(mdp[i].name,fkValue);
However, I often name the property/column something different than the exact entity name, especially if I have to relationships to the same entity from one entity. For example, having both sender and recipient properties in an entity that map to a person object. So instead I've changed the code to get the value of the last part of the CFC attribute for use when instantiating, like so:
local.varValue = EntityLoadByPK( listLast( mdp[i].cfc, '.' ),fkValue );
I could see this only being valid if you always have the CFC file name be left as the entity name, but for me, so far, that has always been the case.
- Shaun
That's a good solution to the problem in your environment, and a good example of what can be done with the BPO. It's not really meant to be taken and used "as-is". It's the basis for something that you can then customize to work exactly the way you want it to., just like you did.