Introducing The Crud Manager

We’re excited to introduce a new data level class called “Crud Manager” in our 2.5 and 3.x branches. This class is generic and will help you a lot when dealing with multiple dependent stores. We have two specialized implementations of the CM for the Scheduler and Gantt components, and in this post we’ll see how it makes life easier when implementing the Gantt chart.

Background

Dealing with Gantt related data has been a bit of a pain in the past so let’s take a look at some of the challenges the Crud Manager will help you solve. In previous versions of Ext Gantt 2.x, the implementer had to manage loading and saving of each individual store. This is bad for at least three reasons:

  • * Performance: Using multiple ajax proxies means multiple ajax request being used.
  • * Data consistency: It’s impossible to use an “all-or-nothing” approach with transactions if saving is split into multiple requests.
  • * Complexity: Some changes have dependencies on other data entities. Consider a new assignment, before it can be saved, its Task and Resource must first be saved to assert that they get proper Ids assigned. This type of dependency means that such multi-store save operations must be queued in a specific order. This introduces unnecessary complexity.

Additionally, this approach meant that the server never got to “see the full picture” of the update at once. Without seeing all the changes coming in, it’s much harder to do proper validation of the new data.

Loading Data

In previous versions of Ext Gantt, you could see something like this in the console when instantiating the Gantt chart and its data stores.

Screen Shot 2014-12-25 at 11.15.00

4 requests are made to load the initial data, and of course more if you use calendars or additional data stores. Not very optimized. When calling the load method the Crud Manager will perform a single ajax request to fetch data for all its stores.

var crudManager   = Ext.create('Gnt.data.CrudManager', {
    autoLoad        : true,
    taskStore       : taskStore,
    transport       : {
        load    : {
            url     : 'php/read.php'
        },
        sync    : {
            url     : 'php/save.php'
        }
    }
});

This is of course much more performant than doing one request per store. The server should then return a container object with a “success” property as well as separate JSON objects for each separate data set. Here is an example:

{
    "success"      : true,

    "dependencies" : {
        "rows" : [
            {"Id" : 1, "From" : 11, "To" : 17 },
            ...
        ]
    },

    "assignments" : {
        "rows" : [
            {
                "Id"         : 1,
                "TaskId"     : 11,
                "ResourceId" : 1,
                "Units"      : 100
            },
            ...
        ]
    },

    "resources" : {
        "rows" : [
            {"Id" : 1, "Name" : "Mats" },
            {"Id" : 2, "Name" : "Nickolay" },
            ...
        ]
    },

    "tasks" : {
        "rows" : [
            {
                "Id"                : 1,
                "Name"              : "Planning",
                "Duration"          : 10,
                "StartDate"         : "2010-04-23"
            },
            ...
        ]
    }
}

The CRUD manager will also perform a ‘smart’ loading of all these data sets to cause minimal refreshes of the views that observe it. For example, on the initial load – loading assignments, dependencies and resources before loading the taskStore assures that only one full refresh of the view will be performed. In previous versions of the Gantt chart, each loaded store would cause a full view refresh.

Saving Data

The Crud Manager is very similar to a regular Ext JS data store when it comes to saving. You can either use the autoSync config to always trigger a save upon any change to the data. Or you can handle this yourself by calling ‘sync’ when you want to save:

crudManager.sync(function() {
   // A simple callback
});

When calling save on the Gantt crud manager, this will trigger an ajax request passing the following data:

{
    requestId   : 123890,
    type        : 'sync',
    revision    : 123,

    tasks      : {
        added : [
            { $PhantomId : 'q1w2e3r4t5', Name : 'smth', ... },
            ...
        ],
        updated : [
            { Id : 123, PercentDone : 100 },
            ...
        ],
        removed : [
            { Id : 345 },
            ...
        ]
    },

    dependencies      : {
        added : [...],
        updated :  [...],
        removed :  [...]
    }
}

You can see 3 additional properties being sent along with the data updates. They are:

  • * requestId – A unique request identifier shipped with any request
  • * type – The request type (‘sync’ – for sync requests)
  • * revision – The server revision number

When the server receives this data, it can of course do any type of data operations based on what’s coming in. Some changes may not be allowed, other changes might have side effects. The server needs to respond with a data object containing all the delta changes it performs so that the client is up-to-date with the database. Below you can see a a sample sync response object:

    {
        requestId   : 123890,
        success     : true, // true to indicate a successful response
        revision    : 124,  // new server revision stamp
        tasks      : {
            rows : [
                // processed phantom record initially sent from client
                { $PhantomId : 'q1w2e3r4t5', Id : 9000 },
                // processed updated record initially sent from client
                { Id : 123, SomeField2 : '2013-08-01' },
                // record added/updated by server logic (not sent from client)
                { Id : 124, SomeField : 'server generated', SomeField2 : '2013-08-01' }
                ...
            ],
            removed : [
                // processed removed record initially sent from client
                { Id : 345 },
                // record removed by server logic (not sent from client)
                { Id : 145 },
                ...
            ]
        },

        dependencies      : {
            rows : [...],
            removed : [...]
        }
    }

For each store we have two sections rows and removed where rows holds all records added or updated by the server. As a bare minimum, for phantom records sent from the client, the server returns a combination of phantom Id and real Id (Id assigned by server). If the server decides to update some other record (either phantom or a persisted one) or create a new one, it should return an object holding a combination of the real Id and those field values. The field values will be applied to the corresponding store record on the client side. The removed array holds Ids of records removed by the server, whether initially sent from client or removed due to some server logic.

Managing Additional Stores

You can of course have the Crud Manager take care of loading and saving for your own custom stores too. For example, you might want to visualize a number of global dates in your schedule using the Lines or Zones plugin. Such data could easily be handled by the Crud Manager:


var zoneStore = new Sch.data.EventStore({
    storeId : 'zones'
});

var cm = new Sch.data.CrudManager({
    autoLoad      : true,
    eventStore    : eventStore,
    resourceStore : resourceStore,
    transport     : {
        load : {
            url : 'data.js'
        }
    },
    stores : [zoneStore]
});

Summing up

You can already see the CrudManager in action in some of our examples, such as the Advanced Gantt sample. We’re hoping this new data class will make your life easier and we look forward to hearing your feedback. For further information about the Crud Manager, please have a look in our Guides section.

Leave a Comment