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.

Entities

The library allows the flexibility of adding a Kinvey backend to any object by simply extending com.google.api.client.json.GenericJson.

You can model your entities using any kind of java Object implementation you choose. To work with the library, the class must extend com.google.api.client.json.GenericJson. Attributes can either be added using GenericJson.put, or by annotating any class member with @Key (com.google.api.client.util.Key).

For example let's assume we are building a Calendar application, and want to represent an event. Our event will have a name, date, time, and a location. Here is what that class might look like:

public class EventEntity extends GenericJson {

    @Key("_id")
    private String id; 
    @Key
    private String name;
    @Key
    private String location;
    @Key
    private String date;
    @Key("_kmd")
    private KinveyMetaData meta; // Kinvey metadata, OPTIONAL
    @Key("_acl")
    private KinveyMetaData.AccessControlList acl; //Kinvey access control, OPTIONAL

    public EventEntity(){}  //GenericJson classes must have a public empty constructor


}

GenericJson

The GenericJson base class is utilized to map your Java data model classes to the Kinvey backend. The @Key annotation tells the library what the field names for your classes' properties are. Keys can be applied to any primitive, as well as any complex Objects that are serializable by the Gson library.

The @Key annotation will use the member name as the JSON Key name, unless an alternate key name is specified. For example, the below example will create a JSON name-value pair with the name "FirstName".

@Key("FirstName")
private String givenName;

In our example, we have properties for name,date, and location. In the data browser, this entity looks similar to:

Example fields in the Events Collection

In addition to the @Key values, additional values can be dynamically added to the entity by calling the put(name, value) method:

myEntity.put("FirstName", "John");

Any property of a GenericJson object can be retrieved by calling the get(name) method:

String givenName = (String) myEntity.get("FirstName");

Datatypes

Any property of the GenericJson object will be serialized, provided that the object is serializable by the Gson library. Some common serializations are:

Java TypeJSON Type
Stringstring
int or Integernumber
double or Doublenumber
long or Longnumber
boolean or Booleanboolean
Java Arraysarray
Collection<> typesobject
GenericJson objectsobject

Nested Classes and Objects

GenericJson can serialize and de-serialize static nested classes as well as collection types into inner JSON Objects. For example, consider the following classes representing a Location:

public class Location extends GenericJson {
    @Key
    private String Name;
    @Key
    private String FoodType;

    public Location(){} //GenericJson classes must have a public empty constructor

    private static class Address extends GenericJson {            
        @Key
        private String Address;
        @Key
        private String City;
        @Key
        private String State;
        @Key
        private String Zip;

        public Address(){} //GenericJson classes must have a public empty constructor
    }
}

The Gson parser would create a JSON object similar to the following:

{
    "Name" : "Kinvey Cafe",
    "FoodType" : "eclectic",
    "Address" : 
        {
            "Address" : "1 Main Street",
            "City" : "Boston",
            "State" : "MA",
            "Zip" : "02220" 
        }
}

Java Collection types such as ArrayList<> and HashMap<> can also be used as types that are serialized as JSON objects, and multiple JSON objects can be nested inside collections.

For more details about Gson and type support, see the Gson User Guide

Metadata

You can optionally have the library supply metadata information by mapping a KinveyMetaData object as a @Key member of your GenericJson entity. KinveMetaData provides meta-information about the entity:

  • getLastModifiedTime the time the entity was last updated on the server
  • getCreatorId the id of the user that first created the object.

There are also additional fields to control read and write permissions of the object; these are covered in the Security guide.

AppData

The AppData API provides an interface to save and load data from the backend. The Kinvey backend allows for the organization of data into one or more collections. To create an instance of AppData, you need to call client.appdata and provide the collection name, as well as a Class object that represents the Entity in your app.

//The EventEntity class is defined above
EventEntity event = new EventEntity();
event.setName("Launch Party");
event.setAddress("Kinvey HQ");
AsyncAppData<EventEntity> myevents = mKinveyClient.appData("eventsCollection",EventEntity.class);
myevents.save(event, new KinveyClientCallback<EventEntity>() {
    @Override
    public void onFailure(Throwable e) {
        Log.e("TAG", "failed to save event data", e);
    }

    @Override
    public void onSuccess(EventEntity r) {
        Log.d("TAG", "saved data for entity "+ r.getName()); 
    }
});

Saving

When saving an entity, your code simply calls the save method and passes in your GenericJson entity. If the entity has no corresponding _id mapped, the server will generate one and add it to the entity that is returned to the onSuccess() method.

//The EventEntity class is defined above
EventEntity event = new EventEntity();
event.setName("Launch Party");
event.setAddress("Kinvey HQ");
AsyncAppData<EventEntity> myevents = mKinveyClient.appData("events", EventEntity.class);
myevents.save(event, new KinveyClientCallback<EventEntity>() {
    @Override
    public void onFailure(Throwable e) {
        Log.e("TAG", "failed to save event data", e); 
    }

    @Override
    public void onSuccess(EventEntity r) {
        Log.d("TAG", "saved data for entity "+ r.getName()); 
    }
});

New Entities

When you create a new entity, you have the option of supplying your own id or letting Kinvey create one for you on the server side (this happens upon save).

The id can be any per-collection unique string. If you were storing a list of marathon runners, you could use their bib number as the id field. In the example above, there is no unique property (multiple people could create different "Launch Parties"), so an ID would automatically be created.

Existing Entities

The result sent back from the server in response to the save will be an updated form of the entity. This means the object will contain a server-assigned _id if one does not exist, have an updated lastModifiedTime for the entity, and perform any pre and post-save updates as dictated by custom Business Logic. When the save returns from the backend, the library will update the object to reflect the current server state.

Fetching

You can retrieve entities from a Kinvey collection into a GenericJson object using either a query or an entity id.

See the querying section for building compound queries, ordering and sorting.

Fetching by id

In some cases, you may just want to fetch one object out of your collection. You can do that by making use of the getEntity method.

This can be used to update an object with the latest version from the server, or additional entities.

//The EventEntity class is defined above
EventEntity event = new EventEntity();
AsyncAppData<EventEntity> myEvents = mKinveyClient.appData("events", EventEntity.class);
myEvents.getEntity(eventID, new KinveyClientCallback<EventEntity>() {
    @Override
    public void onSuccess(EventEntity result) { 
        Log.v("TAG", "received "+ result.getId() );
    }

    @Override
    public void onFailure(Throwable error) { 
        Log.e("TAG", "failed to fetchByFilterCriteria", error);
    }
});

Fetching by Query

Use a Query object to retrieve the items from the store that match the given query. Once built, pass the Query object to the AppData.get method to retrieve a filtered result set. To find out how to write more complex queries, see the querying guide.

//The EventEntity class is defined above
EventEntity events = new EventEntity();
Query myQuery = mKinveyClient.query();
myQuery.equals("Name","Launch Party");
AsyncAppData<EventEntity> myEvents = mKinveyClient.appData("events", EventEntity.class);

myEvents.get(myQuery, new KinveyListCallback<EventEntity>() {
    @Override
    public void onSuccess(EventEntity[] results) { 
        Log.v("TAG", "received "+ results.length + " events");
    }

    @Override
    public void onFailure(Throwable error) { 
        Log.e("TAG", "failed to fetchByFilterCriteria", error);
    }
});

If you're getting no results, make sure your types match. For example, if you try to use a String to match an int value, no results will be returned, even if their printed values are the same (12345 != "12345").

Fetching all

If you would like to fetch all of the entities of a particular collection, you can do this easily by calling AppData.get with no arguments other than the callback.

//The EventEntity class is defined above
AsyncAppData<EventEntity> myevents = mKinveyClient.appData("events", EventEntity.class);
myevents.get(new KinveyListCallback<EventEntity>() {
    @Override
    public void onSuccess(EventEntity[] result) { 
        Log.v("TAG", "received "+ result.length + " events");
    }

    @Override
    public void onFailure(Throwable error)  { 
        Log.e("TAG", "failed to fetch all", error);
    }
});

Deleting

To delete an entity in a collection, use AppData.delete.

//The EventEntity class is defined above
String eventId = event.getId();
AsyncAppData<KinveyDeleteResponse> myevents = mKinveyClient.appData("events", EventEntity.class);
myevents.delete(eventId, new KinveyDeleteCallback() {
    @Override
    public void onSuccess(KinveyDeleteResponse response) {
        Log.v("TAG", "deleted successfully"); 
    }

    public void onFailure(Throwable error) {
        Log.e("TAG", "failed to delete ", error);
    }  
});

Deleting Multiple Entities at Once

In addition to supplying a String id to AsyncAppData.delete(…), you can also supply a Query to delete all entites matching the results of the Query. See the querying guide for more information about structuring queries.

Querying

Queries to the backend are represented as an instance of the Query class. These queries are used with AppData.get to return a filtered result set. Query provides a number of methods to add filters and modifiers to the query. Calling mKinveyClient.query() returns a new instance of a Query object that returns all records in a collection by default:

Query query = mKinveyClient.query();

Under most circumstances, you want to fetch data from your backend that match a specific set of conditions. The Query class 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 the highest rated stores from your "events" collection:

Query q = mKinveyClient.query();
q.greaterThan("rating",80);
q.addSort("name", SortOrder.Asc);

AsyncAppData<EventEntity> myData = mKinveyClient.appData("events", EventEntity.class);
myData.get(q, new KinveyListCallback<EventEntity>() {
    @Override
    public void onFailure(Throwable t) {
         Log.d(TAG, "Error fetching data: " + t.getMessage());
    }

    @Override
    public void onSuccess(EventEntity[] eventEntities) {
         Log.d(TAG, "Retrieved high " + eventEntities.length + " scores");
    }
});

Operators

Comparison 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. Each operation can be added to a query by simply calling its method on an existing Query object.

Comparison operators

  • equals - matches where field value = the supplied value
  • lessThan - matches where field value is < the supplied value
  • lessThanEqualTo - matches where field value is <= the supplied value
  • greaterThan - matches where field value is > the supplied value
  • greaterThanEqualTo - matches where field value is >= the supplied value
  • notEqual - matches where field value is != the supplied value

Array Operators

  • all - matches array fields that contain all of the supplied values
  • size - matches array fields with the exact number of items
  • in - matches a field whose value is in the specified array
  • notIn - matches a field whose value is not in the specified array

String Queries

You can perform textual searches on fields using Regular Expressions. This can be done with Query.regex. For example, to filter a table view by event name using a search bar:

EditText searchBar = (EditText) findViewById(R.id.search_bar);
Query query = new Query();
query.regEx("name", searchBar.getText().toString());
AsyncAppData<EventEntity> searchedEvents = mKinveyClient.appData("events", EventEntity.class);
searchedEvents.get(query, new KinveyListCallback<EventEntity>() {
    @Override
    public void onSuccess(EventEntity[] event) {
        Log.v("TAG", "received "+ event.length);
    }

    @Override
    public void onFailure(Throwable error) {
        Log.e("TAG", "failed to query ", error);
    }      
});

Beginning and End of Strings

You can also search the beginning or end of a String field for a specific string pattern using startsWith or endsWith. For example, to find all names that begin with "Ki":

Query query = new Query();
query.startsWith("name", "Ki");
AsyncAppData<EventEntity> searchedEvents = mKinveyClient.appData("events", EventEntity.class);
searchedEvents.get(query, new KinveyListCallback<EventEntity>() {
    @Override
    public void onSuccess(EventEntity[] event) { 
        Log.v("TAG", "received "+ event.length);
    }

    @Override
    public void onFailure(Throwable error) {
        Log.e("TAG", "failed to query ", error);
    } 
});

Modifiers

Query modifiers control how query results are presented.

Limit and Skip

Limit and skip allow for paging of results. For example if there are 100 possible results, you can break them up into 20-item pages by setting the limit to 20, and then incrementing the skip modifier by 0,20,40,60, and 80 in separate queries to eventually retrieve them all.

Query q = new Query();
q.equals("name", storeName);
q.setLimit(20);
q.setSkip(20);

Sort

You can order the results returned by the queries you submit.

Query q = new Query();
q.equals("name", storeName);
q.addSort("score", SortOrder.desc);
q.addSort("name", SortOrder.asc);

The enum SortOrder determines if the criteria is sorted Ascending or Descending.

The results are sorted in the order that sort fields are added. In the example above, the result is first sorted by Score in descending order, then by name in ascending order.

Compound Queries

You can make expressive queries by stringing together several queries. For example, you can query for dates in a range or events at a particular day and place.

Conditional operators can be strung together to create a query that is a combination of conditionals. For example, the following code uses greaterThanEqualTo and lessThanEqualTo to query for ages in a range.

// Search for ages between 18 and 35
Query ageRangeQuery = new Query().greaterThanEqualTo("age",18).lessThanEqualTo("age", 35);

Multiple query conditions can either be chained together or executed sequentially. The example above is functionally equivalent to:

Query ageRangeQuery = new Query();
ageRangeQuery.greaterThanEqualTo("age", 18);
ageRangeQuery.lessThanEqualTo("age", 35);

Certain non-logical query conditions will not be accepted by the query engine. For example, querying the same field for both = and > would result in only the second condition being executed (>).

Joining Operators

You can also combine queries using boolean operators or and and.

Query query1 = new Query().equals("state","MA");
Query query2 = new Query().equals("state","CA");
query1.or(query2);

Complex combinations of queries can be created and joined using either of these operators.

Negating Queries

Sometimes it's easier to write a negative query by first expressing the positive query and then negating it. This can be achieved by calling not.

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. To do this, use AppData's various aggregation methods, specifying the fields to group by and an optional query to filter the results.

The results are returned as a GenericJson object that represents a dictionary of the grouped fields and the result of the reduce function.

For example, let's say you had a collection of events, and you want to count how many events were created by each user:

AsyncAppData<GenericJson> aggregate = mKinveyClient.appData("events", EventEntity.class");
ArrayList<String> fields = new ArrayList<String>();
fields.add("userName");
aggregate.count(fields,null, new KinveyAggregateCallback() {
    @Override
    public void onSuccess(Aggregation res) { 
        Log.i("TAG",  "got: " +res.results[0].get("_result"));
    }  
    @Override
    public void onFailure(Throwable error) {
        Log.e("TAG", "something went wrong ", error);
    }     
});

Reduce Function

There are five pre-defined reduce functions:

  • count - counts all elements in the group
  • sum - sums together the numeric values for the input field
  • min - finds the minimum of the numeric values of the input field
  • max - finds the maximum of the numeric values of the input field
  • average - finds the average of the numeric values of the input field

If you would like to see additional functions, email support@kinvey.com.

Scoping With Queries

The group method can also take an optional condition. This condition is a query that acts as a filter and is applied on the server before the reduce function is evaluated.

In our above example, if we wanted to count only events in the states of MA and CA, we would use:

AsyncAppData<GenericJson> aggregate = mKinveyClient.appData("events", EventEntity.class");
ArrayList<String> fields = new ArrayList<String>();
fields.add("userName");
String[] states = new String[] {"MA","CA"};
Query myQuery = new Query().in("states",states);
aggregate.count(fields,myQuery,new KinveyAggregateCallback() {
    @Override
    public void onSuccess(Aggregation res) { 
        Log.i("TAG",  "got: " +res.results[0].get("_result"));
    }
    @Override
    public void onFailure(Throwable error) {
        Log.e("TAG", "something went wrong ", error);
    }       
});

Location Querying

See the Location guide for information on how to query data by location.

Relational Data

Your data often includes entities with relationships to other entities. Relationships can be modeled either by denormalization or by having properties that are pointers to other objects.

For example, let's say we have an Events collection and an Invitations collection, where an event will have zero or more invitations, in a 1-to-many relationship. When loading the Event entity from the backend, you might want to load all of the associated invitations at the same time.

When you load an Employer entity from the backend, you might want to load all its employee entities at the same time. Or when inviting new people to an event, you might want to save those invitations along with the updated Event at the same time. This can be done through a KinveyReference.

On the backend, instead of saving the related object's data, a special dictionary will be used in its place. This looks like: { "_type" : "KinveyRef", "_id" : object_id, "_collection" : collection_name}, where the _collection and _id are used to resolve the entity.

Setup

In order to persist entity relationships to the backend, the GenericJson entity class must contain a @Key annotated member variable of type KinveyReference. The KinveyReference object maps an entity's field to another collection in the backend. This is defined by the unqiue _id field of the of referenced entity.

Event and Invitation Example

Continuing the example from other guides, the code below demonstrates an Event object that has a KinveyReference to an Invitation object.

public class EventEntity extends GenericJson {
    @Key
    private String name;
    @Key
    private String eventDate;
    @Key
    private String location;
    @Key
    private KinveyReference invitations;

    ...
}

Before setting a KinveyReference, ensure the object has first been given a unique _id, either by defining it yourself or saving the entity to a collection in your backend at Kinvey (which will automatically generate a unique _id if one isn't present).

In a method in EventEntity, initialize the invitations reference:

public void initReference(InvitationEntity myInvitation){
    KinveyReference reference = new KinveyReference("invitationCollection", myInvitation.get("_id").toString());
    this.invitations = reference;
}

The InvitationEntity also extends GenericJson:

public class InvitationEntity extends GenericJson {
    @Key
    private String objectId;
    @Key
    private int status;
    @Key("_id") 
    private String id;

    ...
}

Saving

When an object is saved through AppData.save the parent entity, and all of its relations (to which the active user will have write access) will be saved.

Fetching

Fetching an entity with its related entities is as simple as invoking the AppData.get method on any entity that includes a member variable of type KinveyReference as outlined in the above setup section. If a KinveyReference is present, the related entities are retrieved whether you fetch the data by entityID or by Query.

Limitations

When using this feature, you need to be aware of the following limitations:

  • Per request, a maximum of 100 references will be resolved. This limit is enforced to achieve a good trade-off between wait times and user experience.
  • When loading entities, if two objects share a reference to a common third entity, two separate objects will be created for that third entity. This means two objects will be created, holding the same data. Changes to one of these objects will not be reflected in the other, meaning saving an object will override the others changes.

Request Cancellation and timeouts

The library lets you cancel pending data store requests with KinveyCancellableCallback.

The return value of the isCancelled() method is checked regularly while execution occurs, and before the callback is made. To cancel a pending request, your callback should return true in isCancelled(). Once a request has been cancelled, onCancelled() is called on the UI thread.

The following code shows how you would cancel pending requests.

//The EventEntity class is defined above
EventEntity event = new EventEntity();

AsyncAppData<EventEntity> myEvents = mKinveyClient.appData("events", EventEntity.class);
myEvents.getEntity(eventID, new KinveyCancellableCallback<EventEntity>() {
   @Override
   public void isCancelled() 
      if (/*insert condition in your app that, if true, should cancel this request*/)
        return true;
      }
      else {
        return false;
      }
   }       
   @Override
   public void onCancelled() {
       Log.v("TAG", "The fetch operation was cancelled");
   }
   @Override
   public void onSuccess(EventEntity result) {
       Log.v("TAG", "Received "+ result.getId() );
   }
   @Override
   public void onFailure(Throwable error) {
       Log.e("TAG", "Failed to fetch", error);
   }
});

The default request timeout for all network requests is infinity. Client.Builder#setRequestTimeout lets you set a custom timeout in milliseconds that applies globally. Below is an example of how you would set a global custom timeout of 15 seconds.

final Client mKinveyClient = new Client.Builder(your_app_key, your_app_secret
    , this.getApplicationContext()).setRequestTimeout(15 * 1000).build();

[[KCSClient sharedClient] initializeWithConfiguration:config];

You could also set the timeout as a property in the kinvey.properties file as shown below. Refer to the Getting Started guide for details on using the kinvey.properties file.

request.timeout = 15000

Related Samples

Got a question?