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
.
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")
// Kinvey metadata, OPTIONAL
private KinveyMetaData meta;
@Key("_acl")
//Kinvey access control, OPTIONAL
private KinveyMetaData.AccessControlList acl;
//GenericJson classes must have an empty public constructor
public EventEntity(){}
}
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:
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 Type | JSON Type |
---|---|
String | string |
int or Integer | number |
double or Double | number |
long or Long | number |
boolean or Boolean | boolean |
Java Arrays | array |
Collection<> types | object |
GenericJson objects | object |
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 servergetCreatorId
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");
AppData<EventEntity> myevents = mKinveyClient.appData("eventsCollection",EventEntity.class);
try{
EventEntity result = myevents.saveBlocking(event).execute();
}catch (IOException e){
System.out.println("Couldn't save! -> " + e);
}
Saving
When saving an entity, your code simply calls the saveBlocking
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 by calling the execute()
method.
//The EventEntity class is defined above
EventEntity event = new EventEntity();
event.setName("Launch Party");
event.setAddress("Kinvey HQ");
AppData<EventEntity> myevents = mKinveyClient.appData("eventsCollection",EventEntity.class);
try{
EventEntity result = myevents.saveBlocking(event).execute();
}catch (IOException e){
System.out.println("Couldn't save! -> " + e);
}
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.
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();
AppData<EventEntity> myEvents = mKinveyClient.appData("events", EventEntity.class);
try{
EventEntity result = myEvents.getEntityBlocking(eventID).execute();
}catch (IOException e){
System.out.println("Couldn't get! -> " + e);
}
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.getBlocking
method to retrieve a filtered result set. To find out how to write more complex queries, see the querying guide.
Query myQuery = mKinveyClient.query();
myQuery.equals("Name","Launch Party");
AppData<EventEntity> myEvents = mKinveyClient.appData("events", EventEntity.class);
try{
EventEntity[] results = myEvents.getBlocking(myQuery).execute();
}catch (IOException e){
System.out.println("Couldn't get! -> " + e);
}
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.
//The EventEntity class is defined above
AppData<EventEntity> myevents = mKinveyClient.appData("events", EventEntity.class);
try{
EventEntity[] results = myevents.getBlocking().execute();
}catch (IOException e){
System.out.println("Couldn't get! -> " + e);
}
Deleting
To delete an entity in a collection, use AppData.delete
.
//The EventEntity class is defined above
String eventId = event.getId();
AppData<KinveyDeleteResponse> myevents = mKinveyClient.appData("events", EventEntity.class);
try{
KinveyDeleteResponse result = myevents.deleteBlocking(eventId).execute();
}catch (IOException e){
System.out.println("Couldn't delete! -> " + e);
}
Deleting Multiple Entities at Once
In addition to supplying a String
id to AppData.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 valuelessThan
- matches where field value is < the supplied valuelessThanEqualTo
- matches where field value is <= the supplied valuegreaterThan
- matches where field value is > the supplied valuegreaterThanEqualTo
- matches where field value is >= the supplied valuenotEqual
- matches where field value is != the supplied value
Array Operators
all
- matches array fields that contain all of the supplied valuessize
- matches array fields with the exact number of itemsin
- matches a field whose value is in the specified arraynotIn
- 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:
Query query = new Query();
query.regEx("name","searchText");
AppData<EventEntity> searchedEvents = mKinveyClient.appData("events", EventEntity.class);
try{
EventEntity[] results = searchedEvents.getBlocking(query).execute();
}catch (IOException e){
System.out.println("Couldn't get! -> " + e);
}
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");
AppData<EventEntity> searchedEvents = mKinveyClient.appData("events", EventEntity.class);
try{
EventEntity[] results = searchedEvents.getBlocking(query).execute();
}catch (IOException e){
System.out.println("Couldn't get! -> " + e);
}
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.
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:
AppData<GenericJson> aggregate = mKinveyClient.appData("events", EventEntity.class");
ArrayList<String> fields = new ArrayList<String>();
fields.add("userName");
try{
Aggregation results = aggregate.count(fields, null).execute();
}catch (IOException e){
System.out.println("Couldn't count! -> " + e);
}
Reduce Function
There are five pre-defined reduce functions:
count
- counts all elements in the groupsum
- sums together the numeric values for the input fieldmin
- finds the minimum of the numeric values of the input fieldmax
- finds the maximum of the numeric values of the input fieldaverage
- 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:
AppData<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);
try{
Aggregation results = aggregate.count(fields,myQuery).execute();
}catch (IOException e){
System.out.println("Couldn't count! -> " + e);
}
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 timeout
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];