Querying the Local Datastore
If you have enabled the local datastore by calling Parse.enableLocalDatastore() before your call to Parse.initialize(), then you can also query against the objects stored locally on the device. To do this, call the fromLocalDatastore method on the query.
query.fromLocalDatastore();
query.findInBackground(new FindCallback<ParseObject>() {
public void done(List<ParseObject> scoreList, ParseException e) {
if (e == null) {
// Results were successfully found from the local datastore.
} else {
// There was an error.
}
}
});
You can query from the local datastore using exactly the same kinds of queries you use over the network. The results will include every object that matches the query that's been pinned to your device. The query even takes into account any changes you've made to the object that haven't yet been saved to the cloud. For example, if you call deleteEventually, on an object, it will no longer be returned from these queries.
Caching Queries
It's often useful to cache the result of a query on a device. This lets you show data when the user's device is offline, or when the app has just started and network requests have not yet had time to complete. The easiest way to do this is with the local datastore. When you pin objects, you can attach a label to the pin, which lets you manage a group of objects together. For example, to cache the results of the query above, you can call pinAllInBackground and give it a label.
final String TOP_SCORES_LABEL = "topScores";
// Query for the latest objects from Parse.
query.findInBackground(new FindCallback<ParseObject>() {
public void done(final List<ParseObject> scoreList, ParseException e) {
if (e != null) {
// There was an error or the network wasn't available.
return;
}
// Release any objects previously pinned for this query.
ParseObject.unpinAllInBackground(TOP_SCORES_LABEL, scoreList, new DeleteCallback() {
public void done(ParseException e) {
if (e != null) {
// There was some error.
return;
}
// Add the latest results for this query to the cache.
ParseObject.pinAllInBackground(TOP_SCORES_LABEL, scoreList);
}
});
}
});
Now when you do any query with fromLocalDatastore, these objects will be included in the results if they still match the query.
If you aren't using the local datastore, you can use the per-query cache for ParseQuery instead. The default query behavior doesn't use the cache, but you can enable caching with setCachePolicy. For example, to try the network and then fall back to cached data if the network is not available:
query.setCachePolicy(ParseQuery.CachePolicy.NETWORK_ELSE_CACHE);
query.findInBackground(new FindCallback<ParseObject>() {
public void done(List<ParseObject> scoreList, ParseException e) {
if (e == null) {
// Results were successfully found, looking first on the
// network and then on disk.
} else {
// The network was inaccessible and we have no cached data
// for this query.
}
}
});
Parse provides several different cache policies:
IGNORE_CACHE
The query does not load from the cache or save results to the cache. IGNORE_CACHE is the default cache policy.
CACHE_ONLY
The query only loads from the cache, ignoring the network. If there are no cached results, that causes a ParseException.
NETWORK_ONLY
The query does not load from the cache, but it will save results to the cache.
CACHE_ELSE_NETWORK
The query first tries to load from the cache, but if that fails, it loads results from the network. If neither cache nor network succeed, there is a ParseException.
NETWORK_ELSE_CACHE
The query first tries to load from the network, but if that fails, it loads results from the cache. If neither network nor cache succeed, there is a ParseException.
CACHE_THEN_NETWORK
The query first loads from the cache, then loads from the network. In this case, the FindCallback will actually be called twice - first with the cached results, then with the network results. This cache policy can only be used asynchronously with findInBackground.
If you need to control the cache's behavior, you can use methods provided in ParseQuery to interact with the cache. You can do the following operations on the cache:
Check to see if there is a cached result for the query with:
boolean isInCache = query.hasCachedResult();
Remove any cached results for a query with:
query.clearCachedResult();
Remove cached results for all queries with:
ParseQuery.clearAllCachedResults();
Control the maximum age of a cached result with:
query.setMaxCacheAge(TimeUnit.DAYS.toMillis(1));
Query caching also works with ParseQuery helpers including getFirst() and getInBackground().
Counting Objects
If you just need to count how many objects match a query, but you do not need to retrieve all the objects that match, you can use count instead of find. For example, to count how many games have been played by a particular player:
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Sean Plott");
query.countInBackground(new CountCallback() {
public void done(int count, ParseException e) {
if (e == null) {
// The count request succeeded. Log the count
Log.d("score", "Sean has played " + count + " games");
} else {
// The request failed
}
}
});
If you want to block the calling thread, you can also use the synchronous query.count() method.
For classes with over 1000 objects, count operations are limited by timeouts. They may routinely yield timeout errors or return results that are only approximately correct. Thus, it is preferable to architect your application to avoid this sort of count operation.
Compound Queries
If you want to find objects that match one of several queries, you can use ParseQuery.or method to construct a query that is an or of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do:
ParseQuery<ParseObject> lotsOfWins = ParseQuery.getQuery("Player");
lotsOfWins.whereGreaterThan(150);
ParseQuery<ParseObject> fewWins = ParseQuery.getQuery("Player");
fewWins.whereLessThan(5);
List<ParseQuery<ParseObject>> queries = new ArrayList<ParseQuery<ParseObject>>();
queries.add(lotsOfWins);
queries.add(fewWins);
ParseQuery<ParseObject> mainQuery = ParseQuery.or(queries);
mainQuery.findInBackground(new FindCallback<ParseObject>() {
public void done(List<ParseObject> results, ParseException e) {
// results has the list of players that win a lot or haven't won much.
}
});
You can add additional constraints to the newly created ParseQuery that act as an 'and' operator.
Note that we do not, however, support non-filtering constraints (e.g. setLimit, skip, orderBy..., include) in the subqueries of the compound query.
Subclasses
Parse is designed to get you up and running as quickly as possible. You can access all of your data using the ParseObject class and access any field with get(). In mature codebases, subclasses have many advantages, including terseness, extensibility, and support for autocomplete. Subclassing is completely optional, but can transform this code:
ParseObject shield = new ParseObject("Armor");
shield.put("displayName", "Wooden Shield");
shield.put("fireproof", false);
shield.put("rupees", 50);
Into this:
Armor shield = new Armor();
shield.setDisplayName("Wooden Shield");
shield.setFireproof(false);
shield.setRupees(50);
Subclassing ParseObject
To create a ParseObject subclass:
Declare a subclass which extends ParseObject.
Add a @ParseClassName annotation. Its value should be the string you would pass into the ParseObject constructor, and makes all future class name references unnecessary.
Ensure that your subclass has a public default (i.e. zero-argument) constructor. You must not modify any ParseObject fields in this constructor.
Call ParseObject.registerSubclass(YourClass.class) in your Application constructor before calling Parse.initialize().
The following code sucessfully implements and registers the Armor subclass of ParseObject:
// Armor.java
import com.parse.ParseObject;
import com.parse.ParseClassName;
@ParseClassName("Armor")
public class Armor extends ParseObject {
}
// App.java
import com.parse.Parse;
import android.app.Application;
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
ParseObject.registerSubclass(Armor.class);
Parse.initialize(this, PARSE_APPLICATION_ID, PARSE_CLIENT_KEY);
}
}
Accessors, Mutators, and Methods
Adding methods to your ParseObject subclass helps encapsulate logic about the class. You can keep all your logic about a subject in one place rather than using separate classes for business logic and storage/transmission logic.
You can add accessors and mutators for the fields of your ParseObject easily. Declare the getter and setter for the field as you normally would, but implement them in terms of get() and put(). The following example creates a displayName field in the Armor class:
// Armor.java
@ParseClassName("Armor")
public class Armor extends ParseObject {
public String getDisplayName() {
return getString("displayName");
}
public void setDisplayName(String value) {
put("displayName", value);
}
}
You can now access the displayName field using armor.getDisplayName() and assign to it using armor.setDisplayName("Wooden Sword"). This allows your IDE to provide autocompletion as you develop your app and allows typos to be caught at compile-time.
Accessors and mutators of various types can be easily defined in this manner using the various forms of get() such as getInt(), getParseFile(), or getMap().
If you need more complicated logic than simple field access, you can declare your own methods as well:
public void takeDamage(int amount) {
// Decrease the armor's durability and determine whether it has broken
increment("durability", -amount);
if (getDurability() < 0) {
setBroken(true);
}
}
Initializing Subclasses
You should create new instances of your subclasses using the constructors you have defined. Your subclass must define a public default constructor that does not modify fields of the ParseObject, which will be used throughout the Parse SDK to create strongly-typed instances of your subclass.
To create a reference to an existing object, use ParseObject.createWithoutData():
Armor armorReference = ParseObject.createWithoutData(Armor.class, armor.getObjectId());
Queries
You can get a query for objects of a particular subclass using the static method ParseQuery.getQuery(). The following example queries for armors that the user can afford:
ParseQuery<Armor> query = ParseQuery.getQuery(Armor.class);
query.whereLessThanOrEqualTo("rupees", ParseUser.getCurrentUser().get("rupees"));
query.findInBackground(new FindCallback<Armor>() {
@Override
public void done(List<Armor> results, ParseException e) {
for (Armor a : results) {
// ...
}
}
});
Files
The ParseFile
ParseFile lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular ParseObject. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes).
Getting started with ParseFile is easy. First, you'll need to have the data in byte[] form and then create a ParseFile with it. In this example, we'll just use a string:
byte[] data = "Working at Parse is great!".getBytes();
ParseFile file = new ParseFile("resume.txt", data);
Notice in this example that we give the file a name of resume.txt. There's two things to note here:
You don't need to worry about filename collisions. Each upload gets a unique identifier so there's no problem with uploading multiple files named resume.txt.
It's important that you give a name to the file that has a file extension. This lets Parse figure out the file type and handle it accordingly. So, if you're storing PNG images, make sure your filename ends with .png.
Next you'll want to save the file up to the cloud. As with ParseObject, there are many variants of the save method you can use depending on what sort of callback and error handling suits you.
file.saveInBackground();
Finally, after the save completes, you can associate a ParseFile onto a ParseObject just like any other piece of data:
ParseObject jobApplication = new ParseObject("JobApplication");
jobApplication.put("applicantName", "Joe Smith");
jobApplication.put("applicantResumeFile", file);
jobApplication.saveInBackground();
Retrieving it back involves calling one of the getData variants on the ParseObject. Here we retrieve the resume file off another JobApplication object:
ParseFile applicantResume = (ParseFile)anotherApplication.get("applicantResumeFile");
applicantResume.getDataInBackground(new GetDataCallback() {
public void done(byte[] data, ParseException e) {
if (e == null) {
// data has the bytes for the resume
} else {
// something went wrong
}
}
});
Just like on ParseObject, you will most likely want to use the background version of getData.
Progress
It's easy to get the progress of both uploads and downloads using ParseFile by passing a ProgressCallback to saveInBackground and getDataInBackground. For example:
byte[] data = "Working at Parse is great!".getBytes();
ParseFile file = new ParseFile("resume.txt", data);
file.saveInBackground(new SaveCallback() {
public void done(ParseException e) {
// Handle success or failure here ...
}
}, new ProgressCallback() {
public void done(Integer percentDone) {
// Update your progress spinner here. percentDone will be between 0 and 100.
}
});
You can delete files that are referenced by objects using the REST API. You will need to provide the master key in order to be allowed to delete a file.
If your files are not referenced by any object in your app, it is not possible to delete them through the REST API. You may request a cleanup of unused files in your app's Settings page. Keep in mind that doing so may break functionality which depended on accessing unreferenced files through their URL property. Files that are currently associated with an object will not be affected.
Analytics
Parse provides a number of hooks for you to get a glimpse into the ticking heart of your app. We understand that it's important to understand what your app is doing, how frequently, and when.
While this section will cover different ways to instrument your app to best take advantage of Parse's analytics backend, developers using Parse to store and retrieve data can already take advantage of metrics on Parse.
Without having to implement any client-side logic, you can view real-time graphs and breakdowns (by device type, Parse class name, or REST verb) of your API Requests in your app's dashboard and save these graph filters to quickly access just the data you're interested in.
App-Open / Push Analytics
Our initial analytics hook allows you to track your application being launched. By adding the following line to the onCreate method of your main Activity, you'll begin to collect data on when and how often your application is opened.
ParseAnalytics.trackAppOpened(getIntent());
Graphs and breakdowns of your statistics are accessible from your app's Dashboard.
Further analytics are available around push notification delivery and open rates. Take a look at the Tracking Pushes and App Opens subsection of our Push Guide for more detailed information on handling notification payloads and push-related callbacks.
Custom Analytics
ParseAnalytics also allows you to track free-form events, with a handful of String keys and values. These extra dimensions allow segmentation of your custom events via your app's Dashboard.
Say your app offers search functionality for apartment listings, and you want to track how often the feature is used, with some additional metadata.
Map<String, String> dimensions = new HashMap<String, String>();
// Define ranges to bucket data points into meaningful segments
dimensions.put("priceRange", "1000-1500");
// Did the user filter the query?
dimensions.put("source", "craigslist");
// Do searches happen more often on weekdays or weekends?
dimensions.put("dayType", "weekday");
// Send the dimensions to Parse along with the 'search' event
ParseAnalytics.trackEvent("search", dimensions);
ParseAnalytics can even be used as a lightweight error tracker — simply invoke the following and you'll have access to an overview of the rate and frequency of errors, broken down by error code, in your application:
Map<String, String> dimensions = new HashMap<String, String>();
dimensions.put('code', Integer.toString(error.getCode()));
ParseAnalytics.trackEvent('error', dimensions);
Note that Parse currently only stores the first eight dimension pairs per call to ParseAnalytics.trackEvent().
Config
Parse Config
ParseConfig is a way to configure your applications remotely by storing a single configuration object on Parse. It enables you to add things like feature gating or a simple "Message of the Day". To start using ParseConfig you need to add a few key/value pairs (parameters) to your app on the Parse Config Dashboard.
After that you will be able to fetch the ParseConfig on the client, like in this example:
ParseConfig.getInBackground(new ConfigCallback() {
@Override
public void done(ParseConfig config, ParseException e) {
int number = config.getInt("winningNumber");
Log.d("TAG", String.format("Yay! The number is %d!", number));
}
});
Retrieving Config
ParseConfig is built to be as robust and reliable as possible, even in the face of poor internet connections. Caching is used by default to ensure that the latest successfully fetched config is always available. In the below example we use getInBackground to retrieve the latest version of config from the server, and if the fetch fails we can simply fall back to the version that we successfully fetched before via getCurrentConfig.
Log.d("TAG", "Getting the latest config...");
ParseConfig.getInBackground(new ConfigCallback() {
@Override
public void done(ParseConfig config, ParseException e) {
if (e == null) {
Log.d("TAG", "Yay! Config was fetched from the server.");
} else {
Log.e("TAG", "Failed to fetch. Using Cached Config.");
config = ParseConfig.getCurrentConfig();
}
// Get the message from config or fallback to default value
String welcomeMessage = config.getString("welcomeMessage", "Welcome!");
Log.d("TAG", String.format("Welcome Messsage From Config = %s", welcomeMessage));
}
});
No comments:
Post a Comment