Data Store

The simplest use case of Kinvey is storing and retrieving data to and from your cloud backend.

The basic unit of data is an entity and entities of the same kind are organized in collections. An entity is a set of key-value pairs which are stored in the backend in JSON format. Kinvey's libraries automatically translate your native objects to JSON.

Kinvey's data store provides simple CRUD operations on data, as well as powerful filtering and aggregation.

Collections

To start working with a collection, you need to instantiate a DataStore in your application. Each DataStore represents a collection on your backend.

// we use the example of a "Book" datastore
DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, client);

If the active user logs out, all DataStore references are invalid, because the data store is cleared on logout. The DataStore must be re-instantiated after login.

Through the remainder of this guide, we will refer to the instance we just retrieved as dataStore.

Optionally, you can configure the DataStore type when creating it. To understand the types of data stores and when to use each type, refer to the DataStore Types section.

DataStore Types

When you get an instance of a datastore in your application, you can optionally select a datastore type. The type has to do with how the library handles intermittent or prolonged interruptions in connectivity to the backend.

Choose a type that most closely resembles your data requirements.

  • DataStoreType.Sync: You want a copy of some (or all) the data from your backend to be available locally on the device and you like to sync it periodically with the backend.
  • DataStoreType.Cache: You want data stored on the device to optimize your app’s performance and/or provide offline support for short periods of network loss. This is the default DataStore type if a type is not explicitly set.
  • DataStoreType.Network: You want the data in your backend to never be stored locally on the device, even if it means that the app will not offer offline use.

Head to the Specify DataStore Type section to learn how to request the DataStore type that you selected.

Entities

The Kinvey service has the concept of entities, which represent a single resource.

public class Book extends GenericJson {
    @Key("name")
    public String name;
}

Fetching

You can retrieve entities by either looking them up using an id, or by querying a collection.

Fetching by Id

To fetch an (one) entity by id, call dataStore.find.

dataStore.find(t.ID, new KinveyClientCallback<Book>(){
  @Override
  public void onSuccess(Book result) {
    // Place your code here
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your code here
  }
});

Fetching by Query

To fetch all entities in a collection, call dataStore.find.

dataStore.find(new KinveyListCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
      // Place your code here
  }

  @Override
  public void onFailure(Throwable error) {
      // Place your code here
  }
});

To fetch multiple entities using a query, call dataStore.find and pass in a query.

Query query = client.query().in("_id", new String[]{"1", "2"});

dataStore.find(query, new KinveyListCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
      // Place your code here
  }

  @Override
  public void onFailure(Throwable error) {
      // Place your code here
  }
});

It's worth noting that an empty array is returned when the query matches zero entities.

Saving

You can save an entity by calling dataStore.save.

try
{
  dataStore.save(new Book("MyBook"), new KinveyClientCallback<Book>(){
    @Override
    public void onSuccess(Book result) {
      // Place your code here
      // here we have a Book object with defined unique `_id`
    }

    @Override
    public void onFailure(Throwable error) {
      // Place your code here
    }
  });
}
catch (KinveyException ke)
{
  // handle error
}

The save method acts as upsert. The library uses the _id property of the entity to distinguish between updates and inserts.

  • If the entity has an _id, the library treats it as an update to an existing entity.

  • If the entity does not have an _id, the library treats it as a new entity. The Kinvey backend assigns an automatic _id for a new entity.

Deleting

To delete an entity, call dataStore.delete and pass in the entity _id.

dataStore.delete(id, new KinveyDeleteCallback() {
  @Override
  public void onSuccess(Integer integer) {
    // Place your code here
  }

  @Override
  public void onFailure(Throwable throwable) {
    // Place your code here
  }
});

Deleting Multiple Entities at Once

To delete multiple entities at once, call dataStore.delete. Optionally, you can pass in a query to only delete entities matching the query.


Query query = myClient.query().in("_id", new String[]{"1", "2"});


dataStore.delete(query, new KinveyDeleteCallback() {
  @Override
  public void onSuccess(Integer integer) {
    // Place your code here
  }

  @Override
  public void onFailure(Throwable throwable) {
    // Place your code here
  }
});

Querying

Queries to the backend are performed through iternal implementation. Implementation provides a number of methods to add filters and modifiers to the query, although the Kinvey-Android library only supports a subset of these operators. Querying is performed on the DataStore by using the find(query, KinveyListCallback) method, where the query that is passed into the method can be constructed using Client.query().

Under most circumstances, you want to fetch data from your backend that match a specific set of conditions. Our lib provides a mechanism to build up a query for retrieval of a list of items from a collection. For example, you may want to retrieve a sorted list from your "books" collection:

List<Book> books;

DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, myClient);

Query query = myClient.query().addSort("title", SortOrder.ASC);

// The KinveyListCallback is used to obtain the results of a find operation.
// In the cache datastore case, the `onSuccess()` callback of the delegate will be
// called back with results returned from the source you pass as StoreType parameter in DataStore creation step

KinveyListCallback<Book> kinveyCallback = new KinveyListCallback<Book>()
{
  @Override
  public void onSuccess(List<Book> result) {
    // Place your code here
    books = result;
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
};

bookStore.find(query, kinveyCallback);

Operators

In addition to exact match and null queries, you can query based upon several types of expressions: comparison, set match, string and array operations.

Logical Operators

  • and(query) - matches records where both conditions are true
  • or(query) - matches records where either of the conditions are true

Ordering operators

addSort(fieldName, SortOrder) - orders the result set by chosen field and Sort order.

Comparison operators

  • equals(key, value) - the field value passed as first parameter must be equal to the second parameter
  • greaterThanEqualTo(key, value) - the field value passed as first parameter must be greater than or equal to the second parameter
  • lessThanEqualTo(key, value) - the field value passed as first parameter must be less than or equal to the second parameter
  • greaterThan(key, value) - the field value passed as first parameter must be greater than the second parameter
  • lessThan(key, value) - the field value passed as first parameter must be less than the second parameter
  • notEqual(key, value) - the field value passed as first parameter must be not equal to the second parameter
  • in(key, value[]) - the field value passed as second parameter is a array and it must contain the first parameter

Modifiers

Modifiers can be applied to a query in order to control how query results are presented. This includes returning sections of the results, sorting results, and only returning specific fields of an entity.

Limit and Skip

Depending on the size of the results from a query, you may choose to receive the results in smaller sections of the total result set. This is where the concept of limit and skip come into play. Providing a limit size will ensure that the number of results passed back do not exceed the limit given. In conjunction with this, providing a skip count will allow you to offset where your results start, relative to the total result set. Used together, this can achieve pagination of results.

In current Query implementation, limit is achieved using the setLimit() operators and skip is done using, appropriately enough, the setSkip() operator. The example below shows how you would take the first 30 books from the book store in chunks of 10:

Query query = myClient.query().startsWith("title", "How To").setSkip(0).setLimit(10);
Query query = myClient.query().startsWith("title", "How To").setSkip(10).setLimit(10);
Query query = myClient.query().startsWith("title", "How To").setSkip(20).setLimit(20);

Sort

As mentioned above in the Ordering Operators section, results from a query can be modified in order to be returned in sorted order (either ascending or descending), based on a particular field.

// ascending
Query query = myClient.query().startsWith("title", "How To").addSort("title", SortOrder.ASC);

// descending
Query query = myClient.query().startsWith("title", "How To").addSort("title", SortOrder.DESC);

Aggregation / Grouping

Grouping allows you to collect all entities with the same value for a field or fields, and then apply a reduce function (such as count or average) on all those items.

The results are returned as an Aggregation object that represents the list of groups containing the result of the reduce function.

To count all elements in the group, use the dataStore.group() with AggregateType.COUNT:

ArrayList<String> list = new ArrayList<String>();
list.add("bookname");
Query query = myClient.query();
query = query.equals("field", "value"); 
dataStore.group(AggregateType.COUNT, list, null, query, new KinveyAggregateCallback() {
  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }

  @Override
  public void onSuccess(Aggregation response) {
    Log.i("TAG",  "got: " + response.getResultsFor("key", "value"));
    // Place your code here
  }
  }, new KinveyCachedAggregateCallback() {
  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }

  @Override
  public void onSuccess(Aggregation response) {
    Log.i("TAG",  "got: " + response.getResultsFor("key", "value"));
    // Place your code here
  }
});

There are group method with five pre-defined AggregateTypes:

  • group(AggregateType.COUNT, fields, null, query, kinveyAggregateCallback, kinveyCachedAggregateCallback) - counts all elements in the group
  • group(AggregateType.SUM, fields, reduceField, query, kinveyAggregateCallback, kinveyCachedAggregateCallback) - sums together the numeric values for the input field
  • group(AggregateType.MIN, fields, reduceField, query, kinveyAggregateCallback, kinveyCachedAggregateCallback) - finds the minimum of the numeric values of the input field
  • group(AggregateType.MAX, fields, reduceField, query, kinveyAggregateCallback, kinveyCachedAggregateCallback) - finds the maximum of the numeric values of the input field
  • group(AggregateType.AVERAGE, fields, reduceField, query, kinveyAggregateCallback, kinveyCachedAggregateCallback) - finds the average of the numeric values of the input field

Caching and Offline

A key aspect of good mobile apps is their ability to render responsive UIs, even under conditions of poor or missing network connectivity. The Kinvey library provides caching and offline capabilities for you to easily manage data access and synchronization between the device and the backend.

Kinvey’s DataStore provides configuration options to solve common caching and offline requirements. If you need better control, you can utilize the options described in Granular Control.

Specify DataStore Type

When initializing a new DataStore to work with a Kinvey collection, you can optionally specify the DataStore type. The type determines how the SDK handles data reads and writes in full or intermittent absence of connection to the app backend.

In this section:

Sync

Configuring your datastore as a Sync datastore allows you to pull a copy of your data to the device and work with it completely offline. The library provides APIs to synchronize local data with the backend.

This type of datastore is ideal for apps that need to work for long periods without a network connection.

Here is how you should use a Sync store -

// Get an instance
DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, myClient);

// Pull data from your backend and save it locally on the device.
dataStore.pull(new KinveyPullCallback<Book>(){
  @Override
  public void onSuccess(KinveyPullResponse<Book> result) {
      books = result.getResult();
  }

  @Override
  public void onFailure(Throwable error) {
      // Place your error handler here
  }
});

// Find data locally on the device.
dataStore.find(new KinveyListCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
    booksFromCache = result;
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
});

// Save an entity locally on the device. This will add the item to the
// sync table to be pushed to your backend at a later time.
Book book = new Book("My First Book");
dataStore.save(book, new KinveyClientCallback<Book>(){
  @Override
  public void onSuccess(Book result) {
      savedBook = result;
  }

  @Override
  public void onFailure(Throwable error) {
      // Place your error handler here
  }
});

// Sync local data with the backend
// This will push data that is saved locally on the device to your backend; 
// and then pull any new data on the backend and save it locally.
dataStore.sync(new KinveySyncCallback<Book>(){
  public void onSuccess(KinveyPushResponse kinveyPushResponse, KinveyPullResponse<Book> kinveyPullResponse){};
  public void onPullStarted(){};
  public void onPushStarted(){};
  public void onPullSuccess(KinveyPullResponse<Book> kinveyPullResponse){};
  public void onPushSuccess(KinveyPushResponse kinveyPushResponse){};
  public void onFailure(Throwable t){};
});

The pull, push, sync, and syncCount APIs allow you to synchronize data between the application and the backend. This section describes how these APIs work with examples.

Pull Operation

Calling pull() retrieves data from the backend and stores it locally in the Sync Store.

By default, pulling will retrieve the entire collection to the device. Optionally, you can provide a query parameter to pull to restrict what entities are retrieved. If you prefer to only retrieve the changes since your last pull operation, you should enable delta set caching.

The pull API needs a network connection in order to succeed.

DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, client);
try
{
  // Pull data from your backend and save it locally on the device.
  dataStore.pull(new KinveyPullCallback<Book>(){
    @Override
    public void onSuccess(KinveyPullResponse<Book> result) {
        booksFromCache = result.getResult();
    }

    @Override
    public void onFailure(Throwable error) {
        // Place your error handler here
    }
  });
} catch (KinveyException ke) {
    // handle exception
}

If your Sync Store has pending local changes, they must be pushed to the backend before pulling data to the store.

Push Operation

Calling push() kicks off a uni-directional push of data from the library to the backend.

The library goes through the following steps to push entities modified in local storage to the backend -

  • Reads from the "pending writes queue" to determine what entities have been changed locally. The "pending writes queue" maintains a reference for each entity in local storage that has been modified by the app. For an entity that gets modified multiple times in local storage, the queue only references the last modification on the entity.

  • Creates a REST API request for each pending change in the queue. The type of request depends on the type of modification that was performed locally on the entity.

    • If an entity is newly created, the library builds a POST request.

    • If an entity is modified, the library builds a PUT request.

    • If an entity is deleted, the library builds a DELETE request.

  • Makes the REST API requests against the backend concurrently. Requests are batched to avoid hitting platform limits on the number of open network requests.

    • For each successful request, the corresponding reference in the queue is removed.

    • For each failed request, the corresponding reference remains persisted in the queue. The library adds information in the push/sync response to indicate that a failure occurred. Push failures are discussed in Handling Failures.

  • Returns a response to the application indicating the count of entities that were successfully synced, and a list of errors for entities that failed to sync.

DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, client);
try
{
  // Push data to your backend.
  dataStore.push(new KinveyPushCallback(){
    @Override
    public void onSuccess(KinveyPushResponse result) {
    }

    @Override
    public void onFailure(Throwable error) {
        // Place your error handler here
    }

    @Override
    public void onProgress(long current, long all) {

    }
  });
} catch (KinveyException ke) {
    // handle exception
}
Handling Push Failures

The push response contains information about the entities that failed to push to the backend. For each failed entity, the corresponding reference in the pending writes queue is retained. This is to prevent any data loss during the push operation. The developer can decide how to handle failures. Here are some options -

  • Retry pushing your changes at a later time. You can simply call push again on the datastore to attempt again.
  • Ignore the failed changes. You can call purge on the datastore, which will remove all pending writes from the queue. The failed entity remains in your local cache, but the library will not attempt to push it again to the backend.
  • Destroy the local cache. You call clear on the datastore, which destroys the local cache for the store. You will need to pull fresh data from the backend to start using the cache again.

Each of the APIs mentioned in this section are described in more detail in the datastore API reference.

Sync Operation

Calling sync() on a Sync Store kicks off a bi-directional synchronization of data between the library and the backend. First, the library calls push to send local changes to the backend. Subsequently, the library calls pull to fetch data in the collection from the backend and stores it on the device.

You can provide a query as a parameter to the sync API, to restrict the data that is pulled from the backend. The query does not affect what data gets pushed to the backend. If you prefer to only retrieve the changes since your last sync operation, you should enable delta set caching.

DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.SYNC, client);
try {
  dataStore.sync(new KinveySyncCallback<Book>(){
    public void onSuccess(KinveyPushResponse kinveyPushResponse, KinveyPullResponse<Book> kinveyPullResponse){};
    public void onPullStarted(){};
    public void onPushStarted(){};
    public void onPullSuccess(KinveyPullResponse<Book> kinveyPullResponse){};
    public void onPushSuccess(KinveyPushResponse kinveyPushResponse){};
    public void onFailure(Throwable t){};
  });
} catch (KinveyException ke) {
    // handle exception
}
Sync Count Operation

You can retrieve a count of entities modified locally and pending a push to the backend.

// Number of entities modified offline
int syncStoreCount = dataStore.syncCount();

Cache

Configuring your datastore as a Cache datastore allows you to use the performance optimizations provided by the library. In addition, the cache allows you to work with data when the device goes offline.

The Cache mode is the default mode for the data store. Most of the time you don't need to set it explicitly.

This type of datastore is ideal for apps that are generally used with an active network, but may experience short periods of network loss.

Here is how you should use a Cache store -

// Get an instance
DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.CACHE, myClient);

dataStore.find(new KinveyListCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
    books = result;
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
}, new KinveyCachedClientCallback<Book>() {
  @Override
  public void onSuccess(List<Book> result) {
    books = result;
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
});

// Save an entity. The entity will be saved to the device and your backend.
// If you do not have a network connection, the entity will be stored in local storage,
// to get pushed to the backend when network becomes available.
Book book = new Book("My First Book");
dataStore.save(book, new KinveyClientCallback<Book>(){
  @Override
  public void onSuccess(Book result) {
      book = result;
  }

  @Override
  public void onFailure(Throwable error) {
      // Place your error handler here
  }
});

The intermediate results returning cache data is only valid for use with a data store of type DataStoreType.CACHE. Intermediate results are not applicable for the DataStoreType.SYNC and DataStoreType.NETWORK data stores.

The Cache Store executes all CRUD requests against local storage as well as the backend. Any data retrieved from backend is stored in the cache. This allows the app to work offline by fetching data that has been cached from past usage.

The Cache Store also stores pending write operations when the app is offline. However, the developer is required to push these pending operations to the backend when the network resumes. The push API should be used to accomplish this.

Network

Configuring your datastore as Network turns off all caching in the library. All requests to fetch and save data are sent to the backend.

We don’t recommend this type of datastore for apps in production, since the app will not work without network connectivity. However, it may be useful in a development scenario to validate backend data without a device cache.

Here is how you should use a Network store -

// Get an instance
DataStore<Book> dataStore = DataStore.collection("books", Book.class, StoreType.Network, client);


// Retrieve data directly from your backend
dataStore.find(new KinveyListCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
    books = result
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
});

// Save an entity directly to your backend
Book book = new Book("My First Book");
dataStore.save(book, new KinveyClientCallback<Book>(){
  @Override
  public void onSuccess(List<Book> result) {
    books = result
  }

  @Override
  public void onFailure(Throwable error) {
    // Place your error handler here
  }
});

Granular Control

Selecting a DataStoreType is usually sufficient to solve the caching and offline needs of most apps. However, should you desire more control over how data is managed in your app, you can use the granular configuration options provided by the library. The following sections discuss the advanced options available on the DataStore.

In this section:

Data Reads

The way to control Data Reads is to set a ReadPolicy.

Read Policy

ReadPolicy controls how the library fetches data. When you select a store configuration, the library sets the appropriate ReadPolicy on the store. However, individual reads can override the default read policy of the store by specifying a ReadPolicy.

The following read policies are available:

  • ForceLocal - forces the datastore to read data from local storage. If no valid data is found in local storage, the request fails.
  • ForceNetwork - forces the datastore to read data from the backend. If network is unavailable, the request fails.
  • Both - reads first from the local cache and then attempts to get data from the backend.

Examples

Assume that you are using a datastore with caching enabled (the default), but want to force a certain find request to fetch data from the backend. This can be achieved by specifying a ReadPolicy when you call the find API on your store.

//create a Sync store
DataStore<Book> dataStore = DataStore<Book>.Collection("books", Book.class, StoreType.SYNC, client);
... 

//force the datastore to request data from the backend
DataStore<Book> dataStore = DataStore<Book>.Collection("books", Book.class, StoreType.NETWORK, client);
Delta Set Caching

The library implements a mechanism to optimize the amount of data retrieved from the backend. When you use a Sync or Cache datastore, data requests to the backend only fetch data that changed since the previous update. We call this mechanism "Delta Set Caching".

Delta set caching applies to Sync and Cache data stores. To use delta set caching, you need to enable it on the store.

//set delta set caching flag to client and all `DataStores` using that client will use delta set cache logic
Client myKinveyClient = new Client.Builder(this).build();
myKinveyClient.setUseDeltaCache(true);

How it works

Each DataStore maintains metadata per entity such as the last time the entity was updated. When the library requests data from the backend, it makes multiple calls to the backend - first to retrieve the metadata of each entity, and subsequently (if required) to fetch only those entities that changed since the previous update.

Discussion

  • To request a delta update from the backend, the library will make the following two API calls -

    • To retrieve the metadata for entities, the original request for data is appended with the fields modifier, as follows:

      # only retrieve the _id and lmt fields for entities that match the query
      ?query={...}&fields=_id,_kmd.lmt
    • To retrieve the specific entities that have changed, the library makes a request with the in query filter. If necessary, the library may batch entities into multiple requests, to avoid hitting platform limits on URL length.

      # retrieve specific entities by id
      ?query={"_id":{"$in": ["<entity1.id>", "<entity2.id>", ...]}}

Delta set caching requires that your backend support the query modifiers described above - fields and $in. All Kinvey out-of-the-box connectors are built to support fields and $in. To use delta set caching, custom connectors are required to support these query modifiers.

  • The performance benefits of delta set caching are most noticeable on large collections that have low rate of change. That said, our tests show that in most cases, performance is improved with delta set caching enabled.

Data Writes

Write Policy

When you save data, the type of datastore you use determines how the data gets saved.

For a store of type Sync, data is written to your local copy. In addition, the library maintains additional information in a "pending writes queue" to recognize that this data needs to be sent to the backend when you decide to sync or push.

For a store of type Cache, data is written first to your local copy and sent immediately to be written to the backend. If the write to the backend fails (e.g. because of network connectivity), the library maintains information to recognize that this data needs to be sent to the backend when connectivity becomes available again. Due to platform limitations, this does not happen automatically, but needs to be initiated from the user by calling the push() or sync() methods.

For a store of type Network, data is sent directly to the backend. If the write to the backend fails, the library does not persist the data for a future write.

Conflicts

When using sync and cache stores, you need to be aware of situations where multiple users could be working on the same entity simultaneously offline. Consider the following scenario:

  1. User X edits entity A offline.
  2. User Y edits entity A offline.
  3. Network connectivity is restored for X, and A is synchronized with Kinvey.
  4. Network connectivity is restored for Y, and A is synchronized with Kinvey.

In the above scenario, the changes made by user X are overwritten by Y.

The libraries and backend implement a default mechanism of "client wins", which implies that the data in the backend reflects the last client that performed a write. Custom conflict management policies can be implemented with Business Logic.

Timeout

When performing any datastore operations, you can pass a timeout value as an option to stop the datastore operation after some amount of time if it hasn't already completed.

The global default timeout in the SDK is set to 60 seconds. You can set the global timeout to your own value in three ways:

  1. When you initialize the Client.

    Client myKinveyClient = new Client.Builder(this).setRequestTimeout(120000).build();
  2. In kinvey.properties file, you need add request.timeout option in the end of file.

    request.timeout=120000
  3. When client was already initialized.

    myKinveyClient.setRequestTimeout(120000);

Related Samples

Got a question?