Thursday, July 16, 2009

ColdFusion ORM : The basics

Before starting on the advanced topics, I thought it will be better to build some ground and hence I decided to do a post on the ORM basics. In this post, we will build a simple example to get a taste of ColdFusion ORM (CF-ORM) and during that we will also understand some of the basic concepts.

ORM is object relational mapping and in ColdFusion, objects are created using CFC. CFCs that needs to be persisted are called persistent CFC and that is marked by setting 'persistent' attribute to true on the component. We also need to define what persistent fields will be there in a persistent CFC and that is defined using 'cfproperty'. A field/property is marked persistent by setting persistent attribute to true on the cfproperty. By default, if the CFC is persistent, all its properties are considered as persistent unless you mark a property non-persistent. So typically 'persistent' attribute on the property is used only when you need to make that property non-persistent.

Each persistent CFC in ColdFusion application maps to a table in the database and each property in the persistent CFC maps to a column in the table (not exactly true but we will come to that later.. For the time being lets keep it that way). We will use the cfartgallery datasource for this example which has Artists and Art tables.

The first thing you need to do is - enable ORM for the application and define a datasource to be used (What is an ORM without a datasource?). ColdFusion ORM uses Application.cfc to define all the ORM specific settings. (If you haven't started using Application.cfc for your application, its time to start using it!)

Application.cfc


Component {
this.name="ArtGallery";
this.ormenabled="true";
this.datasource="cfartgallery";
}

Note that the datasource setting defined here makes it the default datasource of your application which can be used by tags like cfquery, cfinsert, cfupdate, cfdbinfo. The same default datasource will be used by ORM as well.

There are a whole bunch of ORM related configuration that you can do in application.cfc which you can refer here.

Now that the application is configured, let us build the object and define the persistence information on it. To start with, we will first define the Artist.cfc
Artists.cfc

/**
* @persistent
*/
Component {
property name="artistid" generator="increment";
property firstname;
property lastname;
property address;
property city;
property state;
property postalcode;
property email;
property phone;
property fax;
property thepassword;
}

This is the most simplistic definition of the component where we have defined only the component and its properties names. Since the table for this CFC already exists in the database, we have not added any table specific information in this and we will let ORM infer all the information from the database. The only additional setting that we have added here is the 'generator' attribute which is used to auto-generate the primary key.

After the components are defined, the first request to this application (i.e a page in this application) will make CF-ORM do all the setup necessary (basically generation of hibernate configuration, mapping files, building the session factory etc). Once the setup is done, you are all set to work with the entities.

We will first list all the artists and here is what you need to do for that

listAll.cfm
<cfscript>
artists = EntityLoad("Artists");
writedump(artists);
</cfscript>

To load a particular Artists with its ID, here is what you do
list.cfm


<cfscript>
artist = EntityLoadByPK("Artists", 1);
writedump(artist);
</cfscript>

There are several flavors of EntityLoad functions details of which can be read here

Let us now see how to perform insert and update on it.

save.cfm


<cfscript>
// Insert a new artist
artist = new Artists();
artist.setfirstname("Leonardo");
artist.setlastname("Da vinci");
artist.setcity("Paris");
EntitySave(artist);
writeOutput(artist.getartistid());// Update an artist
artist = EntityLoadByPK("Artists", 2);
artist.setcity("NewYork"); // artist is automatically updated.
</cfscript>

As we see in the above example, EntitySave is used to insert/update an object in the table. There are some important things to note here
  • EntitySave is an intelligent function which automatically finds if a new row needs to be inserted for the given object or whether an existing row needs to be updated.
  • We called EntitySave for the insert here but not for update but even then artist '2' gets updated. So how did that happen? Actually what happens here is when you load an artist object, it becomes associated with the hibernate session which keeps track of any changes in the object and automatically saves it when the session is flushed. We will talk about more about hibernate session in a later post. For the time being lets just say that Hibernate Session is a short-lived object that represents a conversation between the application and the persistence layer and also acts as the first level of cache.
  • We did not write any setter or getter method for artist's properties in Artists.cfc but we are calling them here. That works because ColdFusion 9 automatically generates accessor methods for any property written in a CFC. More details on generated methods in a later post.
  • At line no 6, we called entitySave, but if you check the database, the row is not inserted yet. So when does that happen? Hibernate batches up all the operations till the end of the request or to be exact till hibernate session is flushed. ColdFusion ORM starts up a session when the first ORM method is called in the request and is automatically flushed when the request ends. The batching is done for performance reason so that hibernate executes the sql with the final state of the objects. It will be a huge performance bottleneck if ORM keeps executing sql for each changes in the object.

To delete an Artist, you need to call EntityDelete() passing the object to be deleted.

delete.cfm


artist = EntityLoadByPK("Artists", 15, true);
EntityDelete(artist);

Relationship


So far we have seen how to perform CRUD for a single entity. But in any application, there will be entities which are associated and ORM must load the associated object as well when loading a particular entity. For our example, an Art will have an Artist and when loading the art object, it should also load the associated artist. So lets build the model first after which we will see how to work with the association.

In cfartgallery, the table Artists has a one-to-many relationship with Art table, which are joined using the foreign key column ARTISTSID. This means that each artist has created multiple arts and each art is created by one artist. To represent this in the object model, each ARTIST object would contain an array of ART objects. Each ART object will also contain a reference to its ARTIST object thereby forming a bidirectional relation.

To achieve this, we will need to add an extra property 'arts' to 'Artists' that contains an array of ART objects for that Artist. The modified Artists.cfc would look like
/**
* @persistent
*/
Component {
property name="artistid" generator="increment";
property firstname;
property lastname;
property address;
property city;
property state;
property postalcode;
property email;
property phone;
property fax;
property thepassword;
property name="arts" fieldtype="one-to-many" fkcolumn="artistid" cfc="Art" cascade="all" inverse="true";
}

Here is the Art.cfc

/**
* @persistent
*/
Component {
property name="artid" generator="increment";
property artname;
property price;
property largeimage;
property mediaid;
property issold;
property name="artist" fieldtype="many-to-one" fkcolumn="artistid" cfc="Artists" ;
}

Notice the artist property above which is of many-to-one type. Also notice that both the property use the same value for fkcolumn attribute i.e 'artistid' of Art table that references artistID pk of Artist table.


Since we have added a new persistent CFC (Art.cfc) after the application was loaded, we need to re-initialize the ORM for this application so that mappings for Art.cfc also gets generated. This can be done by calling ORMReload() method. There are some nice ways to do this but for the time being lets keep it simple by putting this in a separate page which we will call to reload ORM.

initializeORM.cfm
<cfset ormReload()>

If you load and dump Artist (using listAll.cfm), you should also see the associated art objects for artists.

Now let us create a new Art and associate it with an existing Artist.
artCreate.cfm

<cfscript>
artist = EntityLoad("artists", 1 ,"true");
art = new Art();
art.setartname("landscape");
art.setPrice(1500);
art.setissold(false);
art.setartist(artist);
artist.addArts(art);
EntitySave(art);
</cfscript>

If you notice line 7-8 above, we associate artist to art by calling art.setArtist(artist) as well as art to artist by calling artist.addArts(art). Hibernate needs us to do this in order to set up the bidirectional relation properly. Since it is a bidirectional relation, you must also decide which side will set the relation in the database. i.e which side of the relation will set the fkcolumn in the table. This is controlled by the "inverse" attribute of proeprty, which if set to true, tells hibernate that this is a inverse of the other relation and this side of relation should be ignored for persistance. If you don't set inverse to true, Hibernate will unnecessarily fire two sqls for the same association.

So there you have it. We have seen how you can use ORM to perform the basic CRUD operations on entities. For more details, you can refer to the ORM doc and Hibernate docs.

3 comments:

Unknown said...

Hi,

When saving an entity that is associated with another entity, I'm calling EntitySave(), twice. I first call EntitySave(obj1) and then EntitySave(obj2). On the second EntitySave, I'm associating the two entities by getting the ID of the first entity and calling a setter for the Primary Key.

For example:
goat = new Goat();
EntitySave(goat );
farm = new Farm(); // this could be an existing farm
farm.setLiveStockID(goat.getGoatID())
EntitySave(farm );


Is there a cleaner solution in CF ORM using this 1 to many and many to 1 relationship?

Rupesh Kumar said...

Hi David,
Two things
1. In case of relationship, you do not set the foreign keys. You just associate the related objects. So you should not have livestockId property on farm. Instead you should have livestock.
2. There is a cascade attribute on relationship property which allows you to cascade the operation on the parent object.

So your example would look like
goat = new goat();
farm = new farm();
farm.addLiveStock(goat);
goat.setFarm(farm);
EntitySave(farm);

p.s : I have moved my blog to http://www.rupeshk.org/blog. So you might want to follow me there :-)

Unknown said...

This helped me considerably. Thank you very, very much Rupesh.

David