Sunday, January 3, 2010

FlexORM

What seems like many years ago now, because it was - I saw a presentation by Christophe Coenraets where he talked about a simple ORM for AIR. You can see the original blog post here.

I have used Hibernate for many years, and now most recently GORM and I am a big fan of ORM tools when used right and in the right measure.

When I started looking at an AIR application I decided to revisit the work done in that original blog posting and I was very pleased to see that work had continued on with the ORM mapping, now called FlexORM. (Which is an interesting name since it only works with AIR ). You can find more information about the FlexORM tool here.

What I am going to do is present some examples of how I used it. I am hoping that my examples will help answer your questions on how to use the tool or inspire you start using it.

In my simple application my domain model is one where a Project can have sub-projects to form a project hierarchy and each project can have many TimeEntry references but a TimeEntry is only allocated to a single Project.

Lets look at the Project domain class first.
package com.redpointtech.domain
{
import mx.collections.ArrayCollection;
import mx.collections.IList;

[Bindable]
[Table( name="PROJECT")]
public class Project
{
// each project can contain a number of child or
// sub projects.
private var _subProjects:IList = new ArrayCollection();
private var _timeEntries:IList = new ArrayCollection();

[Id]
public var id:int;

[Column( name="proj_name")]
public var name:String;

[Column( name="proj_desc")]
public var desc:String;

[Column(name="color")]
public var color:Number;

[ManyToOne(name="parent_id", inverse="true")]
public var parent:Project;

[OneToMany(type="com.redpointtech.domain.Project", fkColumn="parent_id", lazy="false", cascade="save-update", indexed="true")]
public function set subProjects(value:IList):void {
_subProjects = value;
}
public function get subProjects():IList {
return _subProjects;
}
public function addSubProject(value:Project):void {
value.parent = this;
_subProjects.addItem(value);
}

[OneToMany(type="com.redpointtech.domain.TimeEntry", fkColumn="project_id", lazy="true", cascade="all", indexed="true")]
public function set timeEntries(value:IList):void {
_timeEntries = value;
}
public function get timeEntries():IList {
return _timeEntries;
}
public function addTimeEntry(value:TimeEntry):void {
value.project = this;
_timeEntries.addItem(value);
}


public function Project()
{
}

}
}


The primary metadata tags I used user:
[Table( name="tbd")] to define the table name for the domain object.

[Id] to tell the ORM which field is used for an id. As of right now the id value must be of type 'int'.

[Column( name="tbd")] to define the column name for a property. If the name field is not specified then it uses the property name.

[ManyToOne(name="col name", inverse="true/false")] to define the ManyToOne relationship with itself.

[OneToMany(...)] metadata tag is specific on the set/get methods. Also noticed that we added an 'add' method which sets the parent of the Project and adds the Project to the collection of sub projects.

The TimeEntry class looks like the following:
package com.redpointtech.domain
{
import com.redpointtech.util.Constants;

[Bindable]
[Table( name="TIMEENTRY")]
public class TimeEntry
{

[Id]
public var id:int;

[Column( name="full_year")]
public var fullYear:int;

[Column( name="month")]
public var month:int;

[Column( name="day_of_month")]
public var dayOfMonth:int; // Date.date equivalent

[Column( name="start_hour")]
public var startHour:int;

[Column( name="start_min")]
public var startMin:int;

[Column( name="end_hour")]
public var endHour:int;

[Column( name="end_min")]
public var endMin:int;

[Column( name="notes")]
public var notes:String="";

[Column( name="summary")]
public var summary:String="";

[ManyToOne(name="project_id", inverse="true")]
public var project:Project;

public function TimeEntry()
{
}

public function setDate(date:Date):void {
fullYear = date.fullYear;
month = date.month;
dayOfMonth = date.date;

}
public function setStartTime( hour:int, min:int):void {
startHour = hour;
startMin = min;
}
public function getStartDate():Date {
var startDate:Date = new Date(fullYear, month,dayOfMonth,startHour,startMin);
return startDate;
}

public function setEndTime(hour:int, min:int):void {
endHour = hour;
endMin = min;
}
public function getEndDate():Date {
var endDate:Date = new Date(fullYear,month,dayOfMonth,endHour,endMin);
return endDate;

}
private function _getTimeDiffInMillis():Number {
var startDate:Date = getStartDate();

var endDate:Date = getEndDate();

var diff:Number = endDate.getTime() - startDate.getTime();

return diff;
}

[Transient]
public function get elapsedHours():Number {
var diff:Number = _getTimeDiffInMillis();

var hours:Number = diff / Constants.millisecondsPerHour;

var floorHours:Number = Math.floor(hours);


return floorHours;
}

[Transient]
public function get elapsedMinutes():Number {
var hours:Number = elapsedHours;
var milliHours:Number = hours * Constants.millisecondsPerHour;

var diff:Number = _getTimeDiffInMillis();

var minuteDiff:Number = diff - ( milliHours );

var minutes:Number = minuteDiff / Constants.millisecondsPerMinute;
var roundedMinutes:Number = Math.min(Math.round(minutes),59);
return roundedMinutes;

}

}
}


The only real difference is the use of Transient to tell FlexORM that these are not used in the persistence of the object.

Now that we have a feel for how to annotate our domain classes to be used by FlexORM, lets see how to persist them to the database.

I am using FlashBuilder Beta2 with FlexUnit4 RC1. I will go over the integration of FlashBuilder and FlexUnit in another blog posting. For this one I wanted to remain focused on using FlexORM.

Below is a unit test function to test Project CRUD ( create read update delete )

    [Test]
public function testCRUDNoHier():void {
trace("----------------testCRUDNoHier----------------");
var em:EntityManager = EntityManager.instance;

var p:Project = new Project();
p.name="P1";
p.desc = "proj from unit test";
p.color=0xFF001E;
em.save(p);

Assert.assertEquals("P1",p.name);

var p2:Project = new Project();
p2.name="P2";
p2.desc = "proj2 from unit test";
p2.color=0xAAAAAA;
em.save(p2);

Assert.assertEquals("P2",p2.name);
Assert.assertEquals("P1",p.name);

var p1:Project = em.loadItem(Project, p.id) as Project;
Assert.assertEquals("P1",p1.name);

var ps:ArrayCollection = em.findAll(Project);
Assert.assertEquals(2,ps.length);

p1.desc = "New P1 desc";
em.save(p1);

var p3:Project = em.loadItem(Project,p1.id) as Project;
Assert.assertEquals("New P1 desc", p3.desc);

em.remove(p2);
ps = em.findAll(Project);
Assert.assertEquals(1,ps.length);


}



We start the test method by getting a reference to the EntityManager via EntityManager.instance. The first thing we do in the test case, is to create a Project and the save it to the DB via the em.save(p).

That is all you have to do to save an entity, you do not need to write any sql yet.

To load an item for which you have the id, you use the loadItem method passing in the Project class and the id value.

To find all records for a particular class, you use the findAll method.

FlexORM even has a Criteria API. While it is not on the same level as the Hibernate Criteria, it is still very useful.

Below is an example method to find TimeEntries on a particular day:

    public static function findAllOn(fullYear:Number,month:Number=-1,dayOfMonth:Number=-1):ArrayCollection {
var teCriteria:Criteria = entityManager.createCriteria(TimeEntry);
teCriteria.addEqualsCondition("fullYear", fullYear);
if( month > -1 ) teCriteria.addEqualsCondition("month",month);
if( dayOfMonth > -1 ) teCriteria.addEqualsCondition("dayOfMonth",dayOfMonth);
teCriteria.addSort("startHour");

return entityManager.fetchCriteria(teCriteria);
}



You use the EntityManager to create the criteria for an annotated class. In this example I am using simple conditions to see that the TimeEntry has values that equal those passed in, and then sort the returned values based on the starting hour.

Again - so far I have created no sql myself. I am sure I will have a case where I need to hand craft some sql, and this will be fairly easy even with the EntityManager.

The idea behind the ORM tools, IMHO, is not to take over all of the persistence work but instead take over the mundane and tedious persistence work so we can concentrate on the more difficult tasks. So far FlexORM has done that.

I hope this quick example was useful in getting you interested in looking at FlexORM.

Next time I will discuss how I used FlashBuilder Beta2 and FlexUnit to test the application.

4 comments:

Unknown said...

Thanks very much for this post!
I was hard pressed finding a good guide to FlexORM anywhere.
Which is quite sad, really, since it is such a brilliant library.

Rob McKeown said...

Have you run into issues where saving new projects (or updates to existing projects) result in incorrect values being set in the database for the lft and rgt columns? I can't seem to get the Nested Set stuff to work consistently.

I ended up using my own mechanism in Klok for this same problem, but it would be great if this worked. Perhaps I am doing something wrong when calling save() but the documentation is rather light.

Patrick Ryan said...

Hi Rob
I have not run into issues for the use case that I have. I am still working on this particular project so it is possible as the object gets more complex the hierarchy saving will be an issue. If it is I will post another blog on this.

Thanks for reading and posting a comment.

danielo said...

Thanks very much Patrick, You saved me serious time mate.

Daniel Nachit