Data Stores

DevExtreme supplies a number of classes for working with different kinds of data sources. These classes are called Stores, which allow you to easily perform various querying operations, such as filtering or sorting.

  • The ArrayStore works with plain JavaScript arrays.

  • The LocalStore works with the web browser's local storage.

  • The ODataStore works with OData services.

All stores implement common store functionality and expose the same set of data manipulation methods along with associated events.

JavaScript
// create a wrapper for an OData service
var db = new DevExpress.data.ODataContext({
    url: "/api.svc",
    errorHandler: function (error) {
        alert(error.message);
    },
    entities: {
        Messages: { key: "ID" },
        ToDoLists: { key: "ID" },
    }
});
// read data
db.Messages.load({ select: "title" }).done(function(queryResult) { /*process results*/ });
// create data
db.Messages.create({ Title: viewModel.title() });
// update data
db.Messages.update(new DevExpress.data.Guid(item.MessageId), { Title: title() });
// delete data
db.Messages.remove(new DevExpress.data.Guid(item.MessageId));
// create a list data source
var listDataSource = db.Messages.toDataSource({ select: "title" });

Set up a Store

Stores are designed to allow you to perform CRUD operations over various data sources in a uniform way. However, since the stores have to connect to data sources that are different in nature, they have different configuration options.

ArrayStore

If you have an array of primitive values, simply pass it to the ArrayStore constructor. If you have an array of objects, specify the objects' key property name via the key property, and then pass the array via the data property.

JavaScript
var ArrayStore = DevExpress.data.ArrayStore;
var arrayStoreA = new ArrayStore([1, 2, 3]);
var arrayStoreB = new ArrayStore({
    key: "id",
    data: [
        { id: 1, title: "object 1" }, 
        { id: 2, title: "object 2" }
    ]
});

ODataStore

Typically, you would not want to create an ODataStore directly. Instead, create an ODataContext, and specify the OData service URL via the url property. For each entity exposed by the service, add a correspondingly-named property to the entities configuration object. The property value must be an object whose key property specifies the name of the entity key. You can also specify a custom error handler via the errorHandler property, and optionally enable jsonp via the corresponding property.

ODataContext will create an ODataStore for each entity specified via the entities configuration object, and replace it with the created data store. So, for example, in the following code snippet, two entity stores will be created. They will be accessible via the db.entities.Messages and db.entities.ToDoLists properties.

JavaScript
var db = new DevExpress.data.ODataContext({
    url: "/api.svc",
    jsonp: true,
    errorHandler: function (error) {
        alert(error.message);
    },
    entities: {
        Messages: { key: "ID" },
        ToDoLists: { key: "ID" },
    }
});

LocalStore

For LocalStore, supply a unique name and the key property of the objects to store. Note that a LocalStore does not persist any individual changes immediately by default. Instead, the store periodically commits outstanding changes in batches. You can customize the time between such commits by specifying the flushInterval property. If you need to disable this behavior completely, set the immediate property to true to make a LocalStore persist changes immediately. You can optionally specify that store changes are to be persisted immediately or specify an auto-save interval.

JavaScript
var localStore = new DevExpress.data.LocalStore({
    key: "id",
    name: "myApplicationMessages",
    immediate: false,
    flushInterval: 7000
});

BasicOperations

Store exposes several methods to perform the basic functionality associated with persistent storage.

  • Insert
    Creates an object according to the data passed as the method parameter.

    JavaScript
    // create a new person 
    // with the name property set to 'Josh' and age property set to 29
    var newPerson = { name: "Josh", age: 29 };
    myPersonStore.insert(newPerson).done(function (createdPerson, personKey) {
        // the insert callback receives two arguments - a created object and its key
    });
  • Load
    Loads objects from the store. Note that this method supports advanced querying via the options parameter. Its usage is described separately in the Advanced Queries article.

    JavaScript
    myPersonStore.load().done(function (result) {
        // the load callback receives a single argument - an array of the retrieved objects
    });
  • Update
    Modifies the object with a specified key according to the data passed as the second parameter.

    JavaScript
    // change the name of the person whose key is 15 to 'James'
    myPersonStore.update(15, { name: "James" }).done(function (personKey, modifiedPerson) {
        // the update callback receives two arguments - an updated object's key and the object itself
    });
  • Remove
    Removes the object with a specified key.

    JavaScript
    // remove the person whose key is 67
    myPersonStore.remove(67).done(function (personKey) {
        // the remove callback receives a single argument - the key of a deleted object
    });

The following code snippet is a complete example demonstrating all basic store operations. First, an empty store is declared. Then, all basic operations are performed sequentially. An object is inserted into the store, then updated, loaded and finally removed.

JavaScript
var personStore = new DevExpress.data.ArrayStore({ key: "id", data: [] });
personStore.insert({ name: "Josh", age: 29 }).done(onInsert);
function onInsert(createdPerson, personKey) {
    alert("created an object with the '" + personKey + "' key");
    personStore.update(personKey, { name: "James" }).done(onUpdate);
};
function onUpdate(personKey, modifiedPerson) {
    alert("changed the object's name to '" + modifiedPerson.name + "'");
    personStore.load().done(onLoad);
};
function onLoad(result) {
    alert("loaded " + result.length + "object(s)");
    personStore.remove(result[0].id).done(onRemove);
};
function onRemove(personKey) {
    alert("removed an object with the '" + personKey + "' key");
};

OData Service Operations

An OData web service can expose service operations that execute server-side methods. To invoke a web service operation, use the ODataContext.get method. This method takes two parameters. The first parameter specifies the operation name. The second parameter specifies operation parameter values. The following code snippet demonstrates how to invoke a "MyMethod" web service operation that accepts two parameters.

JavaScript
var db = new DevExpress.data.ODataContext({
    // context configuration
});
db.get("MyMethod", { parameterA: 100, parameterB: "caption" }).done(
    function (result) {
        // process the result
    }
);

OData Service Operations

An OData web service can expose service operations that execute server-side methods. To invoke a web service operation, use the ODataContext.get method. This method takes two parameters. The first parameter specifies the operation name. The second parameter specifies operation parameter values. The following code snippet demonstrates how to invoke a "MyMethod" web service operation that accepts two parameters.

JavaScript
var db = new DevExpress.data.ODataContext({
    // context configuration
});
db.get("MyMethod", { parameterA: 100, parameterB: "caption" }).done(
    function (result) {
        // process the result
    }
);

Handle Events

Stores expose five pairs of events implemented via jQuery callback objects. Each pair signifies the beginning and completion of an operation.

  • Inserting/Inserted
    These events occur when an insert operation is performed. The inserting event occurs before performing an insertion and has a single argument specifying the object being inserted. The inserted event occurs after an insertion has been completed. It has two arguments, one specifies the object that was inserted, the other specifies the key assigned to the object.

    JavaScript
    myStore.inserting.add(function(values) {
        alert("creating an object");
    });
    myStore.inserted.add(function(callbackValues, callbackKey) {
        alert("successfully created a new object");
    });
  • Loading/Loaded
    These events occur when a load operation is performed. The loading event occurs before loading objects, and has a single argument specifying the options passed to the load method. The loaded event occurs after the objects have been loaded. It also has a single argument specifying the loaded objects.

    JavaScript
    myStore.loading.add(function(options) {
        alert("loading objects");
    });
    myStore.loaded.add(function(data) {
        alert("successfully loaded objects");
    });
  • Updating/Updated
    These events occur when an update operation is performed. The updating event occurs before updating an object. The event has two arguments specifying the key of the object to update and the values of modified properties. The updated event occurs after the objects has been updated. It has the same arguments as the updating event.

    JavaScript
    myStore.updating.add(function(key, values) {
        alert("modifying an object");
    });
    myStore.updated.add(function(callbackKey, callbackValues) {
        alert("successfully modified an object");
    });
  • Removing/Removed
    These events occur when a remove operation is performed. The removing event occurs before deleting an object. The event has a single argument specifying the key of the object to delete. The removed event occurs after the objects have been deleted. This even also has a single argument specifying the key of the deleted object.

    JavaScript
    myStore.removing.add(function(key) {
        alert("deleting an object");
    });
    myStore.removed.add(function(callbackKey) {
        alert("successfully deleted an object");
    });
  • Modifying/Modified
    These events do not have any arguments. Their only purpose is to indicate that a store has changed. The modifying event occurs before performing insert, update and remove operations. The modified event occurs after one of these operations has been completed.

    JavaScript
    myStore.modifying.add(function() {
        alert("store is being modified");
    });
    myStore.modified.add(function() {
        alert("store has been modified");
    });

Advanced Queries

The stores' load method supports advanced querying via the options parameter. The parameter takes a configuration object specifying query details. The object can have several properties specifying various querying options such as sorting, grouping or filtering. You can combine these options as required, for example to group and sort at the same time.

JavaScript
myStore.load({
    filter: //...,
    group: //...,
    select: //...,
    skip: //...,
    take: //...,
    sort: //...
}).done(function (queryResult) {
    //...
});

Filter

Filters query results based on the specified criteria. For server-side filtering, the filter property supports the performing of a single binary operation. The property accepts an array of three values. The first value specifies the left operand, the second value specifies a binary operator, and the third value specifies the right operand.

The following operators are supported.

  • "=" - Determines whether two values are equal.
  • "<>" - Determines whether two values are not equal.
  • ">" - Determines whether one value is greater than another value.
  • "<" - Determines whether one value is lesser than another value.
  • ">=" - Determines whether one value is greater than or equal to another value.
  • "<=" - Determines whether one value is lesser than or equal to another value.
  • "startswith" - Determines whether or not the beginning of one string matches another string.
  • "endswith" - Determines whether or not the end of one string matches another string.
  • "contains" - Determines whether or not one string occurs within another string.
  • "notcontains"- Determines whether one string does not occur within another string.
  • "and" - Determines whether two expressions are true.
  • "or" - Determines whether either of two expressions is true.

For client-side filtering, assign a function to the filter property. This function will receive an element from a query result and should return true for objects to include in the query result.

JavaScript
var myStore = new DevExpress.data.ArrayStore([{ id: 1 }, { id: 2 }, { id: 3 }]);
myStore.load({
    filter: ["id", ">", 1]
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":2},{"id":3}]'
});
myStore.load({
    filter: [["id", ">", 1], "and", ["id", "<", 3]]
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":2}]'
});
myStore.load({
    filter: function (element) {
        return element.id % 2 !== 0;
    }
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":1},{"id":3}]'
});

Group

Groups query results. For server-side grouping, the group property accepts an object with a string field property specifying the field to group by. Optionally, the object can also have a Boolean desc property specifying the group sort order. For client-side grouping, assign a function to the group property. This function will receive an element from a query result and is expected to return a value according to which the element will be grouped.

JavaScript
var myStore = new DevExpress.data.ArrayStore([
    { name: "Bob", year: 2008 },
    { name: "Alex", year: 2006 },
    { name: "Alice", year: 2011 },
    { name: "Ann", year: 2011 }
]);
myStore.load({
    group: { field: "year", desc: true }
}).done(function (queryResult) {        
    alert(JSON.stringify(queryResult));
    // outputs
    //[{
    //    "key": 2008,
    //    "items": [{
    //        "name": "Bob",
    //        "year": 2008
    //    }]
    //}, {
    //    "key": 2006,
    //    "items": [{
    //        "name": "Alex",
    //        "year": 2006
    //    }]
    //}, {
    //    "key": 2011,
    //    "items": [{
    //        "name": "Alice",
    //        "year": 2011
    //    }, {
    //        "name": "Ann",
    //        "year": 2011
    //    }]
    //}]
});
myStore.load({
    group: { selector: function (element) { return element.year - 2000; } }
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult));
    // outputs
    //[{
    //    "key": 8,
    //    "items": [{
    //        "name": "Bob",
    //        "year": 2008
    //    }]
    //}, {
    //    "key": 6,
    //    "items": [{
    //        "name": "Alex",
    //        "year": 2006
    //    }]
    //}, {
    //    "key": 11,
    //    "items": [{
    //        "name": "Alice",
    //        "year": 2011
    //    }, {
    //        "name": "Ann",
    //        "year": 2011
    //    }]
    //}]
});

To define several grouping levels, assign an array to the group property. Each array value should specify a grouping level.

JavaScript
myStore.load({
    group: [
        { selector: function (element) { return element.name.charAt(0) } },
        { field: "year", desc: true }
    ]
}).done(function (queryResult) {        
    alert(JSON.stringify(queryResult));
    // outputs
    //[{
    //    "key": "B",
    //    "items": [{
    //        "key": 2008,
    //        "items": [{
    //            "name": "Bob",
    //            "year": 2008
    //        }]
    //    }]
    //}, {
    //    "key": "A",
    //    "items": [{
    //        "key": 2006,
    //        "items": [{
    //            "name": "Alex",
    //            "year": 2006
    //        }]
    //    }, {
    //        "key": 2011,
    //        "items": [{
    //            "name": "Alice",
    //            "year": 2011
    //        }, {
    //            "name": "Ann",
    //            "year": 2011
    //        }]
    //    }]
    //}]
});

Select

Creates a projection of query results based on particular object properties. For server-side projection, the select property accepts an array of strings specifying the fields to include in the projection. For client-side projection, assign a function to the select property. This function will receive an element from a query result and is expected to return a corresponding projection.

JavaScript
var myStore = new DevExpress.data.ArrayStore([
    { id: 1, name: "John", department: "Sales" },
    { id: 2, name: "Sam", department: "Operations" }
]);
myStore.load({
    select: ["id", "department"]
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":1,"department":"Sales"},{"id":2,"department":"Operations"}]'
});
myStore.load({
    select: function (element) {
        var employeeCode = element.department + element.id;
        return {
            code: employeeCode,
            employee: element.name
        }
    }
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"code":"Sales1","employee":"John"},{"code":"Operations2","employee":"Sam"}]'
});

Skip Take

Skips the number of objects specified via the skip property in the query results and returns the next take objects.

JavaScript
var myStore = new DevExpress.data.ArrayStore([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]);
myStore.load({
    skip: 4
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":5}]'
});
myStore.load({
    take: 3
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":1},{"id":2},{"id":3}]'
});
myStore.load({
    skip: 3,
    take: 2
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '[{"id":4},{"id":5}]'
});

Sort

Sorts the query results. For server-side sorting, the sort property accepts an object with a string field property specifying the field to sort by. Optionally, the object can also have a Boolean desc property specifying the sort order. For client-side sorting, assign a function to the sort property. This function will receive an element from a query result and is expected to return a value according to which the element will be repositioned.

JavaScript
var myStore = new DevExpress.data.ArrayStore([{ name: "John" }, { name: "Sam" }, { name: "Paul" }]);
myStore.load({
    sort: { field: "name", desc: true }
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '["Sam","Paul","John"]'
});
myStore.load({
    sort: function (element) { return element.name; }
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult)); // outputs '["John","Paul","Sam"]'
});

To define several levels of sorting, assign an array to the sort property. Each array value should specify a sorting level.

JavaScript
var myStore = new DevExpress.data.ArrayStore([
    { name: "John", age: 20 },
    { name: "Sam", age: 35 },
    { name: "John", age: 43 },
    { name: "Paul", age: 27 },
    { name: "Sam", age: 52 }
]);
myStore.load({
    sort: [{ field: "name" }, { field: "age" }]
}).done(function (queryResult) {
    alert(JSON.stringify(queryResult));
    // outputs 
    //'[{"name":"John","age":20},
    //  {"name":"John","age":43},
    //  {"name":"Paul","age":27},
    //  {"name":"Sam","age":35},
    //  {"name":"Sam","age":52}]'
});

DataSource

A Store is a global object and can thus be referenced from any place in the code. Once you create a Store, you can use it for several DevExtreme widgets within your application. Set the dataSource property of the widgets' configuration object to the Store object. An individual DataSource object will be created for the widgets using the specified Store. When the Store is updated, the widgets will be updated automatically. When a widget is discarded, its DataSource is discarded as well, but the Store remains in the application.

Here is an example of an ArrayStore:

JavaScript
var arrayStore = new DevExpress.data.ArrayStore({
    data: [
        //...
    ]
});
listOptions = {
    dataSource: arrayStore
    //...
};
chartOptions = {
    dataSource: arrayStore
    //...
};

When assigning a Store to the dataSource configuration property directly, there is no way to apply query options such as "sort", "filter", "select" and "group" (see Advanced Queries) to the DataSource object created from the Store for the widget. To utilize query options, use the Store's toDataSource function. Specify the query options within the object passed as the function's parameter. The function will return the DataSource object created using the Store with the query options applied. Assign this DataSource object to the required widgets in your application using their dataSource configuration property.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore({
    data: [
        //...
    ]
});
listOptions = {
    dataSource: arrayStore.toDataSource({filter: ["year", ">", 2007]})
    //...
};
chartOptions = {
    dataSource: arrayStore.toDataSource({filter: ["year", ">", 2007]})
    //...
}

NOTE: The DataSource object created by the toDataSource function is not discarded along with the widget to which it is bound. Discard this DataSource manually when required.

You can specify an individual Store and query options for a widget. In this instance, a DataSource object will be created for the widget using the specified Store and query options. This DataSource object will be discarded along with the widget automatically.

JavaScript
listOptions = {
    dataSource: {
        store: new DevExpress.data.ArrayStore({
            data: [
                //...
            ]
        },
        filter: ["year", ">", 2007]
    }
    //...
}

Use the following options to configure the DataSource object.

  • sort, filter, select, and group query options (see Advanced Queries).

  • paginate
    A Boolean value specifying whether or not paging is enabled. For grouped data, the default value is false. Otherwise, the default value is true.

  • pageSize
    A number specifying the page size when paging is enabled. The default is 20.

  • updateMode
    Set this option to full to reload the DataSource when the underlying store changes. Set it to item to reload only the changed items.

  • map
    A function that transforms each item when retrieved from the store. The function should take a single parameter that will receive an item to transform and return the transformed item.

  • postProcess
    A function that transforms the data retrieved by the DataSource. The function should take a single parameter that receives data and returns the transformed resulting set.