Conflict management

Conflicts can happen in an offline application when the client modifies data that is already modified on the server. To identify such a conflict, the session.Commit method passes the following data to the ColdFusion server sync method:

operations: An array of operations to perform INSERT, UPDATE, or DELETE.

clientobjects: An array of new data changes.

originalobjects: An array of data that was in the client database before the change. There is no conflict in the following circumstances:

  • If your are updating a record and the data on the server is same as the data in the originalobject. The client before the change had the same data as the server. In this case, the server updates its data source. If the old client data differs from that on the server, the application must handle the conflict.

  • If you are inserting a new record. In this case, there is no originalobject value and ColdFusion can insert the record in the data store.

You use the ColdFusion ObjectEquals function to identify conflicts. Pass the function the new instance of cfc from the client and the original instance to check if they are equal. If they are equal, the client has been working with the latest data. If it is not, the server can raise a conflict by returning the sever version of the instance present on the server from the sync method by creating an instance of CFIDE.AIR.conflict.cfc, setting its serverobject property (its only property) to the server value of the data, and returning the array of conflict objects to the AIR client.

The following code is an ideal example of sync method that uses ORM methods for syncing operations and also handles conflicts.

<cffunction name="sync" returntype="any"> 
<cfargument name="operations" type="array" required="true"> 
<cfargument name="clientobjects" type="array" required="true"> 
<cfargument name="originalobjects" type="array" required="false"> 
<cfset conclits = ArrayNew(1)> 
<cfset conflictcount = 1> 
 
<cfloop index="i" from="1" to="#ArrayLen( operations )#"> 
    <cfset operation = operations[i]> 
    <cfset clientobject = clientobjects[i]> 
    <cfset originalobject = originalobjects[i]> 
    <cfif operation eq "INSERT"> 
                <cfset obj = ORMGetSession().merge(clientobject)> 
                <cfset EntitySave(obj)> 
    <cfelseif listfindnocase("UPDATE,DELETE",operation) neq 0> 
        <cfset serverobject = EntityLoadByPK("employee",originalobject.getId())> 
        <cfif not isdefined('serverobject') > 
            <cflog text="CONFLICT::SERVER OBJECT NOT FOUND, RECORD MAY BE DELETED ALREADY"> 
                    <cfset conflict = CreateObject("component","CFIDE.AIR.conflict")> 
                    <cfset conflict.clientobject = clientobject> 
                    <cfset conflict.originalobject = originalobject> 
                    <cfset conflict.operation = operation> 
                    <cfset conflicts[conflictcount++] = conflict> 
                    <cfcontinue> 
            </cfif>     
            <cfset isNotConflict = ObjectEquals(originalobject, serverobject)> 
            <cfif isNotConflict> 
                    <cfif operation eq "UPDATE"> 
                          <cfset obj = ORMGetSession().merge(clientobject)> 
                          <cfset EntitySave(obj)>           
                    <cfelseif operation eq "DELETE"> 
                          <cfset obj = ORMGetSession().merge(originalobject)> 
                          <cfset EntityDelete(obj)> 
                    </cfif> 
            <cfelse><!----Conflict---> 
                    <cflog text = "is a conflict"> 
                    <cfset conflict = CreateObject("component","CFIDE.AIR.conflict")> 
                    <cfset conflict.serverobject = serverobject> 
                    <cfset conflict.clientobject = clientobject> 
                    <cfset conflict.originalobject = originalobject> 
                    <cfset conflict.operation = operation> 
                    <cfset conflicts[conflictcount++] = conflict> 
                    <cfcontinue> 
            </cfif> 
    </cfif> 
            </cfloop> 
<cfif conflictcount gt 1> 
<cfreturn conflicts> 
</cfif> 
</cffunction>

The CFC handling of the conflict depends on your application. In some cases, it can be appropriate to ignore the conflict and update the server data source with the new client data. In many cases, as in the preceding example, the CFC informs the client about the conflict by returning the server value of the data.

On the client side, you use code such as the following to register the method that handles the conflict that the server returns.

syncmanager.addEventListener(ConflictEvent.CONFLICT, conflictHandler); 
function conflictHandler(event:ConflictEvent):void 
{ 
var conflicts:ArrayCollection = event.result as ArrayCollection; 
var token:SessionToken = session.keepAllServerObjects(conflicts); 
token.addResponder(new mx.rpc.Responder(conflictSuccess, conflictFault)); 
} 

The conflictevent object contains an array of conflict objects that contain the clientinstance, originalinstance and the serverinstance. To accept the server's data, the application calls keepAllServerObjects, which takes an ArrayCollection that was passed to the conflict handler, or call the keepServerObject that takes an individual Conflict instance as shown in the following code. This conflict handler simply accepts any returned server object.

function conflictHandler(event:ConflictEvent):void 
{ 
var conflicts:ArrayCollection = event.result as ArrayCollection; 
var conflict:Conflict = conflicts.getItemAt(0); 
var token:SessionToken = session.keepServerObject(conflict); 
token.addResponder(new mx.rpc.Responder(conflictSuccess, conflictFault)); 
}

Conflicts can happen in the following cases:

  • When the client does an update after the server data was updated. In this case, the client was using an old instance of data and not the latest data on the server. The server can inform the client by creating an instance of conflict.cfc in the sync method and setting the server instance on it. On the client side, you can call the keepServerObject function in the conflict handler to resolve the conflict by updating the client database with the server instance.

  • When the client does an update but that record no longer exists on the server. Again, a conflict can be passed to the client from the server by creating an instance of Conflict.cfc and returning it. There is no need to set a serverobject property, as there is no server instance of the inserted data.

  • When the client did an insert, but for example, the server data uses an autoincrement primary key field. The server, therefore, does not use the primary key inserted by the client. To inform the client of the correct key field value, the server returns the conflict cfc instance with the server instance. The ActionScript Calling keepServerObject method can then update the local data with the new primary key value from the server.

Note: After a commit or conflict resolution, it is recommended to synchronize the client database with the server data source, because the server can have new data available from other clients.

ActionScript has a few reserved keywords. When you name the Class/SQLite table, ensure that you do not use any of the reserved keywords. For example, Order is an ActionScript reserved keyword. If you name a table or class as Order, the table creation fails. To avoid this name conflict, use the [Table(name="OrderTable")] metadata tag to override the default name. Your code for the Order.as class could look something like the following:

package test 
{ 
[Entity] 
[Table(name="OrderTable")] 
public class Order 
{ 
public function Order() 
{ 
} 
[Id] 
public var oid:uint; 
public var name:String; 
[ManyToMany(targetEntity="test::Product",cascadeType='ALL')] 
public var products:Array; 
} 
}