Showing posts with label orm. Show all posts
Showing posts with label orm. Show all posts

Saturday, April 24, 2010

GORM vs ColdMVC ORM

One of the more interesting session's at cf.Objective() was Matt Woodward's presentation CFML on Grails. In the presentation, Matt showed how he was able to create a CFML plugin for Grails in order to integrate ColdFusion and Grails.

While the code worked, it seemed a little sketchy at times trying to get the two languages to play nice together cohesively. Even so, Matt achieved his goal of integrating the languages, so he gets props for that.

In his presentation, Matt said one of the biggest advantages of using Grails is being able to leverage Grails Object Relational Mapping (GORM), which is essentially a user-friendly abstraction layer on top of Hibernate.

While Matt took the approach of integrating ColdFusion into Grails in order to use GORM, I took the opposite approach by trying to recreate GORM in ColdFusion inside my ColdMVC framework. Not everything from GORM has been ported over yet, but here's a set of examples for comparison's sake. The top line in each pair is Grails, while the second line is ColdMVC.


def count = Book.count()
var count = _Book.count();

def count = Book.countByTitle("The Shining")
var count = _Book.countByTitle("The Shining");

def count = Book.countByTitleAndAuthor("The Sum of All Fears", "Tom Clancy")
var count = _Book.countByTitleAndAuthor("The Sum of All Fears", "Tom Clancy");

def book = Book.find("from Book as b where b.author=:author",[author:'Dan Brown'])
var book = _Book.find("from Book as b where b.author=:author",{author='Dan Brown'});

def book = Book.findAll("from Book as b where b.author=? order by b.releaseDate",['Dan Brown'],[max:10, offset:5])
var book = _Book.findAll("from Book as b where b.author=? order by b.releaseDate",['Dan Brown'],{max=10, offset=5});

def book = Book.findByTitle("The Shining")
var book = _Book.findByTitle("The Shining");

def books = Book.findAllByTitleLike("%Hobbit%")
var books = _Book.findAllByTitleLike("Hobbit");

def books = Book.findAllByTitle("The Shining", [max:10, sort:"title", order:"desc", offset:100])
var books = _Book.findAllByTitle("The Shining", {max=10, sort="title", order="desc", offset=100});

def book = Book.findWhere(title:"The Shining", author:"Stephen King")
var book = _Book.findWhere({title="The Shining", author="Stephen King"});

def books = Book.findAllWhere(author:"Stephen King")
var books = _Book.findAllWhere({author="Stephen King"});

def book = Book.get(1)
var book = _Book.get(1);

def books = Book.getAll(2,1,3)
var books = _Book.getAll(2,1,3);

def books = Book.getAll([1,2,3])
var books = _Book.getAll([1,2,3]);

def books = Book.list()
var books = _Book.list();

def books = Book.list(max:10, offset:100, sort:"title", order:"desc")
var books = _Book.list({max=10, offset=100, sort="title", order="desc"});


As you can see, aside from a couple small syntax differences, they're almost identical.

Wednesday, November 11, 2009

ORM Event Handling in ColdFusion 9

I've been playing around with ORM event handling in ColdFusion 9. For my application, I want to be able to fire events during various points in the lifecyle of my persistent entities. However, I don't want to have to clutter up my business logic with tasks such as logging, synchronizing integration data, or pushing changes out to Flex. I want to take a more AOP-style approach.

To do this, I created an XML-driven EventManager to use as my application's global event handler. With my EventManager, I'm able to configure listeners to respond to events fired by my entities using the built in ORM event handlers. The events can be registered using regular expressions and all follow the pattern {EntityName}:{EventHandler}. For example, I might have User:postUpdate or Product:postDelete.

In my sample application, I've configured my EventManager to send an email before and after any entity is loaded, inserted, updated, or deleted. In other words, basically anytime something happens to an entity. I'm not sure how useful this would be, but it's just an example.

Here's the directory structure:



Application.cfc

component {

this.name = "Sample";
this.datasource = "Sample";
this.mappings["/sample"] = getDirectoryFromPath(getCurrentTemplatePath());

this.ormEnabled = true;
this.ormSettings.dbcreate = "update";
this.ormSettings.eventHandling = true;
this.ormSettings.eventHandler = "sample.com.EventHandler";

public void function onApplicationStart() {

application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init();

application.beanFactory.loadBeans("/sample/config/coldspring.xml");

}

}


EventHandler.cfc

component implements="cfide.orm.IEventHandler" {

public void function preLoad(any entity) {
handleEvent(entity,"preLoad");
}

public void function postLoad(any entity) {
handleEvent(entity,"postLoad");
}

public void function preInsert(any entity) {
handleEvent(entity,"preInsert");
}

public void function postInsert(any entity) {
handleEvent(entity,"postInsert");
}

public void function preUpdate(any entity, struct oldData) {
handleEvent(entity,"preUpdate");
}

public void function postUpdate(any entity) {
handleEvent(entity,"postUpdate");
}

public void function preDelete(any entity) {
handleEvent(entity,"preDelete");
}

public void function postDelete(any entity) {
handleEvent(entity,"postDelete");
}

private void function handleEvent(any entity, string handler) {

var collection = {};
collection.entity = ormGetSession().getEntityName(entity);
collection.id = entity.getID();
collection.handler = handler;

var eventManager = application.beanFactory.getBean("eventManager");
eventManager.dispatchEvent("#collection.entity#:#collection.handler#",collection);

}

}


EventManager.cfc

component accessors="true" {

property configPath;

public any function init() {
variables.events = {};
variables.loaded = false;
}

public void function setConfigPath(required string configPath) {

if(fileExists(configPath)) {
variables.configPath = configPath;
}
else {
variables.configPath = expandPath(configPath);
}

}

public void function dispatchEvent(required string event, struct data) {

if(!structKeyExists(arguments,"data")) {
arguments.data = {};
}

if(!variables.loaded) {
loadConfig();
variables.loaded = true;
}

local.listeners = getListeners(event);

for (var i=1;i <= arrayLen(local.listeners);i++) {

local.bean = application.beanFactory.getBean(local.listeners[i].bean);

evaluate("local.bean.#local.listeners[i].method#(argumentCollection=data)");
}

}

private array function getListeners(required string event) {

if(!structKeyExists(variables.events,event)) {

local.used = {};

local.listeners = [];

for (var i=1;i <= arrayLen(variables.config);i++) {

if(reFindNoCase(variables.config[i].name,event)) {

for (var j=1;j <= arrayLen(variables.config[i].listeners);j++) {

local.listener = variables.config[i].listeners[j];

if(!structKeyExists(local.used,local.listener.id)) {

arrayAppend(local.listeners,local.listener);
local.used[local.listener.id] = true;

}

}

}

}

variables.events[event] = local.listeners;

}

return variables.events[event];

}

private void function loadConfig() {

variables.config = [];
local.xml = xmlParse(fileRead(getConfigPath()));

for (var i=1;i <= arrayLen(local.xml.events.xmlChildren);i++) {

local.event = {};
local.event.name = local.xml.events.xmlChildren[i].xmlAttributes.name;
local.event.listeners = [];

for (var j=1;j <= arrayLen(local.xml.events.xmlChildren[i].xmlChildren);j++) {

local.listener = {};
local.listener.bean = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.bean;
local.listener.method = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.method;
local.listener.id = local.listener.bean & "." & local.listener.method;

arrayAppend(local.event.listeners,local.listener);
}

arrayAppend(variables.config,local.event);

}

}

}


NotficationService.cfc

component {

public void function sendNotification() {

savecontent variable="local.body" {
writeDump(arguments)
}

var notification = new Mail();
notification.setTo("joe@example.com");
notification.setFrom("joe@example.com");
notification.setSubject("Notification");
notification.setType("html");
notification.send(body=local.body);

}

}


coldspring.xml

<beans>

<bean id="eventManager" class="sample.com.EventManager">
<property name="configPath">
<value>/sample/config/events.xml</value>
</property>
</bean>

<bean id="notificationService" class="sample.com.NotificationService" />

</beans>


events.xml

<events>
<event name="[\w]+:(pre|post)(Load|Insert|Update|Delete)">
<listener bean="notificationService" method="sendNotification" />
</event>
</events>


I'm not sure if this is the best approach or how well this would scale, but it seems to work pretty well for now.

PS - note the use of savecontent inside script. And they said it was pointless... :)

Monday, October 19, 2009

ColdFusion 9 Oddities

I ran into a couple interesting things when playing around with ColdFusion 9 today.

Oddity #1:
I was trying to create a really simple User entity using ORM.

User.cfc

<cfcomponent persistent="true">

<cfproperty name="id" fieldtype="id" generator="native" />
<cfproperty name="firstName" />
<cfproperty name="lastName" />
<cfproperty name="isDeleted" type="boolean" />
<cfproperty name="createdOn" type="date" />

</cfcomponent>


Here's what a dump of the User looks like:



I realized that I typically put isDeleted and createdOn on all of my entities, so I decided I would put them on a base class, Entity.cfc, that my User entity could extend. Here's the updated components:

User.cfc

<cfcomponent persistent="true" extends="Entity">

<cfproperty name="id" fieldtype="id" generator="native" />
<cfproperty name="firstName" />
<cfproperty name="lastName" />

</cfcomponent>


Entity.cfc

<cfcomponent>

<cfproperty name="isDeleted" type="boolean" />
<cfproperty name="createdOn" type="date" />

</cfcomponent>


However, when I went to look at my User, I noticed the extended properties were missing.



After a little while, I tried adding accessors="true" to my Entity base class. It seems slightly odd to need to add that since the parent object is persistent, but it gave me my getters and setters back.

Entity.cfc

<cfcomponent accessors="true">

<cfproperty name="isDeleted" type="boolean" />
<cfproperty name="createdOn" type="date" />

</cfcomponent>


And here's the result:



All good, right? Unfortunately not. I tried executing the following code:

index.cfm

<cfset user = EntityNew("User") />

<cfset user.setFirstName("Tony") />
<cfset user.setLastName("Nelson") />
<cfset user.setIsDeleted(0) />
<cfset user.setCreatedOn(now()) />

<cfset EntitySave(user) />


While this appeared to work in the interface. when I took a peak at the database, it didn't save my isDeleted or createdOn values. In fact, it didn't even create the columns in the database.

I decided to check out what Hibernate was doing by setting this.ormSettings.saveMapping = true in my Application.cfc. Here's the auto-generated User.hbmxml:

User.hbmxml

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class entity-name="User" lazy="true" name="cfc:cat.User" table="`User`">
<id name="id" type="int">
<column name="id"/>
<generator class="native"/>
</id>
<property name="firstName" type="string">
<column name="firstName"/>
</property>
<property name="lastName" type="string">
<column name="lastName"/>
</property>
</class>
</hibernate-mapping>


Apparently Hibernate wasn't able to pick up any properties defined in my base Entity.cfc. To solve this, I simply manually added the properties to the hibernate mapping file like such:

User.hbmxml

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class entity-name="User" lazy="true" name="cfc:cat.User" table="`User`">
<id name="id" type="int">
<column length="10" name="id"/>
<generator class="native"/>
</id>
<property name="firstName" type="string">
<column length="255" name="firstName"/>
</property>
<property name="lastName" type="string">
<column length="255" name="lastName"/>
</property>
<property name="isDeleted" type="boolean">
<column name="isDeleted"/>
</property>
<property name="createdOn" type="date">
<column name="createdOn"/>
</property>
</class>
</hibernate-mapping>


This seemed to do the trick and everything was happy once again. I guess I'll be using the mapping files more than I had originally planned.

Oddity #2:
It appears that using ColdFusion mixins inside CFCs no longer works in CF9. Take the following really basic example:

User.cfc

<cfcomponent>

<cfinclude template="mixin.cfm" />

</cfcomponent>


mixin.cfm

<cffunction name="sayHello">

<cfreturn "Hello, world" />

</cffunction>


I could then create a new User like so:

index.cfm

<cfset user = createObject("component","User") />

<cfdump var="#user#" />


If I were to run this code in CF8, I would get a User that looked like:



However in CF9, I get a completely empty User object:



Odd. Yeah it's not a very common use-case, but it's still something that can come in handy every now and then.

UPDATE: Apparently mixins still work, they just don't appear when you dump the component.