back

Object relational mapping for the
Adobe AIR developer – Part 2

by David Tucker

In the previous installment of this series, we examined the basics of object relational mapping (ORM) and how it applies to Adobe AIR. You created an AIR application that uses a single simple data type, and you saved those values to the local embedded SQLite database using FlexORM. In this article, you will expand your application to include some complex relationships using the standard FlexORM metadata.

Data relationships

In the AIR application you created in the previous installment of this article, all your data was contained within a single ActionScript class: ContactVO. As I mentioned, this works fine for a demonstration, but in the real world, data is rarely that simple. For example, a contact doesn't simply contain a first and last name (as the previous version did). A contact might have multiple e-mail addresses, phone numbers, and street addresses. In addition, a single contact may belong to a single company that has many different employees.

While it might seem that the possibilities are endless, these relationships are generally divided into four different types:

Let's start by modifying the ContactVO from the previous exercise. In addition to a firstName and lastName, you need to add an ArrayCollection of addresses that will be mapped as a OneToMany relationship. In this case, one contact can have many addresses. The updated ContactVO now appears as:

package vo
{
   import mx.collections.ArrayCollection;

   [Bindable]
   [Table(name="CONTACTS")]
   public class ContactVO
   {
      
      [Id]
      public var id:int;
      [Column( name="first_name" )]
      public var firstName:String;
      [Column( name="last_name" )]
      public var lastName:String;
      
      [OneToMany( type="vo.AddressVO", lazy="false", cascade="all" )]
      public var addresses:ArrayCollection = new ArrayCollection();
      
      public function toString():String
      {
         return lastName + ", " + firstName;
      }
      
   }
}

This is the first time we have seen a complex relationship being defined with metadata. In this case, we use the OneToMany metadata tag with a few additional properties:

Now you need to create the actual AddressVO that will be attached to the ContactVO. In the same package, create a new ActionScript class named AddressVO:

package vo
{
   
   [Bindable]
   [Table(name="ADDRESSES")]
   public class AddressVO
   {
      
      [Id]
      public var id:int;
      [Column( name="street_address" )]
      public var streetAddress:String;
      [Column( name="city" )]
      public var city:String;
      [Column( name="state" )]
      public var state:String;
      [Column( name="postal_code" )]
      public var postalCode:String;
      
      [ManyToOne( cascade="none" )]
      public var type:TypeVO;
      
   }
}

In this class, the first five properties should look similar to you. These are all properties that will be reflected in the ADDRESSES table. However, the final property introduces another complex relationship, ManyToOne. In this case, each address will have a type. This enables the end user to define whether an address is for home or work. In this case, a specific TypeVO will contain this data. In this data model, many contacts can have one type. The only property that is needed on the metadata is the cascade value.

Finally, you can create the TypeVO, which will not have any complex relationships. You could chose to make the relationship between addresses and type bi-directional. If you did, you could have a collection of addresses on each type instance. Instead, let's just keep the association unidirectional. The TypeVO should appear as:

package vo
{
   
   [Bindable]
   [Table(name="TYPES")]
   public class TypeVO
   {
      
      [Id]
      public var id:int;
      [Column( name="name" )]
      public var name:String;
      
      public function TypeVO( name:String = "" )
      {
         this.name = name;
      }
      
   }
}

Cascading changes

One core concept among ORMs is cascading. Cascading determines what happens when an item attached to a data type is created, deleted, or modified in some way. While I briefly introduced the concept in the previous installment of this article (without actually naming it), let's see exactly how FlexORM handles cascading.

According to the documentation, FlexORM currently supports four different settings for cascade:

In our example, it makes sense to set cascading to All for the addresses. If we delete a contact, we also want the address record in the ADDRESSES table to be deleted. There would be no need to hold onto it. In addition, when we save a contact, we want to be sure that all changes to the address are saved as well. The Type property on the address is different, however. If we delete an address, we don't want to delete the type because other addresses might be using it. In addition, we don't really need to look for changes on the type value when we save a contact. Simply attaching an address to a contact should not be considered modifying it. Knowing which option to use for cascade is a matter of how well you know your data model and its requirements.

Lazy loading

Another configuration option that is common to most ORMs is lazy loading. This concept is fairly simple. Sometimes you don't want to retrieve all the values of a complex relationship until you actually need them. For example, if we added a company data object to our application, it may have a collection of 10,000 employees. We wouldn't want to retrieve all 10,000 employees if we just wanted to display a list of company names in a ComboBox. In a case like this, we could set Lazy to in the metadata. FlexORM would only retrieve the employees from the database when we actually access the Employees property.

While lazy loading is a powerful feature, I did run across a few minor bugs in FlexORM when working with lazy loading (specifically in ManyToOne relationships). Knowing how to best use lazy loading is a matter of knowing how your application will use the data and when it needs each piece of data.

Building the application

Now that the application's data types have been created, you just need to build out the views to support this functionality. The finished application can be found with the exercise files for this article.

Since we will be working with different types, we need to be sure to check that the type values have been populated when we connect to the database. In this case, we will create a method that gets called from the creation complete handler (after the database is connected):

protected function setupTypes():void
{
   types = entityManager.findAll( TypeVO );
   if( !types || types.length == 0 )
   {
      var type:TypeVO;
      
      type = new TypeVO( "Home" );
      entityManager.save( type );
      
      type = new TypeVO( "Office" );
      entityManager.save( type );
      
      types = entityManager.findAll( TypeVO );
   }
}

The first time the code runs, it will create a type for Home and Office and save them both in the database. The next time the code runs, it will simply retrieve all the types and use that to populate the types' ArrayCollection that was defined earlier in the application.

Since we want the user to be able to define multiple addresses for the contact, we need to create a way to display a form for each address. In addition, we also need to give the user the ability to add a new form for a new address. To accomplish this, we will use a repeater to display a single form for every element in the contact's Addresses property:

<mx:Repeater id="addressRepeater" dataProvider="{ selectedContact.addresses }">
   <view:AddressForm
      address="{ addressRepeater.currentItem as AddressVO }"
      types="{ types }" />
</mx:Repeater>

<mx:Button id="addAddress"
   label="Add Address"
   click="addNewAddress();" />

With this configuration, the only thing we need to do in order to add a new address is to add a new AddressVO to the selected contact's addresses collection:

protected function addNewAddress():void
{
   selectedContact.addresses.addItem( new AddressVO() );
}
Once these items are in place, we save the contact with all of its child entities:
protected function saveContact():void
{
   entityManager.save( selectedContact );
   getAllContacts();
}

You can get all the completed code in the application files. If you launch the application, you will see that you can add a first and last name and then click the Address Information tab to add as many addresses as you like. In this case, enter a single contact and give it two addresses: one for home and one for work. Click Save Contact. Once completed, your application should look similar to Figure 1.

The Contact Manager application.

Figure 1. The Contact Manager application.

Once we close the application, we can open the SQLite administration tool that was used in the first installment of this article. If we open up the database for this application, we can see all the values populated for contacts (the one that we saved), addresses (the two we created and attached to the contact), and types (which we created in the setupTypes method). The addresses are shown in Figure 2.

The SQLite administration tool showing inserted addresses.

Figure 2. The SQLite administration tool showing inserted addresses.

During development, it is always a good idea to verify that the data was inserted properly into the database with some type of SQLite administration tool. If data for some of your complex relationships is not being inserted properly, it could be a sign that you didn't place all the proper metadata elements on your properties.

Asynchronous database calls

In all the examples in this series, we have been using synchronous database calls. While that works fine in many cases, synchronous database calls have their pros and cons. In most cases, AIR is single-threaded (which means that it can only do a single task at a time). If the application is busy with a complex task, the user will not be able to interact with the application until the task is complete. AIR provides an asynchronous option for dealing with the embedded database, and FlexORM supports that as well.

Using the asynchronous mode is not as easy as setting a property. The way you interact with the database is completely different. Instead of being able to make calls inline, you have to make a call and then listen for specific events to know if it succeeded or failed. While this provides the best experience for the end user, it requires more code.

To learn more about using FlexORM asynchronously, consult the FlexORM documentation and source.

Moving forward

I hope that after seeing the power of an ORM layer, AIR developers will be excited about learning more. Here are two key resources that can help you continue to grow in your knowledge of object relational mapping:

‹ Back


David Tucker is a software engineer for Universal Mind, focusing on the next generation of RIAs with Adobe Flex and AIR. He is based in Savannah, Georgia, but you can find him online at DavidTucker.net.