Getting Started

If you haven’t installed the SDK yet, please head over to the QuickStart guide to get our SDK up and running in Android Studio. Note that we support Android 2.3 and higher. You can also check out our API Reference for more detailed information about our SDK.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Objects

The ParseObject

Storing data on Parse is built around the ParseObject. Each ParseObject contains key-value pairs of JSON-compatible data. This data is schemaless, which means that you don’t need to specify ahead of time what keys exist on each ParseObject. You simply set whatever key-value pairs you want, and our backend will store it.

For example, let’s say you’re tracking high scores for a game. A single ParseObject could contain:

score: 1337, playerName: "Sean Plott", cheatMode: false

Keys must be alphanumeric strings. Values can be strings, numbers, booleans, or even arrays and objects - anything that can be JSON-encoded.

Each ParseObject has a class name that you can use to distinguish different sorts of data. For example, we could call the high score object a GameScore. We recommend that you NameYourClassesLikeThis and nameYourKeysLikeThis, just to keep your code looking pretty.

Saving Objects

Let’s say you want to save the GameScore described above to the Parse Cloud. The interface is similar to a Map, plus the saveInBackground method:

ParseObject gameScore = new ParseObject("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.saveInBackground();

After this code runs, you will probably be wondering if anything really happened. To make sure the data was saved, you can look at the Data Browser in your app on Parse. You should see something like this:

objectId: "xWMyZ4YEGZ", score: 1337, playerName: "Sean Plott", cheatMode: false,
createdAt:"2011-06-10T18:33:42Z", updatedAt:"2011-06-10T18:33:42Z"

There are two things to note here. You didn’t have to configure or set up a new Class called GameScore before running this code. Your Parse app lazily creates this Class for you when it first encounters it.

There are also a few fields you don’t need to specify that are provided as a convenience. objectId is a unique identifier for each saved object. createdAt and updatedAt represent the time that each object was created and last modified in the cloud. Each of these fields is filled in by Parse, so they don’t exist on a ParseObject until a save operation has completed.

Retrieving Objects

Saving data to the cloud is fun, but it’s even more fun to get that data out again. If you have the objectId, you can retrieve the whole ParseObject using a ParseQuery:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.getInBackground("xWMyZ4YEGZ", new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      // object will be your game score
    } else {
      // something went wrong
    }
  }
});

To get the values out of the ParseObject, there’s a getX method for each data type:

int score = gameScore.getInt("score");
String playerName = gameScore.getString("playerName");
boolean cheatMode = gameScore.getBoolean("cheatMode");

If you don’t know what type of data you’re getting out, you can call get(key), but then you probably have to cast it right away anyways. In most situations you should use the typed accessors like getString.

The three special values have their own accessors:

String objectId = gameScore.getObjectId();
Date updatedAt = gameScore.getUpdatedAt();
Date createdAt = gameScore.getCreatedAt();

If you need to refresh an object you already have with the latest data that is in the cloud, you can call the fetchInBackground method like so:

myObject.fetchInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      // Success!
    } else {
      // Failure!
    }
  }
});

The code in the GetCallback will be run on the main thread.

The Local Datastore

Parse also lets you store objects in a local datastore on the Android device itself. You can use this for data that doesn’t need to be saved to the cloud, but this is especially useful for temporarily storing data so that it can be synced later. To enable the datastore, call Parse.enableLocalDatastore() in your Application constructor before calling Parse.initialize(). Once the local datastore is enabled, you can store an object by pinning it.

ParseObject gameScore = new ParseObject("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.pinInBackground();

As with saving, this recursively stores every object and file that gameScore points to, if it has been fetched from the cloud. Whenever you save changes to the object, or fetch new changes from Parse, the copy in the datastore will be automatically updated, so you don’t have to worry about it.

Retrieving Objects from the Local Datastore

Storing an object is only useful if you can get it back out. To get the data for a specific object, you can use a ParseQuery just like you would while on the network, but using the fromLocalDatastore method to tell it where to get the data.

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.fromLocalDatastore();
query.getInBackground("xWMyZ4YEGZ", new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      // object will be your game score
    } else {
      // something went wrong
    }
  }
});

If you already have an instance of the object, you can instead use the fetchFromLocalDatastoreInBackground method.

ParseObject object = ParseObject.createWithoutData("GameScore", "xWMyZ4YEGZ");
object.fetchFromLocalDatastoreInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      // object will be your game score
    } else {
      // something went wrong
    }
  }
});

Unpinning Objects

When you are done with the object and no longer need to keep it on the device, you can release it with unpinInBackground.

gameScore.unpinInBackground();

Saving Objects Offline

Most save functions execute immediately, and inform your app when the save is complete. If you don’t need to know when the save has finished, you can use saveEventually instead. The advantage is that if the user currently doesn’t have a network connection, saveEventually will store the update on the device until a network connection is re-established. If your app is closed before the connection is back, Parse will try again the next time the app is opened. All calls to saveEventually (and deleteEventually) are executed in the order they are called, so it is safe to call saveEventually on an object multiple times. If you have the local datastore enabled, then any object you saveEventually will be pinned as long as that save is in progress. That makes it easy to retrieve your local changes while waiting for the network to be available.

ParseObject gameScore = new ParseObject("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.saveEventually();

Updating Objects

Updating an object is simple. Just set some new data on it and call one of the save methods. Assuming you have saved the object and have the objectId, you can retrieve the ParseObject using a ParseQuery and update its data:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");

// Retrieve the object by id
query.getInBackground("xWMyZ4YEGZ", new GetCallback<ParseObject>() {
  public void done(ParseObject gameScore, ParseException e) {
    if (e == null) {
      // Now let's update it with some new data. In this case, only cheatMode and score
      // will get sent to the Parse Cloud. playerName hasn't changed.
      gameScore.put("score", 1338);
      gameScore.put("cheatMode", true);
      gameScore.saveInBackground();
    }
  }
});

Parse automatically figures out which data has changed so only “dirty” fields will be transmitted during a save. You don’t need to worry about squashing data in the cloud that you didn’t intend to update.

Counters

The above example contains a common use case. The “score” field is a counter that we’ll need to continually update with the player’s latest score. Using the above method works but it’s cumbersome and can lead to problems if you have multiple clients trying to update the same counter.

To help with storing counter-type data, Parse provides methods that atomically increment (or decrement) any number field. So, the same update can be rewritten as:

gameScore.increment("score");
gameScore.saveInBackground();

You can also increment by any amount using increment(key, amount).

Arrays

To help with storing array data, there are three operations that can be used to atomically change an array field:

  • add and addAll append the given objects to the end of an array field.
  • addUnique and addAllUnique add only the given objects which aren’t already contained in an array field to that field. The position of the insert is not guaranteed.
  • removeAll removes all instances of the given objects from an array field.

For example, we can add items to the set-like “skills” field like so:

gameScore.addAllUnique("skills", Arrays.asList("flying", "kungfu"));
gameScore.saveInBackground();

Note that it is not currently possible to atomically add and remove items from an array in the same save. You will have to call save in between every different kind of array operation.

Deleting Objects

To delete an object from the Parse Cloud:

myObject.deleteInBackground();

If you want to run a callback when the delete is confirmed, you can provide a DeleteCallback to the deleteInBackground method. If you want to block the calling thread, you can use the delete method.

You can delete a single field from an object with the remove method:

// After this, the playerName field will be empty
myObject.remove("playerName");

// Saves the field deletion to the Parse Cloud
myObject.saveInBackground();

Relational Data

Objects can have relationships with other objects. To model this behavior, any ParseObject can be used as a value in other ParseObjects. Internally, the Parse framework will store the referred-to object in just one place, to maintain consistency.

For example, each Comment in a blogging app might correspond to one Post. To create a new Post with a single Comment, you could write:

// Create the post
ParseObject myPost = new ParseObject("Post");
myPost.put("title", "I'm Hungry");
myPost.put("content", "Where should we go for lunch?");

// Create the comment
ParseObject myComment = new ParseObject("Comment");
myComment.put("content", "Let's do Sushirrito.");

// Add a relation between the Post and Comment
myComment.put("parent", myPost);

// This will save both myPost and myComment
myComment.saveInBackground();

You can also link objects using just their objectIds like so:

// Add a relation between the Post with objectId "1zEcyElZ80" and the comment
myComment.put("parent", ParseObject.createWithoutData("Post", "1zEcyElZ80"));

By default, when fetching an object, related ParseObjects are not fetched. These objects’ values cannot be retrieved until they have been fetched like so:

fetchedComment.getParseObject("post")
    .fetchIfNeededInBackground(new GetCallback<ParseObject>() {
        public void done(ParseObject post, ParseException e) {
          String title = post.getString("title");
          // Do something with your new title variable
        }
    });

You can also model a many-to-many relation using the ParseRelation object. This works similar to List<ParseObject>, except that you don’t need to download all the ParseObjects in a relation at once. This allows ParseRelation to scale to many more objects than the List<ParseObject> approach. For example, a User may have many Posts that they might like. In this case, you can store the set of Posts that a User likes using getRelation. In order to add a post to the list, the code would look something like:

ParseUser user = ParseUser.getCurrentUser();
ParseRelation<ParseObject> relation = user.getRelation("likes");
relation.add(post);
user.saveInBackground();

You can remove a post from the ParseRelation with something like:

relation.remove(post);

By default, the list of objects in this relation are not downloaded. You can get the list of Posts by calling findInBackground on the ParseQuery returned by getQuery. The code would look like:

relation.getQuery().findInBackground(new FindCallback<ParseObject>() {
    void done(List<ParseObject> results, ParseException e) {
      if (e != null) {
        // There was an error
      } else {
        // results have all the Posts the current user liked.
      }
    }
});

If you want only a subset of the Posts you can add extra constraints to the ParseQuery returned by getQuery. The code would look something like:

ParseQuery<ParseObject> query = relation.getQuery();
// Add other query constraints.

For more details on ParseQuery, please look at the query portion of this guide. A ParseRelation behaves similar to a List<ParseObject> for querying purposes, so any queries you can do on lists of objects (other than include) you can do on ParseRelation.

Data Types

So far we’ve used values with type String, Integer, bool, and ParseObject. Parse also supports float, java.util.Date, and JSONObject.NULL.

You can nest JSONObject and JSONArray objects to store more structured data within a single ParseObject. Overall, the following types are allowed for each field in your object:

  • String => String
  • Number => primitive numeric values such as ints, doubles, longs, or floats
  • Bool => bool
  • Array => JSONArray
  • Object => JSONObject
  • Date => java.util.Date
  • File => ParseFile
  • Pointer => other ParseObject
  • Relation => ParseRelation
  • Null => JSONObject.NULL

Some examples:

int myNumber = 42;
String myString = "the number is " + myNumber;
Date myDate = new Date();

JSONArray myArray = new JSONArray();
myArray.put(myString);
myArray.put(myNumber);

JSONObject myObject = new JSONObject();
myObject.put("number", myNumber);
myObject.put("string", myString);

ParseObject bigObject = new ParseObject("BigObject");
bigObject.put("myNumber", myNumber);
bigObject.put("myString", myString);
bigObject.put("myDate", myDate);
bigObject.put("myArray", myArray);
bigObject.put("myObject", myObject);
bigObject.put("myNull", JSONObject.NULL);
bigObject.saveInBackground();

We do not recommend storing large pieces of binary data like images or documents on ParseObject. ParseObjects should not exceed 128 kilobytes in size. We recommend you use ParseFiles to store images, documents, and other types of files. You can do so by instantiating a ParseFile object and setting it on a field. See Files for more details.

For more information about how Parse handles data, check out our documentation on Data.

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:

  1. Declare a subclass which extends ParseObject.
  2. 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.
  3. Ensure that your subclass has a public default (i.e. zero-argument) constructor. You must not modify any ParseObject fields in this constructor.
  4. 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) {
      // ...
    }
  }
});
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Queries

We’ve already seen how a ParseQuery with getInBackground can retrieve a single ParseObject from Parse. There are many other ways to retrieve data with ParseQuery - you can retrieve many objects at once, put conditions on the objects you wish to retrieve, cache queries automatically to avoid writing that code yourself, and more.

Basic Queries

In many cases, getInBackground isn’t powerful enough to specify which objects you want to retrieve. The ParseQuery offers different ways to retrieve a list of objects rather than just a single object.

The general pattern is to create a ParseQuery, put conditions on it, and then retrieve a List of matching ParseObjects using the findInBackground method with a FindCallback. For example, to retrieve scores with a particular playerName, use the whereEqualTo method to constrain the value for a key:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Dan Stemkoski");
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> scoreList, ParseException e) {
        if (e == null) {
            Log.d("score", "Retrieved " + scoreList.size() + " scores");
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});

findInBackground works similarly to getInBackground in that it assures the network request is done on a background thread, and runs its callback in the main thread.

Query Constraints

There are several ways to put constraints on the objects found by a ParseQuery. You can filter out objects with a particular key-value pair with whereNotEqualTo:

query.whereNotEqualTo("playerName", "Michael Yabuti");

You can give multiple constraints, and objects will only be in the results if they match all of the constraints. In other words, it’s like an AND of constraints.

query.whereNotEqualTo("playerName", "Michael Yabuti");
query.whereGreaterThan("playerAge", 18);

You can limit the number of results with setLimit. By default, results are limited to 100, but anything from 1 to 1000 is a valid limit:

query.setLimit(10); // limit to at most 10 results

If you want exactly one result, a more convenient alternative may be to use getFirst or getFirstBackground instead of using find.

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerEmail", "dstemkoski@example.com");
query.getFirstInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (object == null) {
      Log.d("score", "The getFirst request failed.");
    } else {
      Log.d("score", "Retrieved the object.");
    }
  }
});

You can skip the first results with setSkip. This can be useful for pagination:

query.setSkip(10); // skip the first 10 results

For sortable types like numbers and strings, you can control the order in which results are returned:

// Sorts the results in ascending order by the score field
query.orderByAscending("score");

// Sorts the results in descending order by the score field
query.orderByDescending("score");

You can add more sort keys to the query as follows:

// Sorts the results in ascending order by the score field if the previous sort keys are equal.
query.addAscendingOrder("score");

// Sorts the results in descending order by the score field if the previous sort keys are equal.
query.addDescendingOrder("score");

For sortable types, you can also use comparisons in queries:

// Restricts to wins < 50
query.whereLessThan("wins", 50);

// Restricts to wins <= 50
query.whereLessThanOrEqualTo("wins", 50);

// Restricts to wins > 50
query.whereGreaterThan("wins", 50);

// Restricts to wins >= 50
query.whereGreaterThanOrEqualTo("wins", 50);

If you want to retrieve objects matching several different values, you can use whereContainedIn, providing a collection of acceptable values. This is often useful to replace multiple queries with a single query. For example, if you want to retrieve scores made by any player in a particular list:

String[] names = {"Jonathan Walsh", "Dario Wunsch", "Shawn Simon"};
query.whereContainedIn("playerName", Arrays.asList(names));

If you want to retrieve objects that do not match any of several values you can use whereNotContainedIn, providing an array of acceptable values. For example, if you want to retrieve scores from players besides those in a list:

String[] names = {"Jonathan Walsh", "Dario Wunsch", "Shawn Simon"};
query.whereNotContainedIn("playerName", Arrays.asList(names));

If you want to retrieve objects that have a particular key set, you can use whereExists. Conversely, if you want to retrieve objects without a particular key set, you can use whereDoesNotExist.

// Finds objects that have the score set
query.whereExists("score");

// Finds objects that don't have the score set
query.whereDoesNotExist("score");

You can use the whereMatchesKeyInQuery method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:

ParseQuery<ParseObject> teamQuery = ParseQuery.getQuery("Team");
teamQuery.whereGreaterThan("winPct", 0.5);
ParseQuery<ParseUser> userQuery = ParseUser.getQuery();
userQuery.whereMatchesKeyInQuery("hometown", "city", teamQuery);
userQuery.findInBackground(new FindCallback<ParseUser>() {
  void done(List<ParseUser> results, ParseException e) {
    // results has the list of users with a hometown team with a winning record
  }
});

Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use whereDoesNotMatchKeyInQuery. For example, to find users whose hometown teams have losing records:

ParseQuery<ParseUser> losingUserQuery = ParseUser.getQuery();
losingUserQuery.whereDoesNotMatchKeyInQuery("hometown", "city", teamQuery);
losingUserQuery.findInBackground(new FindCallback<ParseUser>() {
  void done(List<ParseUser> results, ParseException e) {
    // results has the list of users with a hometown team with a losing record
  }
});

You can restrict the fields returned by calling selectKeys with a collection of keys. To retrieve documents that contain only the score and playerName fields (and also special built-in fields such as objectId, createdAt, and updatedAt):

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.selectKeys(Arrays.asList("playerName", "score"));;
List<ParseObject> results = query.find();

The remaining fields can be fetched later by calling one of the fetchIfNeeded variants on the returned objects:

ParseObject object = results.get(0);
object.fetchIfNeededInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    // all fields of the object will now be available here.
  }
});

Queries on Array Values

If a key contains an array value, you can search for objects where the key’s array value contains 2 by:

// Find objects where the array in arrayKey contains the number 2.
query.whereEqualTo("arrayKey", 2);

You can also search for objects where the key’s array value contains each of the values 2, 3, and 4 with the following:

// Find objects where the array in arrayKey contains all of the numbers 2, 3, and 4.
ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(2);
numbers.add(3);
numbers.add(4);
query.whereContainsAll("arrayKey", numbers);

Queries on String Values

If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend.

Use whereStartsWith to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets:

// Finds barbecue sauces that start with 'Big Daddy's'.
ParseQuery<ParseObject> query = ParseQuery.getQuery("BarbecueSauce");
query.whereStartsWith("name", "Big Daddy's");

The above example will match any BarbecueSauce objects where the value in the “name” String key starts with “Big Daddy’s”. For example, both “Big Daddy’s” and “Big Daddy’s BBQ” will match, but “big daddy’s” or “BBQ Sauce: Big Daddy’s” will not.

Queries that have regular expression constraints are very expensive. Refer to the Performance Guide for more details.

Relational Queries

There are several ways to issue queries for relational data. If you want to retrieve objects where a field matches a particular ParseObject, you can use whereEqualTo just like for other data types. For example, if each Comment has a Post object in its post field, you can fetch comments for a particular Post:

// Assume ParseObject myPost was previously created.
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereEqualTo("post", myPost);

query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // commentList now has the comments for myPost
  }
});

If you want to retrieve objects where a field contains a ParseObject that matches a different query, you can use whereMatchesQuery. Note that the default limit of 100 and maximum limit of 1000 apply to the inner query as well, so with large data sets you may need to construct queries carefully to get the desired behavior. In order to find comments for posts containing images, you can do:

ParseQuery<ParseObject> innerQuery = ParseQuery.getQuery("Post");
innerQuery.whereExists("image");
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereMatchesQuery("post", innerQuery);
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // comments now contains the comments for posts with images.
  }
});

If you want to retrieve objects where a field contains a ParseObject that does not match a different query, you can use whereDoesNotMatchQuery. In order to find comments for posts without images, you can do:

ParseQuery<ParseObject> innerQuery = ParseQuery.getQuery("Post");
innerQuery.whereExists("image");
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereDoesNotMatchQuery("post", innerQuery);
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // comments now contains the comments for posts without images.
  }
});

In some situations, you want to return multiple types of related objects in one query. You can do this with the include method. For example, let’s say you are retrieving the last ten comments, and you want to retrieve their related posts at the same time:

ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");

// Retrieve the most recent ones
query.orderByDescending("createdAt");

// Only retrieve the last ten
query.setLimit(10);

// Include the post data with each comment
query.include("post");

query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // commentList now contains the last ten comments, and the "post"
    // field has been populated. For example:
    for (ParseObject comment : commentList) {
      // This does not require a network access.
      ParseObject post = comment.getParseObject("post");
      Log.d("post", "retrieved a related post");
    }
  }
});

You can also do multi level includes using dot notation. If you wanted to include the post for a comment and the post’s author as well you can do:

query.include("post.author");

You can issue a query with multiple fields included by calling include multiple times. This functionality also works with ParseQuery helpers like getFirst() and getInBackground().

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(final 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: java boolean isInCache = query.hasCachedResult();
  • Remove any cached results for a query with: java query.clearCachedResult();
  • Remove cached results for all queries with: java ParseQuery.clearAllCachedResults();

Query caching also works with ParseQuery helpers including getFirst() and getInBackground().

Counting Objects

Caveat: Count queries are rate limited to a maximum of 160 requests per minute. They can also return inaccurate results for classes with more than 1,000 objects. Thus, it is preferable to architect your application to avoid this sort of count operation (by using counters, for example.)

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.

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 GeoPoint or non-filtering constraints (e.g. whereNear, withinGeoBox, setLimit, skip, orderBy..., include) in the subqueries of the compound query.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Users

At the core of many apps, there is a notion of user accounts that lets users access their information in a secure manner. We provide a specialized user class called ParseUser that automatically handles much of the functionality required for user account management.

With this class, you’ll be able to add user account functionality in your app.

ParseUser is a subclass of the ParseObject, and has all the same features, such as flexible schema, automatic persistence, and a key value interface. All the methods that are on ParseObject also exist in ParseUser. The difference is that ParseUser has some special additions specific to user accounts.

Properties

ParseUser has several properties that set it apart from ParseObject:

  • username: The username for the user (required).
  • password: The password for the user (required on signup).
  • email: The email address for the user (optional).

We’ll go through each of these in detail as we run through the various use cases for users. Keep in mind that if you set username and email using the setters, you do not need to set it using the put method.

Signing Up

The first thing your app will do is probably ask the user to sign up. The following code illustrates a typical sign up:

ParseUser user = new ParseUser();
user.setUsername("my name");
user.setPassword("my pass");
user.setEmail("email@example.com");

// other fields can be set just like with ParseObject
user.put("phone", "650-253-0000");

user.signUpInBackground(new SignUpCallback() {
  public void done(ParseException e) {
    if (e == null) {
      // Hooray! Let them use the app now.
    } else {
      // Sign up didn't succeed. Look at the ParseException
      // to figure out what went wrong
    }
  }
});

This call will asynchronously create a new user in your Parse App. Before it does this, it checks to make sure that both the username and email are unique. Also, it securely hashes the password in the cloud using bcrypt. We never store passwords in plaintext, nor will we ever transmit passwords back to the client in plaintext.

Note that we used the signUpInBackground method, not the saveInBackground method. New ParseUsers should always be created using the signUpInBackground (or signUp) method. Subsequent updates to a user can be done by calling save.

The signUpInBackground method comes in various flavors, with the ability to pass back errors, and also synchronous versions. As usual, we highly recommend using the asynchronous versions when possible, so as not to block the UI in your app. You can read more about these specific methods in our API docs.

If a signup isn’t successful, you should read the error object that is returned. The most likely case is that the username or email has already been taken by another user. You should clearly communicate this to your users, and ask them try a different username.

You are free to use an email address as the username. Simply ask your users to enter their email, but fill it in the username property — ParseUser will work as normal. We’ll go over how this is handled in the reset password section.

Logging In

Of course, after you allow users to sign up, you need be able to let them log in to their account in the future. To do this, you can use the class method logInInBackground.

ParseUser.logInInBackground("Jerry", "showmethemoney", new LogInCallback() {
  public void done(ParseUser user, ParseException e) {
    if (user != null) {
      // Hooray! The user is logged in.
    } else {
      // Signup failed. Look at the ParseException to see what happened.
    }
  }
});

Verifying Emails

Enabling email verification in an application’s settings allows the application to reserve part of its experience for users with confirmed email addresses. Email verification adds the emailVerified key to the ParseUser object. When a ParseUser’s email is set or modified, emailVerified is set to false. Parse then emails the user a link which will set emailVerified to true.

There are three emailVerified states to consider:

  1. true - the user confirmed his or her email address by clicking on the link Parse emailed them. ParseUsers can never have a true value when the user account is first created.
  2. false - at the time the ParseUser object was last fetched, the user had not confirmed his or her email address. If emailVerified is false, consider calling fetch() on the ParseUser.
  3. missing - the ParseUser was created when email verification was off or the ParseUser does not have an email.

Current User

It would be bothersome if the user had to log in every time they open your app. You can avoid this by using the cached currentUser object.

Whenever you use any signup or login methods, the user is cached on disk. You can treat this cache as a session, and automatically assume the user is logged in:

ParseUser currentUser = ParseUser.getCurrentUser();
if (currentUser != null) {
  // do stuff with the user
} else {
  // show the signup or login screen
}

You can clear the current user by logging them out:

ParseUser.logOut();
ParseUser currentUser = ParseUser.getCurrentUser(); // this will now be null

Anonymous Users

Being able to associate data and objects with individual users is highly valuable, but sometimes you want to be able to do this without forcing a user to specify a username and password.

An anonymous user is a user that can be created without a username and password but still has all of the same capabilities as any other ParseUser. After logging out, an anonymous user is abandoned, and its data is no longer accessible.

You can create an anonymous user using ParseAnonymousUtils:

ParseAnonymousUtils.logIn(new LogInCallback() {
  @Override
  public void done(ParseUser user, ParseException e) {
    if (e != null) {
      Log.d("MyApp", "Anonymous login failed.");
    } else {
      Log.d("MyApp", "Anonymous user logged in.");
    }
  }
});

You can convert an anonymous user into a regular user by setting the username and password, then calling signUp(), or by logging in or linking with a service like Facebook or Twitter. The converted user will retain all of its data. To determine whether the current user is an anonymous user, you can check ParseAnonymousUtils.isLinked():

if (ParseAnonymousUtils.isLinked(ParseUser.getCurrentUser())) {
  enableSignUpButton();
} else {
  enableLogOutButton();
}

Anonymous users can also be automatically created for you without requiring a network request, so that you can begin working with your user immediately when your application starts. When you enable automatic anonymous user creation at application startup, ParseUser.getCurrentUser() will never be null. The user will automatically be created in the cloud the first time the user or any object with a relation to the user is saved. Until that point, the user’s object ID will be null. Enabling automatic user creation makes associating data with your users painless. For example, in your Application.onCreate() method, you might write:

ParseUser.enableAutomaticUser();
ParseUser.getCurrentUser().increment("RunCount");
ParseUser.getCurrentUser().saveInBackground();

Setting the Current User

If you’ve created your own authentication routines, or otherwise logged in a user on the server side, you can now pass the session token to the client and use the become method. This method will ensure the session token is valid before setting the current user.

ParseUser.becomeInBackground("session-token-here", new LogInCallback() {
  public void done(ParseUser user, ParseException e) {
    if (user != null) {
      // The current user is now set to user.
    } else {
      // The token could not be validated.
    }
  }
});

Security For User Objects

The ParseUser class is secured by default. Data stored in a ParseUser can only be modified by that user. By default, the data can still be read by any client. Thus, some ParseUser objects are authenticated and can be modified, whereas others are read-only.

Specifically, you are not able to invoke any of the save or delete type methods unless the ParseUser was obtained using an authenticated method, like logIn or signUp. This ensures that only the user can alter their own data.

The following illustrates this security policy:

ParseUser user = ParseUser.logIn("my_username", "my_password");
user.setUsername("my_new_username"); // attempt to change username
user.saveInBackground(); // This succeeds, since the user was authenticated on the device

// Get the user from a non-authenticated manner
ParseQuery<ParseUser> query = ParseUser.getQuery();
query.getInBackground(user.getObjectId(), new GetCallback<ParseUser>() {
  public void done(ParseUser object, ParseException e) {
    object.setUsername("another_username");

    // This will throw an exception, since the ParseUser is not authenticated
    object.saveInBackground();
  }
});

The ParseUser obtained from getCurrentUser() will always be authenticated.

If you need to check if a ParseUser is authenticated, you can invoke the isAuthenticated() method. You do not need to check isAuthenticated() with ParseUser objects that are obtained via an authenticated method.

Security for Other Objects

The same security model that applies to the ParseUser can be applied to other objects. For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. To support this type of security, each object has an access control list, implemented by the ParseACL class.

The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that limits access to that user. An object’s ACL is updated when the object is saved, like any other property. Thus, to create a private note that can only be accessed by the current user:

ParseObject privateNote = new ParseObject("Note");
privateNote.put("content", "This note is private!");
privateNote.setACL(new ParseACL(ParseUser.getCurrentUser()));
privateNote.saveInBackground();

This note will then only be accessible to the current user, although it will be accessible to any device where that user is signed in. This functionality is useful for applications where you want to enable access to user data across multiple devices, like a personal todo list.

Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using setReadAccess and setWriteAccess. For example, let’s say you have a message that will be sent to a group of several users, where each of them have the rights to read and delete that message:

ParseObject groupMessage = new ParseObject("Message");
ParseACL groupACL = new ParseACL();

// userList is an Iterable<ParseUser> with the users we are sending this message to.
for (ParseUser user : userList) {
  groupACL.setReadAccess(user, true);
  groupACL.setWriteAccess(user, true);
}

groupMessage.setACL(groupACL);
groupMessage.saveInBackground();

You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess. This allows patterns like posting comments on a message board. For example, to create a post that can only be edited by its author, but can be read by anyone:

ParseObject publicPost = new ParseObject("Post");
ParseACL postACL = new ParseACL(ParseUser.getCurrentUser());
postACL.setPublicReadAccess(true);
publicPost.setACL(postACL);
publicPost.saveInBackground();

To help ensure that your users’ data is secure by default, you can set a default ACL to be applied to all newly-created ParseObjects:

ParseACL.setDefaultACL(defaultACL, true);

In the code above, the second parameter to setDefaultACL tells Parse to ensure that the default ACL assigned at the time of object creation allows read and write access to the current user at that time. Without this setting, you would need to reset the defaultACL every time a user logs in or out so that the current user would be granted access appropriately. With this setting, you can ignore changes to the current user until you explicitly need to grant different kinds of access.

Default ACLs make it easy to create apps that follow common access patterns. An application like Twitter, for example, where user content is generally visible to the world, might set a default ACL such as:

ParseACL defaultACL = new ParseACL();
defaultACL.setPublicReadAccess(true);
ParseACL.setDefaultACL(defaultACL, true);

For an application like Dropbox, where a user’s data is only accessible by the user itself unless explicit permission is given, you would provide a default ACL where only the current user is given access:

ParseACL.setDefaultACL(new ParseACL(), true);

An application that logs data to Parse but doesn’t provide any user access to that data would instead deny access to the current user while providing a restrictive ACL:

ParseACL.setDefaultACL(new ParseACL(), false);

Operations that are forbidden, such as deleting an object that you do not have write access to, result in a ParseException.OBJECT_NOT_FOUND error code. For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which object ids do not exist at all.

Resetting Passwords

It’s a fact that as soon as you introduce passwords into a system, users will forget them. In such cases, our library provides a way to let them securely reset their password.

To kick off the password reset flow, ask the user for their email address, and call:

ParseUser.requestPasswordResetInBackground("myemail@example.com", new RequestPasswordResetCallback() {
  public void done(ParseException e) {
    if (e == null) {
      // An email was successfully sent with reset instructions.
    } else {
      // Something went wrong. Look at the ParseException to see what's up.
    }
  }
});

This will attempt to match the given email with the user’s email or username field, and will send them a password reset email. By doing this, you can opt to have users use their email as their username, or you can collect it separately and store it in the email field.

The flow for password reset is as follows:

  1. User requests that their password be reset by typing in their email.
  2. Parse sends an email to their address, with a special password reset link.
  3. User clicks on the reset link, and is directed to a special Parse page that will allow them type in a new password.
  4. User types in a new password. Their password has now been reset to a value they specify.

Note that the messaging in this flow will reference your app by the name that you specified when you created this app on Parse.

Querying

To query for users, you need to use the special user query:

ParseQuery<ParseUser> query = ParseUser.getQuery();
query.whereEqualTo("gender", "female");
query.findInBackground(new FindCallback<ParseUser>() {
  public void done(List<ParseUser> objects, ParseException e) {
    if (e == null) {
        // The query was successful.
    } else {
        // Something went wrong.
    }
  }
});

In addition, you can use get to get a ParseUser by id.

Associations

Associations involving a ParseUser work right of the box. For example, let’s say you’re making a blogging app. To store a new post for a user and retrieve all their posts:

ParseUser user = ParseUser.getCurrentUser();

// Make a new post
ParseObject post = new ParseObject("Post");
post.put("title", "My New Post");
post.put("body", "This is some great content.");
post.put("user", user);
post.saveInBackground();

// Find all posts by the current user
ParseQuery<ParseObject> query = ParseQuery.getQuery("Post");
query.whereEqualTo("user", user);
query.findInBackground(new FindCallback<ParseObject>() { ... });

Facebook Users

Parse provides an easy way to integrate Facebook with your application. The Facebook SDK can be used with our SDK, and is integrated with the ParseUser class to make linking your users to their Facebook identities easy.

Using our Facebook integration, you can associate an authenticated Facebook user with a ParseUser. With just a few lines of code, you’ll be able to provide a “Log in with Facebook” option in your app, and be able to save their data to Parse.

Note: Parse is compatible with both Facebook SDK 3.x and 4.x for Android. These instructions are for Facebook SDK 4.x.

Setup

To start using Facebook with Parse, you need to:

  1. Set up a Facebook app, if you haven’t already.
  2. Add your application’s Facebook Application ID on your Parse application’s settings page.
  3. Follow Facebook’s instructions for getting started with the Facebook SDK to create an app linked to the Facebook SDK. Once you get to Step 6, stop after linking the Facebook SDK project and configuring the Facebook app ID. You can use our guide to attach your Parse users to Facebook accounts when logging in.
  4. Add com.parse:parsefacebookutils-v4-android:1.10.3@aar to your Gradle dependencies. This includes the contents of the Parse-*.jar and the com.parse:parse-android:1.10.+ repository, so be sure to remove as needed to prevent duplicate dependencies, otherwise a com.android.dex.DexException will be thrown.
  5. Add the following where you initialize the Parse SDK in your Application.onCreate():

java ParseFacebookUtils.initialize(context);

Facebook’s Android SDK provides an enhanced login experience on devices that have Facebook’s official Android app installed. This allows users of apps that support Facebook login to sign in directly through the Facebook app, using credentials that are already on the device. If the Facebook app is not installed, the default dialog-based authentication will be used. Facebook calls this feature “Single sign-on” (SSO), and requires you to override onActivityResult() in your calling Activity:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  ParseFacebookUtils.onActivityResult(requestCode, resultCode, data);
}

If your Activity is already using onActivityResult(), you can avoid requestCode collisions by specifying your own request code offset when initializing ParseFacebookUtils.initialize(context, callbackRequestCodeOffset). Otherwise, a sensible default activityCode will be used.

If you encounter any issues that are Facebook-related, a good resource is the official Facebook SDK for Android page.

There are two main ways to use Facebook with your Parse users: (1) logging in as a Facebook user and creating a ParseUser, or (2) linking Facebook to an existing ParseUser.

It is up to you to record any data that you need from the Facebook user after they authenticate. To accomplish this, you'll need to do a graph query using the Facebook SDK.

Login & Signup

ParseFacebookUtils provides a way to allow your ParseUsers to log in or sign up through Facebook. This is generally accomplished using the logInWithReadPermissionsInBackground(String, Collection<String>) method:

ParseFacebookUtils.logInWithReadPermissionsInBackground(this, permissions, new LogInCallback() {
  @Override
  public void done(ParseUser user, ParseException err) {
    if (user == null) {
      Log.d("MyApp", "Uh oh. The user cancelled the Facebook login.");
    } else if (user.isNew()) {
      Log.d("MyApp", "User signed up and logged in through Facebook!");
    } else {
      Log.d("MyApp", "User logged in through Facebook!");
    }
  }
});

When this code is run, the following happens:

  1. The user is shown the Facebook login dialog or a prompt generated by the Facebook app.
  2. The user authenticates via Facebook, and your app receives a callback.
  3. Our SDK receives the user’s Facebook access data and saves it to a ParseUser. If no ParseUser exists with the same Facebook ID, then a new ParseUser is created.
  4. Your LogInCallback is called with the user.
  5. The current user reference will be updated to this user.

In order to display the Facebook login dialogs and activities, the current Activity must be provided (often, the current activity is this when calling logInWithReadPermissionsInBackground() from within the Activity) as we have done above.

ParseUser integration doesn’t require any permissions to work out of the box (i.e. null is perfectly acceptable). When logging in, you can only use read permissions. See our documentation below about requesting additional permissions (read or publish). Read more about permissions on Facebook’s developer guide.

Facebook Linking

If you want to associate an existing ParseUser to a Facebook account, you can link it like so:

if (!ParseFacebookUtils.isLinked(user)) {
  ParseFacebookUtils.linkWithReadPermissionsInBackground(user, this, permissions, new SaveCallback() {
    @Override
    public void done(ParseException ex) {
      if (ParseFacebookUtils.isLinked(user)) {
        Log.d("MyApp", "Woohoo, user logged in with Facebook!");
      }
    }
  });
}

The steps that happen when linking are very similar to log in. The difference is that on successful login, the existing ParseUser is updated with the Facebook information. Future logins via Facebook will now log the user into their existing account.

If you want to unlink Facebook from a user, simply do this:

ParseFacebookUtils.unlinkInBackground(user, new SaveCallback() {
  @Override
  public void done(ParseException ex) {
    if (ex == null) {
      Log.d("MyApp", "The user is no longer associated with their Facebook account.");
    }
  }
});

Requesting Permissions

As of v3.0 of the Facebook SDK, read and publish permissions must be requested separately. To request additional permissions, you may call ParseFacebookUtils.linkWithReadPermissionsInBackground() or ParseFacebookUtils.linkWithPublishPermissionsInBackground(). For more information about requesting new permissions, please see Facebook’s API documentation for these functions.

After successfully retrieving new permissions, please call ParseFacebookUtilities.linkInBackground(ParseUser, AccessToken), which will save any changes to the session token back to the ParseUser and ensure that this session data follows the user wherever it logs in.

Facebook SDK and Parse

The Facebook Android SDK provides a number of helper classes for interacting with Facebook’s API. Generally, you will use the GraphRequest class to interact with Facebook on behalf of your logged-in user. You can read more about the Facebook SDK here.

To access the user’s AccessToken you can simply call AccessToken.getCurrentAccessToken() to access the AccessToken instance, which can then be passed to GraphRequests.

Twitter Users

As with Facebook, Parse also provides an easy way to integrate Twitter authentication into your application. The Parse SDK provides a straightforward way to authorize and link a Twitter account to your ParseUsers. With just a few lines of code, you’ll be able to provide a “log in with Twitter” option in your app, and be able to save their data to Parse.

Setup

To start using Twitter with Parse, you need to:

  1. Set up a Twitter app, if you haven’t already.
  2. Add your application’s Twitter consumer key on your Parse application’s settings page.
  3. When asked to specify a “Callback URL” for your Twitter app, please insert a valid URL. This value will not be used by your iOS or Android application, but is necessary in order to enable authentication through Twitter.
  4. Add compile 'com.parse:parsetwitterutils-android:1.10.+' to your Gradle dependencies. This includes the contents of the Parse-*.jar and the com.parse:parse-android:1.10.+ repository, so be sure to remove as needed to prevent duplicate dependencies, otherwise a com.android.dex.DexException will be thrown.
  5. Add the following where you initialize the Parse SDK in your Application.onCreate()
ParseTwitterUtils.initialize("YOUR CONSUMER KEY", "YOUR CONSUMER SECRET");

If you encounter any issues that are Twitter-related, a good resource is the official Twitter documentation.

There are two main ways to use Twitter with your Parse users: (1) logging in as a Twitter user and creating a ParseUser, or (2) linking Twitter to an existing ParseUser.

Login & Signup

ParseTwitterUtils provides a way to allow your ParseUsers to log in or sign up through Twitter. This is accomplished using the logIn() method:

ParseTwitterUtils.logIn(this, new LogInCallback() {
  @Override
  public void done(ParseUser user, ParseException err) {
    if (user == null) {
      Log.d("MyApp", "Uh oh. The user cancelled the Twitter login.");
    } else if (user.isNew()) {
      Log.d("MyApp", "User signed up and logged in through Twitter!");
    } else {
      Log.d("MyApp", "User logged in through Twitter!");
    }
  }
});

When this code is run, the following happens:

  1. The user is shown the Twitter login dialog.
  2. The user authenticates via Twitter, and your app receives a callback.
  3. Our SDK receives the Twitter data and saves it to a ParseUser. If it’s a new user based on the Twitter handle, then that user is created.
  4. Your LogInCallback is called with the user.

In order to display the Twitter login dialogs and activities, the current Context must be provided (often, the current context is this when calling logIn() from within the Activity) as we have done above.

Twitter Linking

If you want to associate an existing ParseUser with a Twitter account, you can link it like so:

if (!ParseTwitterUtils.isLinked(user)) {
  ParseTwitterUtils.link(user, this, new SaveCallback() {
    @Override
    public void done(ParseException ex) {
      if (ParseTwitterUtils.isLinked(user)) {
        Log.d("MyApp", "Woohoo, user logged in with Twitter!");
      }
    }
  });
}

The steps that happen when linking are very similar to log in. The difference is that on successful login, the existing ParseUser is updated with the Twitter information. Future logins via Twitter will now log the user into their existing account.

If you want to unlink Twitter from a user, simply do this:

ParseTwitterUtils.unlinkInBackground(user, new SaveCallback() {
  @Override
  public void done(ParseException ex) {
    if (ex == null) {
      Log.d("MyApp", "The user is no longer associated with their Twitter account.");
    }
  }
});

Twitter API Calls

Our SDK provides a straightforward way to sign your API HTTP requests to the Twitter REST API when your app has a Twitter-linked ParseUser. To make a request through our API, you can use the Twitter singleton provided by ParseTwitterUtils:

HttpClient client = new DefaultHttpClient();
HttpGet verifyGet = new HttpGet(
        "https://api.twitter.com/1.1/account/verify_credentials.json");
ParseTwitterUtils.getTwitter().signRequest(verifyGet);
HttpResponse response = client.execute(verifyGet);
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Sessions

Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct Session object for each user-installation pair; if a user issues a login request from a device they’re already logged into, that user’s previous Session object for that Installation is automatically deleted. Session objects are stored on Parse in the Session class, and you can view them on the Parse.com Data Browser. We provide a set of APIs to manage Session objects in your app.

Session is a subclass of a Parse Object, so you can query, update, and delete sessions in the same way that you manipulate normal objects on Parse. Because the Parse Cloud automatically creates sessions when you log in or sign up users, you should not manually create Session objects unless you are building a “Parse for IoT” app (e.g. Arduino or Embedded C). Deleting a Session will log the user out of the device that is currently using this session’s token.

Unlike other Parse objects, the Session class does not have Cloud Code triggers. So you cannot register a beforeSave or afterSave handler for the Session class.

Properties

The Session object has these special fields:

  • sessionToken (readonly): String token for authentication on Parse API requests. In the response of Session queries, only your current Session object will contain a session token.
  • user: (readonly) Pointer to the User object that this session is for.
  • createdWith (readonly): Information about how this session was created (e.g. { "action": "login", "authProvider": "password"}).
    • action could have values: login, signup, create, or upgrade. The create action is when the developer manually creates the session by saving a Session object. The upgrade action is when the user is upgraded to revocable session from a legacy session token.
    • authProvider could have values: password, anonymous, facebook, or twitter.
  • restricted (readonly): Boolean for whether this session is restricted.
    • Restricted sessions do not have write permissions on User, Session, and Role classes on Parse. Restricted sessions also cannot read unrestricted sessions.
    • All sessions that the Parse Cloud automatically creates during user login/signup will be unrestricted. All sessions that the developer manually creates by saving a new Session object from the client (only needed for “Parse for IoT” apps) will be restricted.
  • expiresAt (readonly): Approximate UTC date when this Session object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app’s Parse.com dashboard settings page.
  • installationId (can be set only once): String referring to the Installation where the session is logged in from. For Parse SDKs, this field will be automatically set when users log in or sign up. All special fields except installationId can only be set automatically by the Parse Cloud. You can add custom fields onto Session objects, but please keep in mind that any logged-in device (with session token) can read other sessions that belong to the same user (unless you disable Class-Level Permissions, see below).

Handling Invalid Session Token Error

Apps created on Parse.com before March 25, 2015 use legacy session tokens until you migrate them to use the new revocable sessions. On API requests with legacy tokens, if the token is invalid (e.g. User object was deleted), then the request is executed as a non-logged in user and no error was returned. On API requests with revocable session tokens, an invalid session token will always fail with the “invalid session token” error. This new behavior lets you know when you need to ask the user to log in again.

With revocable sessions, your current session token could become invalid if its corresponding Session object is deleted from the Parse Cloud. This could happen if you implement a Session Manager UI that lets users log out of other devices, or if you manually delete the session via Cloud Code, REST API, or Data Browser. Sessions could also be deleted due to automatic expiration (if configured in app settings). When a device’s session token no longer corresponds to a Session object on the Parse Cloud, all API requests from that device will fail with “Error 209: invalid session token”.

To handle this error, we recommend writing a global utility function that is called by all of your Parse request error callbacks. You can then handle the “invalid session token” error in this global function. You should prompt the user to login again so that they can obtain a new session token. This code could look like this:

// Objective-C
@interface ParseErrorHandlingController : NSObject

+ (void)handleParseError:(NSError *)error;

@end

@implementation ParseErrorHandlingController

+ (void)handleParseError:(NSError *)error {
  if (![error.domain isEqualToString:PFParseErrorDomain]) {
    return;
  }

  switch (error.code) {
    case kPFErrorInvalidSessionToken: {
      [self _handleInvalidSessionTokenError];
      break;
    }
    ... // Other Parse API Errors that you want to explicitly handle.
  }
}

+ (void)_handleInvalidSessionTokenError {
  //--------------------------------------
  // Option 1: Show a message asking the user to log out and log back in.
  //--------------------------------------
  // If the user needs to finish what they were doing, they have the opportunity to do so.
  //
  // UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Invalid Session"
  //                                                     message:@"Session is no longer valid, please log out and log in again."
  //                                                    delegate:self
  //                                           cancelButtonTitle:@"Not Now"
  //                                           otherButtonTitles:@"OK"];
  // [alertView show];

  //--------------------------------------
  // Option #2: Show login screen so user can re-authenticate.
  //--------------------------------------
  // You may want this if the logout button is inaccessible in the UI.
  //
  // UIViewController *presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  // PFLogInViewController *logInViewController = [[PFLogInViewController alloc] init];
  // [presentingViewController presentViewController:logInViewController animated:YES completion:nil];
}

@end

// In all API requests, call the global error handler, e.g.
[[PFQuery queryWithClassName:@"Object"] findInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Query succeeded - continue your app logic here.
  } else {
    // Query failed - handle an error.
    [ParseErrorHandlingController handleParseError:error];
  }
}];
// Swift
class ParseErrorHandlingController {
  class func handleParseError(error: NSError) {
    if error.domain != PFParseErrorDomain {
      return
    }

    switch (error.code) {
    case kPFErrorInvalidSessionToken:
      handleInvalidSessionTokenError()

    ... // Other Parse API Errors that you want to explicitly handle.
  }

  private class func handleInvalidSessionTokenError() {
    //--------------------------------------
    // Option 1: Show a message asking the user to log out and log back in.
    //--------------------------------------
    // If the user needs to finish what they were doing, they have the opportunity to do so.
    //
    // let alertView = UIAlertView(
    //   title: "Invalid Session",
    //   message: "Session is no longer valid, please log out and log in again.",
    //   delegate: nil,
    //   cancelButtonTitle: "Not Now",
    //   otherButtonTitles: "OK"
    // )
    // alertView.show()

    //--------------------------------------
    // Option #2: Show login screen so user can re-authenticate.
    //--------------------------------------
    // You may want this if the logout button is inaccessible in the UI.
    //
    // let presentingViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
    // let logInViewController = PFLogInViewController()
    // presentingViewController?.presentViewController(logInViewController, animated: true, completion: nil)
  }
}

// In all API requests, call the global error handler, e.g.
let query = PFQuery(className: "Object")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]!, error: NSError!) -> Void in
  if error == nil {
    // Query Succeeded - continue your app logic here.
  } else {
    // Query Failed - handle an error.
    ParseErrorHandlingController.handleParseError(error)
  }
}
public class ParseErrorHandler {
  public static void handleParseError(ParseException e) {
    switch (e.getCode()) {
      case INVALID_SESSION_TOKEN: handleInvalidSessionToken()
        break;

      ... // Other Parse API errors that you want to explicitly handle
    }
  }

  private static void handleInvalidSessionToken() {
    //--------------------------------------
    // Option 1: Show a message asking the user to log out and log back in.
    //--------------------------------------
    // If the user needs to finish what they were doing, they have the opportunity to do so.
    //
    // new AlertDialog.Builder(getActivity())
    //   .setMessage("Session is no longer valid, please log out and log in again.")
    //   .setCancelable(false).setPositiveButton("OK", ...).create().show();

    //--------------------------------------
    // Option #2: Show login screen so user can re-authenticate.
    //--------------------------------------
    // You may want this if the logout button could be inaccessible in the UI.
    //
    // startActivityForResult(new ParseLoginBuilder(getActivity()).build(), 0);
  }
}

// In all API requests, call the global error handler, e.g.
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> results, ParseException e) {
    if (e == null) {
      // Query successful, continue other app logic
    } else {
      // Query failed
      ParseErrorHandler.handleParseError(e);
    }
  }
});
function handleParseError(err) {
  switch (err.code) {
    case Parse.Error.INVALID_SESSION_TOKEN:
      Parse.User.logOut();
      ... // If web browser, render a log in screen
      ... // If Express.js, redirect the user to the log in route
      break;

    ... // Other Parse API errors that you want to explicitly handle
  }
}

// For each API request, call the global error handler
query.find().then(function() {
  ...
}, function(err) {
  handleParseError(err);
});
public class ParseErrorHandler {
  public static void HandleParseError(ParseException e) {
    switch (e.Code) {
      case ParseException.ErrorCode.InvalidSessionToken:
        HandleInvalidSessionToken()
        break;

      ... // Other Parse API errors that you want to explicitly handle
    }
  }

  private static void HandleInvalidSessionToken() {
    //--------------------------------------
    // Option 1: Show a message asking the user to log out and log back in.
    //--------------------------------------
    // If the user needs to finish what they were doing, they have the opportunity to do so.

    //--------------------------------------
    // Option #2: Show login screen so user can re-authenticate.
    //--------------------------------------
    // You may want this if the logout button is inaccessible in the UI.
  }
});

// In all API requests, call the global error handler, e.g.
query.FindAsync().ContinueWith(t => {
  if (t.IsFaulted) {
    // Query Failed - handle an error.
    ParseErrorHandler.HandleParseError(t.Exception.InnerException as ParseException);
  } else {
    // Query Succeeded - continue your app logic here.
  }
});
public class ParseErrorHandler {
  public static handleParseError(ParseException $e) {
    $code = $e->getCode();
    switch ($code) {
      case: 209: // INVALID_SESSION_TOKEN
        ParseUser::logOut();
        ... // Redirect the to login page.
        break;

      ... // Other Parse API errors that you want to explicitly handle
    }
  }
});

// For each API request, call the global error handler
try {
  $results = $query->find();
  // ...
} catch (ParseException $e) {
  ParseErrorHandler::handleParseError($e)
}
// No command line example
// No C++ example

Security

Session objects can only be accessed by the user specified in the user field. All Session objects have an ACL that is read and write by that user only. You cannot change this ACL. This means querying for sessions will only return objects that match the current logged-in user.

When you log in a user via a User login method, Parse will automatically create a new unrestricted Session object in the Parse Cloud. Same for signups and Facebook/Twitter logins.

Session objects manually created from client SDKs (by creating an instance of Session, and saving it) are always restricted. You cannot manually create an unrestricted sessions using the object creation API.

Restricted sessions are prohibited from creating, modifying, or deleting any data in the User, Session, and Role classes. Restricted session also cannot read unrestricted sessions. Restricted Sessions are useful for “Parse for IoT” devices (e.g Arduino or Embedded C) that may run in a less-trusted physical environment than mobile apps. However, please keep in mind that restricted sessions can still read data on User, Session, and Role classes, and can read/write data in any other class just like a normal session. So it is still important for IoT devices to be in a safe physical environment and ideally use encrypted storage to store the session token.

If you want to prevent restricted Sessions from modifying classes other than User, Session, or Role, you can write a Cloud Code beforeSave handler for that class:

Parse.Cloud.beforeSave("MyClass", function(request, response) {
  Parse.Session.current().then(function(session) {
    if (session.get('restricted')) {
      response.error('write operation not allowed');
    }
    response.success();
  });
});

You can configure Class-Level Permissions (CLPs) for the Session class just like other classes on Parse. CLPs restrict reading/writing of sessions via the Session API, but do not restrict Parse Cloud’s automatic session creation/deletion when users log in, sign up, and log out. We recommend that you disable all CLPs not needed by your app. Here are some common use cases for Session CLPs:

  • Find, Delete — Useful for building a UI screen that allows users to see their active session on all devices, and log out of sessions on other devices. If your app does not have this feature, you should disable these permissions.
  • Create — Useful for “Parse for IoT” apps (e.g. Arduino or Embedded C) that provision restricted user sessions for other devices from the phone app. You should disable this permission when building apps for mobile and web. For “Parse for IoT” apps, you should check whether your IoT device actually needs to access user-specific data. If not, then your IoT device does not need a user session, and you should disable this permission.
  • Get, Update, Add Field — Unless you need these operations, you should disable these permissions.
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Roles

As your app grows in scope and user-base, you may find yourself needing more coarse-grained control over access to pieces of your data than user-linked ACLs can provide. To address this requirement, Parse supports a form of Role-based Access Control. Roles provide a logical way of grouping users with common access privileges to your Parse data. Roles are named objects that contain users and other roles. Any permission granted to a role is implicitly granted to its users as well as to the users of any roles that it contains.

For example, in your application with curated content, you may have a number of users that are considered “Moderators” and can modify and delete content created by other users. You may also have a set of users that are “Administrators” and are allowed all of the same privileges as Moderators, but can also modify the global settings for the application. By adding users to these roles, you can ensure that new users can be made moderators or administrators, without having to manually grant permission to every resource for each user.

We provide a specialized class called ParseRole that represents these role objects in your client code. ParseRole is a subclass of ParseObject, and has all of the same features, such as a flexible schema, automatic persistence, and a key value interface. All the methods that are on ParseObject also exist on ParseRole. The difference is that ParseRole has some additions specific to management of roles.

Properties

ParseRole has several properties that set it apart from ParseObject:

  • name: The name for the role. This value is required, and can only be set once as a role is being created. The name must consist of alphanumeric characters, spaces, -, or _. This name will be used to identify the Role without needing its objectId.
  • users: A relation to the set of users that will inherit permissions granted to the containing role.
  • roles: A relation to the set of roles whose users and roles will inherit permissions granted to the containing role.

Security for Role Objects

The ParseRole uses the same security scheme (ACLs) as all other objects on Parse, except that it requires an ACL to be set explicitly. Generally, only users with greatly elevated privileges (e.g. a master user or Administrator) should be able to create or modify a Role, so you should define its ACLs accordingly. Remember, if you give write-access to a ParseRole to a user, that user can add other users to the role, or even delete the role altogether.

To create a new ParseRole, you would write:

// By specifying no write privileges for the ACL, we can ensure the role cannot be altered.
ParseACL roleACL = new ParseACL();
roleACL.setPublicReadAccess(true);
ParseRole role = new ParseRole("Administrator", roleACL);
role.saveInBackground();

You can add users and roles that should inherit your new role’s permissions through the “users” and “roles” relations on ParseRole:

ParseRole role = new ParseRole(roleName, roleACL);
for (ParseUser user : usersToAddToRole) {
  role.getUsers().add(user)
}
for (ParseRole childRole : rolesToAddToRole) {
  role.getRoles().add(childRole);
}
role.saveInBackground();

Take great care when assigning ACLs to your roles so that they can only be modified by those who should have permissions to modify them.

Security for Other Objects

Now that you have created a set of roles for use in your application, you can use them with ACLs to define the privileges that their users will receive. Each ParseObject can specify a ParseACL, which provides an access control list that indicates which users and roles should be granted read or write access to the object.

Giving a role read or write permission to an object is straightforward. You can either use the ParseRole:

ParseRole moderators = /* Query for some ParseRole */;
ParseObject wallPost = new ParseObject("WallPost");
ParseACL postACL = new ParseACL();
postACL.setRoleWriteAccess(moderators);
wallPost.setACL(postACL);
wallPost.saveInBackground();

You can avoid querying for a role by specifying its name for the ACL:

ParseObject wallPost = new ParseObject("WallPost");
ParseACL postACL = new ParseACL();
postACL.setRoleWriteAccess("Moderators", true);
wallPost.setACL(postACL);
wallPost.save();

Role-based ParseACLs can also be used when specifying default ACLs for your application, making it easy to protect your users’ data while granting access to users with additional privileges. For example, a moderated forum application might specify a default ACL like this:

ParseACL defaultACL = new ParseACL();
// Everybody can read objects created by this user
defaultACL.setPublicReadAccess(true);
// Moderators can also modify these objects
defaultACL.setRoleWriteAccess("Moderators");
// And the user can read and modify its own objects
ParseACL.setDefaultACL(defaultACL, true);

Role Hierarchy

As described above, one role can contain another, establishing a parent-child relationship between the two roles. The consequence of this relationship is that any permission granted to the parent role is implicitly granted to all of its child roles.

These types of relationships are commonly found in applications with user-managed content, such as forums. Some small subset of users are “Administrators”, with the highest level of access to tweaking the application’s settings, creating new forums, setting global messages, and so on. Another set of users are “Moderators”, who are responsible for ensuring that the content created by users remains appropriate. Any user with Administrator privileges should also be granted the permissions of any Moderator. To establish this relationship, you would make your “Administrators” role a child role of “Moderators”, like this:

ParseRole administrators = /* Your "Administrators" role */;
ParseRole moderators = /* Your "Moderators" role */;
moderators.getRoles().add(administrators);
moderators.saveInBackground();
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

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.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

GeoPoints

Parse allows you to associate real-world latitude and longitude coordinates with an object. Adding a ParseGeoPoint to a ParseObject allows queries to take into account the proximity of an object to a reference point. This allows you to easily do things like find out what user is closest to another user or which places are closest to a user.

ParseGeoPoint

To associate a point with an object you first need to create a ParseGeoPoint. For example, to create a point with latitude of 40.0 degrees and -30.0 degrees longitude:

ParseGeoPoint point = new ParseGeoPoint(40.0, -30.0);

This point is then stored in the object as a regular field.

placeObject.put("location", point);

Geo Queries

Now that you have a bunch of objects with spatial coordinates, it would be nice to find out which objects are closest to a point. This can be done by adding another restriction to ParseQuery using whereNear. Getting a list of ten places that are closest to a user may look something like:

ParseGeoPoint userLocation = (ParseGeoPoint) userObject.get("location");
ParseQuery<ParseObject> query = ParseQuery.getQuery("PlaceObject");
query.whereNear("location", userLocation);
query.setLimit(10);
query.findInBackground(new FindCallback<ParseObject>() { ... });

At this point nearPlaces will be an array of objects ordered by distance (nearest to farthest) from userLocation. Note that if an additional orderByAscending()/orderByDescending() constraint is applied, it will take precedence over the distance ordering.

To limit the results using distance, check out whereWithinKilometers, whereWithinMiles, and whereWithinRadians.

It’s also possible to query for the set of objects that are contained within a particular area. To find the objects in a rectangular bounding box, add the whereWithinGeoBox restriction to your ParseQuery.

ParseGeoPoint southwestOfSF = new ParseGeoPoint(37.708813, -122.526398);
ParseGeoPoint northeastOfSF = new ParseGeoPoint(37.822802, -122.373962);
ParseQuery<ParseObject> query = ParseQuery.getQuery("PizzaPlaceObject");
query.whereWithinGeoBox("location", southwestOfSF, northeastOfSF);
query.findInBackground(new FindCallback<ParseObject>() { ... });

Caveats

At the moment there are a couple of things to watch out for:

  1. Each ParseObject class may only have one key with a ParseGeoPoint object.
  2. Using the whereNear constraint will also limit results to within 100 miles.
  3. Points should not equal or exceed the extreme ends of the ranges. Latitude should not be -90.0 or 90.0. Longitude should not be -180.0 or 180.0. Attempting to set latitude or longitude out of bounds will cause an error.
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Local Datastore

The Parse Android SDK provides a local datastore which can be used to store and retrieve ParseObjects, even when the network is unavailable. To enable this functionality, simply call Parse.enableLocalDatastore() before your call to initialize.

import com.parse.Parse;
import android.app.Application;

public class App extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    Parse.enableLocalDatastore(this);
    Parse.initialize(this, PARSE_APPLICATION_ID, PARSE_CLIENT_KEY);
  }
}

There are a couple of side effects of enabling the local datastore that you should be aware of. When enabled, there will only be one instance of any given ParseObject. For example, imagine you have an instance of the "GameScore" class with an objectId of "xWMyZ4YEGZ", and then you issue a ParseQuery for all instances of "GameScore" with that objectId. The result will be the same instance of the object you already have in memory.

Another side effect is that the current user and current installation will be stored in the local datastore, so you can persist unsaved changes to these objects between runs of your app using the methods below.

Calling the saveEventually method on a ParseObject will cause the object to be pinned in the local datastore until the save completes. So now, if you change the current ParseUser and call ParseUser.getCurrentUser().saveEventually(), your app will always see the changes that you have made.

Pinning

You can store a ParseObject in the local datastore by pinning it. Pinning a ParseObject is recursive, just like saving, so any objects that are pointed to by the one you are pinning will also be pinned. When an object is pinned, every time you update it by fetching or saving new data, the copy in the local datastore will be updated automatically. You don’t need to worry about it at all.

ParseObject gameScore = new ParseObject("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);

gameScore.pinInBackground();

If you have multiple objects, you can pin them all at once with the pinAllInBackground convenience method.

ParseObject.pinAllInBackground(listOfObjects);

Retrieving Objects

Storing objects is great, but it’s only useful if you can then get the objects back out later. Retrieving an object from the local datastore works just like retrieving one over the network. The only difference is calling the fromLocalDatastore method to tell the ParseQuery where to look for its results.

ParseQuery<ParseObject> query = ParseQuery.getQuery(GameScore");
query.fromLocalDatastore();
query.getInBackground("xWMyZ4YE", new GetCallback<ParseObject>() {
    public void done(ParseObject object, ParseException e) {
        if (e == null) {
            // object will be your game score
        } else {
            // something went wrong
        }
    }
});

Querying

Often, you’ll want to find a whole list of objects that match certain criteria, instead of getting a single object by id. To do that, you can use a ParseQuery. Any ParseQuery can be used with the local datastore just as with the network. The results will include any object you have pinned that matches the query. Any unsaved changes you have made to the object will be considered when evaluating the query. So you can find a local object that matches, even if it was never returned from the server for this particular query.

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Joe Bob");
query.fromLocalDatastore();
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> scoreList,
                     ParseException e) {
        if (e == null) {
            Log.d("score", "Retrieved " + scoreList.size());
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});

Unpinning

When you are done with an object and no longer need it to be in the local datastore, you can simply unpin it. This will free up disk space on the device and keep your queries on the local datastore running quickly.

gameScore.unpinInBackground();

There’s also a method to unpin several objects at once.

ParseObject.unpinAllInBackground(listOfObjects);

Pinning with Labels

Manually pinning and unpinning each object individual is a bit like using malloc and free. It is a very powerful tool, but it can be difficult to manage what objects get stored in complex scenarios. For example, imagine you are making a game with separate high score lists for global high scores and your friends’ high scores. If one of your friends happens to have a globally high score, you need to make sure you don’t unpin them completely when you remove them from one of the cached queries. To make these scenarios easier, you can also pin with a label. Labels indicate a group of objects that should be stored together.

// Add several objects with a label.
ParseObject.pinAllInBackground("MyScores", someGameScores);

// Add another object with the same label.
anotherGameScore.pinInBackground("MyScores");

To unpin all of the objects with the same label at the same time, you can pass a label to the unpin methods. This saves you from having to manually track which objects are in each group you care about.

ParseObject.unpinAllInBackground("MyScores");

Any object will stay in the datastore as long as it is pinned with any label. In other words, if you pin an object with two different labels, and then unpin it with one label, the object will stay in the datastore until you also unpin it with the other label.

Caching Query Results

Pinning with labels makes it easy to cache the results of queries. You can use one label to pin the results of each different query. To get new results from the network, just do a query and update the pinned objects.

ParseQuery<ParseObject> query = ParseQuery.getQuery(GameScore");
query.orderByDescending(score);

// Query for new results from the network.
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(final List<ParseObject> scores, ParseException e) {
    // Remove the previously cached results.
    ParseObject.unpinAllInBackground(highScores, new DeleteCallback() {
    public void done(ParseException e) {
      // Cache the new results.
      ParseObject.pinAllInBackground(highScores, scores);
    }
  });
  }
});

When you want to get the cached results for the query, you can then run the same query against the local datastore.

ParseQuery<ParseObject> query = ParseQuery.getQuery(GameScore");
query.orderByDescending(score);
query.fromLocalDatastore();

query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> scores, ParseException e) {
    // Yay! Cached scores!
  }
});

Syncing Local Changes

Once you’ve saved some changes locally, there are a few different ways you can save those changes back to Parse over the network. The easiest way to do this is with saveEventually. When you call saveEventually on a ParseObject, it will be pinned until it can be saved. The SDK will make sure to save the object the next time the network is available.

gameScore.saveEventually();

If you’d like to have more control over the way objects are synced, you can keep them in the local datastore until you are ready to save them yourself using saveInBackground. To manage the set of objects that need to be saved, you can again use a label. The fromPin method on ParseQuery makes it easy to fetch just the objects you care about.

ParseQuery<ParseObject> query = ParseQuery.getQuery(GameScore");
query.fromPin(MyChanges);
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> scores, ParseException e) {
    for (ParseObject score in scores) {
      score.saveInBackground();
      score.unpinInBackground(MyChanges);
    }
  }
});
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Push Notifications

Push notifications are a great way to keep your users engaged and informed about your app. You can reach your entire user base quickly and effectively. This guide will help you through the setup process and the general usage of Parse to send push notifications.

If you haven’t installed the SDK yet, head over to the Push QuickStart to get our SDK up and running.

Setting Up Push

If you want to start using push, start by completing the Android Push Notifications QuickStart Guide to learn how to configure your app and send your first push notification. Come back to this guide afterwards to learn more about the push features offered by Parse.

The Parse library provides push notifications using Google Cloud Messaging (GCM) if Google Play Services are available. Learn more about Google Play Services here.

When sending pushes to Android devices with GCM, there are several pieces of information that Parse keeps track of automatically:

  • Registration ID: The GCM registration ID uniquely identifies an app/device pairing for push purposes.
  • Sender ID: The GCM sender ID is a public number that identifies the sender of a push notification.
  • API key: The GCM API key is a server secret that allows a server to send pushes to a registration ID on behalf of a particular sender ID.

The Parse Android SDK chooses a reasonable default configuration so that you do not have to worry about GCM registration ids, sender ids, or API keys. In particular, the SDK will automatically register your app for push at startup time using Parse’s sender ID (1076345567071) and will store the resulting registration ID in the deviceToken field of the app’s current ParseInstallation.

However, as an advanced feature for developers that want to send pushes from multiple push providers, Parse allows you to optionally register your app for pushes with additional GCM sender IDs. To do this, specify the additional GCM sender ID with the following <meta-data> tag as a child of the <application> element in your app’s AndroidManifest.xml:

<meta-data android:name="com.parse.push.gcm_sender_id"
           android:value="id:YOUR_SENDER_ID" />;

In the sample snippet above, YOUR_SENDER_ID should be replaced by a numeric GCM sender ID. Note that the Parse SDK expects you to prefix your sender ID with an id: prefix, as shown in the sample snippet.

If you want to register your app with multiple additional sender IDs, then the android:value in the <meta-data> element above should hold a comma-delimited list of sender IDs, as in the following snippet:

<meta-data android:name="com.parse.push.gcm_sender_id"
           android:value="id:YOUR_SENDER_ID_1,YOUR_SENDER_ID_2,YOUR_SENDER_ID_3" />;

Installations

Every Parse application installed on a device registered for push notifications has an associated Installation object. The Installation object is where you store all the data needed to target push notifications. For example, in a baseball app, you could store the teams a user is interested in to send updates about their performance. Saving the Installation object is also required for tracking push-related app open events.

In Android, Installation objects are available through the ParseInstallation class, a subclass of ParseObject. It uses the same API for storing and retrieving data. To access the current Installation object from your Android app, use the ParseInstallation.getCurrentInstallation() method. The first time you save a ParseInstallation, Parse will add it to your Installation class and it will be available for targeting push notifications.

// Save the current Installation to Parse.
ParseInstallation.getCurrentInstallation().saveInBackground();

While it is possible to modify a ParseInstallation just like you would a ParseObject, there are several special fields that help manage and target devices.

  • channels: An array of the channels to which a device is currently subscribed.
  • installationId: Unique Id for the device used by Parse (readonly).
  • deviceType: The type of device, “ios”, “osx”, “android”, “winrt”, “winphone”, “dotnet”, or “embedded”. On Android devices, this field will be set to “android” (readonly).
  • pushType: This field is reserved for directing Parse to the push delivery network to be used. If the device is registered to receive pushes via GCM, this field will be marked “gcm”. If Google Play Services is not available, it will be blank (readonly).
  • GCMSenderId: This field only has meaning for Android ParseInstallations that use the GCM push type. It is reserved for directing Parse to send pushes to this installation with an alternate GCM sender ID. This field should generally not be set unless you are uploading installation data from another push provider. If you set this field, then you must set the GCM API key corresponding to this GCM sender ID in your Parse application’s push settings.
  • deviceToken: The token used by GCM to keep track of registration ID. On iOS devices, this is the Apple generated token (readonly).
  • appName: The display name of the client application to which this installation belongs. This value is synchronized every time a ParseInstallation object is saved from the device (readonly).
  • appVersion: The version string of the client application to which this installation belongs. This value is synchronized every time a ParseInstallation object is saved from the device (readonly).
  • parseVersion: The version of the Parse SDK which this installation uses. This value is synchronized every time a ParseInstallation object is saved from the device (readonly).
  • timeZone: The current time zone where the target device is located. This value is synchronized every time a ParseInstallation object is saved from the device (readonly).
  • localeIdentifier: The locale identifier of the device in the format [language code]-[COUNTRY CODE]. The language codes are two-letter lowercase ISO language codes (such as “en”) as defined by ISO 639-1. The country codes are two-letter uppercase ISO country codes (such as “US”) as defined by ISO 3166-1. This value is synchronized every time a ParseInstallation object is saved from the device (readonly).
  • badge: The current value of the icon badge for iOS apps. Changes to this value on the server will be used for future badge-increment push notifications.
  • channelUris: The Microsoft-generated push URIs for Windows devices (readonly).
  • appIdentifier: A unique identifier for this installation’s client application. This parameter is not supported in Android (readonly).

The Parse Android SDK will avoid making unnecessary requests. If a ParseInstallation is saved on the device, a request to the Parse servers will only be made if one of the ParseInstallation’s fields has been explicitly updated.

Sending Pushes

There are two ways to send push notifications using Parse: channels and advanced targeting. Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section.

Sending notifications is often done from the Parse.com push console, the REST API or from Cloud Code. However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set Client Push Enabled in the Push Notifications settings of your Parse app.

However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined on our blog. We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production.

You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.

Using Channels

The simplest way to start sending notifications is using channels. This allows you to use a publisher-subscriber model for sending pushes. Devices start by subscribing to one or more channels, and notifications can later be sent to these subscribers. The channels subscribed to by a given Installation are stored in the channels field of the Installation object.

Subscribing to Channels

A channel is identified by a string that starts with a letter and consists of alphanumeric characters, underscores, and dashes. It doesn’t need to be explicitly created before it can be used and each Installation can subscribe to any number of channels at a time.

Subscribing to a channel can be done using a single method call. For example, in a baseball score app, we could do:

// When users indicate they are Giants fans, we subscribe them to that channel.
ParsePush.subscribeInBackground("Giants");

By default, the main activity for your app will be run when a user responds to notifications.

Once subscribed to the “Giants” channel, your Installation object should have an updated channels field.

Unsubscribing from a channel is just as easy:

// When users indicate they are no longer Giants fans, we unsubscribe them.
ParsePush.unsubscribeInBackground("Giants");

You can also get the set of channels that the current device is subscribed to using:

List<String> subscribedChannels = ParseInstallation.getCurrentInstallation().getList("channels");

Neither the subscribe method nor the unsubscribe method blocks the thread it is called from. The subscription information is cached on the device’s disk if the network is inaccessible and transmitted to the Parse Cloud as soon as the network is usable. This means you don’t have to worry about threading or callbacks while managing subscriptions.

Sending Pushes to Channels

In the Android SDK, the following code can be used to alert all subscribers of the “Giants” channel that their favorite team just scored. This will display a notification center alert to iOS users and a system tray notification to Android users.

ParsePush push = new ParsePush();
push.setChannel("Giants");
push.setMessage("The Giants just scored! It's now 2-2 against the Mets.");
push.sendInBackground();

If you want to target multiple channels with a single push notification, you can use a LinkedList of channels.

LinkedList<String> channels = new LinkedList<String>();
channels.add("Giants");
channels.add("Mets");

ParsePush push = new ParsePush();
push.setChannels(channels); // Notice we use setChannels not setChannel
push.setMessage("The Giants won against the Mets 2-3.");
push.sendInBackground();

Using Advanced Targeting

While channels are great for many applications, sometimes you need more precision when targeting the recipients of your pushes. Parse allows you to write a query for any subset of your Installation objects using the querying API and to send them a push.

Since ParseInstallation is a subclass of ParseObject, you can save any data you want and even create relationships between Installation objects and your other objects. This allows you to send pushes to a very customized and dynamic segment of your user base.

Saving Installation Data

Storing data on a ParseInstallation object is just as easy as storing any other data on Parse. In our Baseball app, we could allow users to get pushes about game results, scores and injury reports.

// Store app language and version
ParseInstallation installation = ParseInstallation.getCurrentInstallation();
installation.put("scores",true);
installation.put("gameResults",true);
installation.put("injuryReports",true);
installation.saveInBackground();

You can even create relationships between your Installation objects and other classes saved on Parse. To associate a ParseInstallation with a particular user, for example, you can simply store the current user on the ParseInstallation.

// Associate the device with a user
ParseInstallation installation = ParseInstallation.getCurrentInstallation();
installation.put("user",ParseUser.getCurrentUser());
installation.saveInBackground();

Sending Pushes to Queries

Once you have your data stored on your ParseInstallation objects, you can use a ParseQuery to target a subset of these devices. ParseInstallation queries work just like any other Parse query, but we use the special static method ParseInstallation.getQuery() to create it. We set this query on our ParsePush object, before sending the notification.

// Create our Installation query
ParseQuery pushQuery = ParseInstallation.getQuery();
pushQuery.whereEqualTo("injuryReports", true);

// Send push notification to query
ParsePush push = new ParsePush();
push.setQuery(pushQuery); // Set our Installation query
push.setMessage("Willie Hayes injured by own pop fly.");
push.sendInBackground();

We can even use channels with our query. To send a push to all subscribers of the “Giants” channel but filtered by those who want score update, we can do the following:

// Create our Installation query
ParseQuery pushQuery = ParseInstallation.getQuery();
pushQuery.whereEqualTo("channels", "Giants"); // Set the channel
pushQuery.whereEqualTo("scores", true);

// Send push notification to query
ParsePush push = new ParsePush();
push.setQuery(pushQuery);
push.setMessage("Giants scored against the A's! It's now 2-2.");
push.sendInBackground();

If we store relationships to other objects in our ParseInstallation class, we can also use those in our query. For example, we could send a push notification to all users near a given location like this.

// Find users near a given location
ParseQuery userQuery = ParseUser.getQuery();
userQuery.whereWithinMiles("location", stadiumLocation, 1.0)

// Find devices associated with these users
ParseQuery pushQuery = ParseInstallation.getQuery();
pushQuery.whereMatchesQuery("user", userQuery);

// Send push notification to query
ParsePush push = new ParsePush();
push.setQuery(pushQuery); // Set our Installation query
push.setMessage("Free hotdogs at the Parse concession stand!");
push.sendInBackground();

Sending Options

Push notifications can do more than just send a message. In Android, pushes can also include custom data you wish to send. You have complete control of how you handle the data included in your push notification as we will see in the Receiving Notifications section. An expiration date can also be set for the notification in case it is time sensitive.

Customizing your Notifications

If you want to send more than just a message, you will need to use a JSONObject to package all of the data. There are some reserved fields that have a special meaning in Android.

  • alert: the notification’s message.
  • uri: (Android only) an optional field that contains a URI. When the notification is opened, an Activity associated with opening the URI is launched.
  • title: (Android, Windows 8, & Windows Phone 8 only) the value displayed in the Android system tray or Windows 8 toast notification.

For example, to send a notification that would increases the badge number by 1 and plays a custom sound, you can do the following. Note that you can set these properties from your Android client, but they would only take effect in the iOS version of your app. The badge and sound fields would have no effects for Android recipients.

JSONObject data = new JSONObject("{\"alert\": \"The Mets scored!\",
                                   \"badge\": \"Increment\",
                                   \"sound\": \"cheering.caf\"}");

ParsePush push = new ParsePush();
push.setChannel("Mets");
push.setData(data);
push.sendPushInBackground();

It is also possible to specify your own data in this dictionary. As we’ll see in the Receiving Notifications section, you’re able to use the data sent with your push to do custom processing when a user receives and interacts with a notification.

JSONObject data = new JSONObject("{\"name\": \"Vaughn\",
                                   \"newsItem\": \"Man bites dog\"}"));

ParsePush push = new ParsePush();
push.setQuery(injuryReportsQuery);
push.setChannel("Indians");
push.setData(data);
push.sendPushInBackground();

Setting an Expiration Date

When a user’s device is turned off or not connected to the internet, push notifications cannot be delivered. If you have a time sensitive notification that is not worth delivering late, you can set an expiration date. This avoids needlessly alerting users of information that may no longer be relevant.

There are two methods provided by the ParsePush class to allow setting an expiration date for your notification. The first is setExpirationTime which simply takes an time (in UNIX epoch time) specifying when Parse should stop trying to send the notification.

// Send push notification with expiration date
ParsePush push = new ParsePush();
push.setExpirationTime(1424841505);
push.setQuery(everyoneQuery);
push.setMessage("Season tickets on sale until February 25th");
push.sendPushInBackground();

There is however a caveat with this method. Since device clocks are not guaranteed to be accurate, you may end up with inaccurate results. For this reason, the ParsePush class also provides the setExpirationTimeInterval method which accepts a timeInterval (in seconds). The notification will expire after the specified interval has elapsed.

// Create time interval
long weekInterval = 60*60*24*7; // 1 week

// Send push notification with expiration interval
ParsePush push = new ParsePush();
push.setExpirationTimeInterval(weekInterval);
push.setQuery(everyoneQuery);
push.setMessage("Season tickets on sale until next week!");
push.sendPushInBackground();

Targeting by Platform

If you build a cross platform app, it is possible you may only want to target devices of a particular operating system. Advanced Targeting allow you to filter which of these devices are targeted.

The following example would send a different notification to Android, iOS, and Windows users.

ParseQuery query = ParseInstallation.getQuery();
query.whereEqualTo("channels", "suitcaseOwners");

// Notification for Android users
query.whereEqualTo("deviceType", "android");
ParsePush androidPush = new ParsePush();
androidPush.setMessage("Your suitcase has been filled with tiny robots!");
androidPush.setQuery(query);
androidPush.sendPushInBackground();

// Notification for iOS users
query.whereEqualTo("deviceType", "ios");
ParsePush iOSPush = new ParsePush();
iOSPush.setMessage("Your suitcase has been filled with tiny apples!");
iOSPush.setQuery(query);
iOSPush.sendPushInBackground();

// Notification for Windows 8 users
query.whereEqualTo("deviceType", "winrt");
ParsePush winPush = new ParsePush();
winPush.setMessage("Your suitcase has been filled with tiny glass!");
winPush.setQuery(query);
winPush.sendPushInBackground();

// Notification for Windows Phone 8 users
query.whereEqualTo("deviceType", "winphone");
ParsePush wpPush = new ParsePush();
wpPush.setMessage("Your suitcase is very hip; very metro.");
wpPush.setQuery(query);
wpPush.sendPushInBackground();

Scheduling Pushes

Sending scheduled push notifications is not currently supported by the Android SDK. Take a look at the REST API, JavaScript SDK or the push console on your Parse Dashboard.

Receiving Pushes

Make sure you’ve gone through the Android Push QuickStart to set up your app to receive pushes.

When a push notification is received, the “title” is displayed in the status bar and the “alert” is displayed alongside the “title” when the user expands the notification drawer. If you choose to subclass com.parse.ParsePushBroadcastReceiver, be sure to replace that name with your class’ name in the registration.

Note that some Android emulators (the ones without Google API support) don’t support GCM, so if you test your app in an emulator make sure to select an emulator image that has Google APIs installed.

Customizing Notifications

Now that your app is all set up to receive push notifications, you can start customizing the display of these notifications.

Customizing Notification Icons

The Android style guide recommends apps use a push icon that is monochromatic and flat. The default push icon is your application’s launcher icon, which is unlikely to conform to the style guide. To provide a custom push icon, add the following metadata tag to your app’s AndroidManifest.xml:

<meta-data android:name="com.parse.push.notification_icon" android:resource="@drawable/push_icon"/>

…where push_icon is the name of a drawable resource in your package. If your application needs more than one small icon, you can override getSmallIconId in your ParsePushBroadcastReceiver subclass.

If your push has a unique context associated with an image, such as the avatar of the user who sent a message, you can use a large push icon to call attention to the notification. When a notification has a large push icon, your app’s static (small) push icon is moved to the lower right corner of the notification and the large icon takes its place. See the Android UI documentation for examples. To provide a large icon, you can override getLargeIcon in your ParsePushBroadcastReceiver subclass.

Responding with a Custom Activity

If your push has no “uri” parameter, onPushOpen will invoke your application’s launcher activity. To customize this behavior, you can override getActivity in your ParsePushBroadcastReceiver subclass.

Responding with a URI

If you provide a “uri” field in your push, the ParsePushBroadcastReceiver will open that URI when the notification is opened. If there are multiple apps capable of opening the URI, a dialog will displayed for the user. The ParsePushBroadcastReceiver will manage your back stack and ensure that clicking back from the Activity handling URI will navigate the user back to the activity returned by getActivity.

Managing the Push Lifecycle

The push lifecycle has three phases:

  1. A notification is received and the com.parse.push.intent.OPEN Intent is fired, causing the ParsePushBroadcastReceiver to call onPushReceive. If either “alert” or “title” are specified in the push, then a Notification is constructed using getNotification. This Notification uses a small icon generated using getSmallIconId, which defaults to the icon specified by the com.parse.push.notification_icon metadata in your AndroidManifest.xml. The Notification’s large icon is generated from getLargeIcon which defaults to null. The notification’s contentIntent and deleteIntent are com.parse.push.intent.OPEN and com.parse.push.intent.DELETE respectively.
  2. If the user taps on a Notification, the com.parse.push.intent.OPEN Intent is fired. The ParsePushBroadcastReceiver calls onPushOpen. The default implementation automatically sends an analytics event back to Parse tracking that this notification was opened. If the push contains a “uri” parameter, an activity is launched to navigate to that URI, otherwise the activity returned by getActivity is launched.
  3. If the user dismisses a Notification, the com.parse.push.intent.DELETE Intent is fired. The ParsePushBroadcastReceiver calls onPushDismiss, which does nothing by default

All of the above methods may be subclassed to customize the way your application handles push notifications. When subclassing the methods onPushReceive, onPushOpen, onPushDismiss, or getNotification, consider delegating to super where appropriate. For example, one might override onPushReceive to trigger a background operation for “silent” pushes and then delegate to super for all other pushes. This provides the most benefit from Parse Push and makes your code forward-compatible.

Tracking Pushes and App Opens

The default implementation of onPushOpen will automatically track user engagment from pushes. If you choose not to use the ParsePushBroadcastReceiver or override the onPushOpen implementation, you may need to track your app open event manually. To do this, add the following to the onCreate method of the Activity or the onReceive method of the BroadcastReceiver which handles the com.parse.push.intent.OPEN Intent:

ParseAnalytics.trackAppOpened(getIntent());

To track push opens, you should always pass the Intent to trackAppOpened. Passing null to trackAppOpened will track only a standard app-opened event, not the push-opened event. If you don’t track the push-opened event, you will not be able to use advanced analytics features such as push-open graphs and A/B testing.

Please be sure to set up your application to save the Installation object. Push open tracking only works when your application’s devices are associated with saved Installation objects.

You can view the open rate for a specific push notification on your Parse.com push console. You can also view your application’s overall app open and push open graphs on the Parse analytics console. Our push open analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release.

Push Experiments

You can A/B test your push notifications to figure out the best way to keep your users engaged. With A/B testing, you can simultaneously send two versions of your push notification to different devices, and use each version’s push open rates to figure out which one is better. You can test by either message or send time.

A/B Testing

Our web push console guides you through every step of setting up an A/B test.

For each push campaign sent through the Parse web push console, you can allocate a subset of your devices to be in the experiment’s test audience, which Parse will automatically split into two equally-sized experiment groups. For each experiment group, you can specify a different push message. The remaining devices will be saved so that you can send the winning message to them later. Parse will randomly assign devices to each group to minimize the chance for a test to affect another test’s results (although we still don’t recommend running multiple A/B tests over the same devices on the same day).

After you send the push, you can come back to the push console to see in real time which version resulted in more push opens, along with other metrics such as statistical confidence interval. It’s normal for the number of recipients in each group to be slightly different because some devices that we had originally allocated to that experiment group may have uninstalled the app. It’s also possible for the random group assignment to be slightly uneven when the test audience size is small. Since we calculate open rate separately for each group based on recipient count, this should not significantly affect your experiment results.

If you are happy with the way one message performed, you can send that to the rest of your app’s devices (i.e. the “Launch Group”). This step only applies to A/B tests where you vary the message.

Push experiments are supported on all recent Parse SDKs (iOS v1.2.13+, Android v1.4.0+, .NET v1.2.7+). Before running experiments, you must instrument your app with push open tracking.

Experiment Statistics

Parse provides guidance on how to run experiments to achieve statistically significant results.

Test Audience Size

When you setup a push message experiment, we’ll recommend the minimum size of your test audience. These recommendations are generated through simulations based on your app’s historical push open rates. For big push campaigns (e.g. 100k+ devices), this recommendation is usually small subset of your devices. For smaller campaigns (e.g. < 5k devices), this recommendation is usually all devices. Using all devices for your test audience will not leave any remaining devices for the launch group, but you can still gain valuable insight into what type of messaging works better so you can implement similar messaging in your next push campaign.

Confidence Interval

After you send your pushes to experiment groups, we’ll also provide a statistical confidence interval when your experiment has collected enough data to have statistically significant results. This confidence interval is in absolute percentage points of push open rate (e.g. if the open rates for groups A and B are 3% and 5%, then the difference is reported as 2 percentage points). This confidence interval is a measure of how much difference you would expect to see between the two groups if you repeat the same experiment many times.

Just after a push send, when only a small number of users have opened their push notifications, the open rate difference you see between groups A and B could be due to random chance, so it might not be reproducible if you run the same experiment again. After your experiment collects more data over time, we become increasingly confident that the observed difference is a true difference. As this happens, the confidence interval will become narrower, allowing us to more accurately estimate the true difference between groups A and B. Therefore, we recommend that you wait until there is enough data to generate a statistical confidence interval before deciding which group’s push is better.

Push Localization

Localizing your app’s content is a proven way to drive greater engagement. We’ve made it easy to localize your push messages with Push Localization. The latest version of the Parse Android SDK will detect and store the user’s language in the installation object, and via the web push console you’ll be able to send localized push messages to your users in a single broadcast.

Setup

To take advantage of Push Localization you will need to make sure you’ve published your app with the Parse Android SDK version 1.10.1 or greater. Any users of your application running the Parse Android SDK version 1.10.1 or greater will then be targetable by Push Localization via the web push console.

It’s important to note that for developers who have users running apps with versions of the Parse Android SDK earlier than 1.10.1 that targeting information for Localized Push will not be available and these users will receive the default message from the push console.

Sending a localized push

Our web push console guides you through every step of setting up a Localized Push.

Troubleshooting

Setting up Push Notifications is often a source of frustration for developers. The process is complicated and invites problems to happen along the way. If you run into issues, try some of these troubleshooting tips.

It’s important to break down the system into components when troubleshooting push notification issues. You can start by asking yourself the following questions:

  • Is the push notification listed in your push logs?
  • Are you targeting your push notification to the correct audience?
  • Are your devices registering for push notifications?
  • Is your app set up to receive these notifications?

If the answer is Yes to all of these questions, there may be an operational issue that is affecting the delivery of push notifications. You can see the current status of the platform in our status page. If everything looks green, please create a bug report with more information. Every bug report opened against the push notification category will be redirected to this troubleshooting guide unless it includes evidence of having gone through these steps, so please be as verbose as possible.

If you’re unsure about the answer to any of the above questions, read on!

Confirm that the push campaign was created

Having everything set up correctly in your Parse app won’t help if your request to send a push notification does not reach Parse. The first step in debugging a push issue is to confirm that the push campaign is listed in your push logs. You can find these logs by visiting your app’s Dashboard and clicking on Push.

If the push notification campaign is not showing up on that list, the issue is quite simple to resolve. Go back to your push notification sending code and make sure to check for any error responses. If you’re using any of the client SDKs, make sure to listen for and catch any errors that may be returned. For example, you could log errors like so:

push.sendPushInBackground(new SendCallback() {
  public void done(ParseException e) {
    if (e == null) {
      Log.d("push", "The push campaign has been created.");
    } else {
      Log.d("push", "Error sending push:" + e.getMessage());
    }
  }
});

Please note that SDKs that use a Client Key, such as the Android SDK, can only send push notifications if Client Push is enabled in your Parse app’s Push Notification settings. Otherwise, you’ll only be able to send pushes from the web console, the REST API, or by using the JavaScript SDK from Cloud Code. We strongly encourage developers to turn off Client Push before releasing their app publicly unless your use case allows for any amount of arbitrary pushes to be sent by any of your users. You can read our security guide for more information.

Verify your Targeting

You have confirmed that the push notification is making it to your push logs. Now what? The next step is to verify if your push notification targeting is correct. Say, if you’re debugging why notifications are not reaching a specific device, it’s important to make sure that this device is actually included in the targeting of this push notification. You can’t deliver something that is not addressed correctly.

In order to do this, you’ll have to confirm that the device’s ParseInstallation object is included in your push notification targeting criteria. This is quite straightforward if you’re using channels: all you need to verify is that your device’s ParseInstallation is subscribed to the channel by checking the channels array. If you’re using advanced targeting, e.g. you’re using a push query with constraints, you’ll have to work a bit more to determine if your targeting is on point.

Basically, you will need to run the same push query you’re using for your targeting, and verify that your installation of interest is included in the result set. As you can only query the installation class programmatically using the Master Key, you will need to use one of the Master Key capable SDKs (JavaScript SDK, .NET) or the REST API. Ideally, you would use the same SDK that you’re currently using to send the push notifications.

Debugging using the REST API

The REST API is quite easy to use for this sort of purpose as you can easily recreate the push query using the information provided in your push notification logs. If you look closely at the “Full Target” value in your push campaign log item, you may notice that it matches the query format for a REST API query. You can grab an example of what a REST API query over ParseInstallations would look like from the REST API docs. Don’t forget to use the X-Parse-Master-Key header to ensure that the Master Key is used to run this query.

# Query over installations
curl -X GET \
-H "X-Parse-Application-Id: {YOUR_APPLICATION_ID}" \
-H "X-Parse-Master-Key: {YOUR_MASTER_KEY}" \
-G \
--data-urlencode 'limit=1000' \
--data-urlencode 'where={ "city": "San Francisco", "deviceType": { "$in": [ "ios", "android", "winphone", "embedded" ] } }' \
https://api.parse.com/1/installations

If you type the above into a console, you should be able to see the first 1,000 objects that match your query. Note that constraints are always ANDed, so if you want to further reduce the search scope, you can add a constraint that matches the specific installation for your device:

# Query over installations
curl -X GET \
-H "X-Parse-Application-Id: {YOUR_APPLICATION_ID}" \
-H "X-Parse-Master-Key: {YOUR_MASTER_KEY}" \
-G \
--data-urlencode 'limit=1' \
--data-urlencode 'where={ “objectId”: {YOUR_INSTALLATION_OBJECT_ID}, "city": "San Francisco", "deviceType": { "$in": [ "ios", "android", "winphone", "embedded" ] } }' \
https://api.parse.com/1/installations

If the above query returns no results, it is likely that your installation does not meet the targeting criteria for your campaign.

Check your Device Configuration

Your push campaign is created and the device is included in the targeting, but you’re still not receiving push notifications. What gives?

You can check the Push Delivery Report for the cause of failed deliveries: Mismatch Sender ID happens when the registered sender is not the same as the one currently sending the notifications, Not Registered is returned when the registration key is deemed invalid, either by misconfiguration or if the user have uninstalled the app.

If everything looks great so far, but push notifications are not showing up on your phone, there are a few more things you can check.

  • Upgrade to the latest SDK. This documentation covers the push API introduced in the 1.7.0 version of the Android Parse SDK. Please upgrade if you are getting compiler errors following these instructions.
  • Make sure you are using the correct packageName in your AndroidManifest.xml.
  • Make sure you have the correct permissions listed in your AndroidManifest.xml file, as outlined in the Android Push Quickstart. If you are using a a custom receiver, be sure you have registered it in the Manifest file with the correct android:name property and the proper intent filters.
  • Make sure you’ve used the correct App ID and client key, and that Parse.initialize() is being called. Parse.initialize() lets the service know which application it is listening for; this code must be in your Application.onCreate rather than Activity.onCreate for a particular Activity, so that any activation technique will know how to use Parse.
  • Check that the push registration call is being called successfully. Your device must successfully register a ParseInstallation object with a valid GCM Registration id in the “deviceToken” field, if using GCM.
  • Check that the device is set to accept push notifications from your app.
  • Note that, by design, force-killed apps will not be able to receive push notifications. Launch the app again to reenable push notifications.
  • Check the number of subscribers in your Parse Push Console. Does it match the expected number of subscribers? Your push might be targeted incorrectly.
  • If testing in the emulator, try cleaning and rebuilding your project and restarting your AVD.
  • Turn on verbose logging with Parse.setLogLevel(Parse.LOG_LEVEL_VERBOSE). The error messages will be a helpful guide to what may be going on.
  • If you see the message “Finished (with error)” in your Dashboard, check your verbose logs. If you are performing pushes from a device, check that client-side push is enabled in your dashboard.
  • In your logs, you may see an error message, “Could not construct writer” or other issues related to a broken pipe. When this occurs, the framework will continue to try reconnecting. It should not crash your app.

If your app has been released for a while, it is expected that a percentage of your user base will have opted out of push notifications from your app or uninstalled your app from their device. Parse does not automatically delete installation objects in either of these cases. When a push campaign is sent out, Parse will detect uninstalled installations and exclude them from the total count of push notifications sent. The recipient estimate on the push composer is based on the estimated number of installations that match your campaign’s targeting criteria. With that in mind, it is possible for the recipient estimate to be higher than the number of push notifications that is sent as reported by the push campaign status page.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

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));
  }
});

Current Config

Every ParseConfig instance that you get is always immutable. When you retrieve a new ParseConfig in the future from the network, it will not modify any existing ParseConfig instance, but will instead create a new one and make it available via ParseConfig.getCurrentConfig(). Therefore, you can safely pass around any ParseConfig object and safely assume that it will not automatically change.

It might be troublesome to retrieve the config from the server every time you want to use it. You can avoid this by simply using the cached getCurrentConfig object and fetching the config only once in a while.

class Helper {
  private static final long configRefreshInterval = 12 * 60 * 60 * 1000;
  private static long lastFetchedTime;

  // Fetches the config at most once every 12 hours per app runtime
  public static void refreshConfig() {
    long currentTime = System.currentTimeMillis();
    if (currentTime - lastFetchedTime > configRefreshInterval) {
      lastFetchedTime = currentTime;
      ParseConfig.getInBackground();
    }
  }
}

Parameters

ParseConfig supports most of the data types supported by ParseObject:

  • String
  • Numbers (boolean/int/double/long)
  • Date
  • ParseFile
  • ParseGeoPoint
  • List
  • Map

We currently allow up to 100 parameters in your config and a total size of 128KB across all parameters.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

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.trackAppOpenedInBackground(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().

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

User Interface

At the end of the day, users of your application will be interacting with Android UI components. We provide several UI widgets to make working with Parse data easier.

ParseLoginUI

If you are using Parse to manage users in your mobile app, you are already familiar with the ParseUser class. At some point in your app, you might want to present a screen to log in your ParseUser. Parse provides an open-source ParseLoginUI library that does exactly this. Please note ParseLoginUI is not included with the Parse Android SDK; you need to import this library from maven or from our Git repository into your project. Check the README.md of the ParseLoginUI for detail steps.

This library contains an Android login activity that is ultra-customizable and easy to integrate with your app. You can configure the look and feel of the login screens by either specifying XML configurations or constructing an Intent in code. In this guide, we first provide several ways to integrate with the login library. Then, we describe in detail how to customize the login screens.

Login Library API

Basic Integration

The login library integration is simple. Our library exposes an activity-level API that lets you launch the login library activity (ParseLoginActivity) to obtain a ParseUser. ParseLoginActivity will guide the user through several screens to log in, sign up, or reset their password. If the user resets their password by email, they are taken back to the login screen. The user will also see helpful toast messages if they provide invalid input (e.g. logging in with an incorrect password or signing up with a username that’s already taken).

To include ParseLoginActivity in your app, import the ParseLoginUI library project, and add the following into your AndroidManifest.xml:

<activity
    android:name="com.parse.ui.ParseLoginActivity"
    android:label="@string/app_name"
    android:launchMode="singleTop">
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.PARSE_LOGIN_ENABLED"
        android:value="true"/>
</activity>

Then, you can launch ParseLoginActivity from your own activity by calling:

ParseLoginBuilder builder = new ParseLoginBuilder(MyActivity.this);
startActivityForResult(builder.build(), 0);

When ParseLoginActivity finishes, your caller activity will receive either:

  • RESULT_OK: The user successfully logged in. ParseUser.getCurrentUser() will be populated.
  • RESULT_CANCELLED: The user pressed the back button. If the user fails to log in or sign up, the only way to return to the previous screen is by pressing the back button.

We’ve provided a sample app, ParseLoginSampleBasic for this use case. This sample app is a simple profile viewer. If you are not logged in, SampleProfileActivity prompts you to log in. Clicking on the login button in SampleProfileActivity launches the ParseLoginActivity, which prompts the user for login credentials. If the user successfully logs in, ParseLoginActivity will automatically finish itself and return RESULT_OK. Then, SampleProfileActivity will display the user’s name and a logout button.

This basic integration case works well if your caller activity is designed to function regardless of whether there is a valid current user. For example, a restaurant reviews app may allow the user to browse restaurants even when the user is not logged in. If the user does log in, the app could provide a more personalized experience on that same screen.

Advanced Integration

If some parts or all of your app cannot function without a valid ParseUser, you can protect these parts of your app with a ParseLoginDispatchActivity (supplied in this library project). This dispatch activity acts like a gatekeeper; it automatically launches ParseLoginActivity if no user is logged in, or launches the protected activity if a user is already logged in. To use this, you subclass ParseLoginDispatchActivity and specify what protected activity to launch.

public class SampleDispatchActivity extends ParseLoginDispatchActivity {
  @Override
  protected Class<?> getTargetClass() {
    return SampleProfileActivity.class;
  }
}

We’ve provided another sample app, ParseLoginSampleWithDispatchActivity for this use case. The SampleProfileActivity in this app cannot function without a logged-in user, so it is protected by SampleDispatchActivity.

The dispatch activity does not necessarily need to be the first activity at app launch. You can launch the dispatch activity from any activity. When your protected activity finishes, the dispatch activity will automatically forward the result code to your caller activity.

Let’s revisit the restaurant reviews app again. You might have a comment activity that requires a user. You can protect this activity behind a dispatch activity. The main restaurant listing activity (supports either user or no user), can launch the dispatch activity when the user presses the comment button. If no user is logged in, the dispatch activity will start ParseLoginActivity to obtain a ParseUser. If the user refuses to log in, they will be gracefully taken back to the restaurant listings activity. In the restaurant listings activity, you can always call ParseUser.getCurrentUser() to determine whether the user logged in.

Customizing the Login Library

There are three ways to customize the login library:

  • Activity metadata in AndroidManifest.xml - This is most recommended because it allows you to customize the login experience without writing any code, and it makes the login experience consistent regardless which activity in your app launches ParseLoginActivity.
  • In code with ParseLoginBuilder - If you love writing code to do everything.
  • Overriding layout resource XMLs - This option is useful if you want to make significant changes to the look and feel of the login experience.

Configure by Activity Metadata

We provide the following options for easily customizing the ParseLoginActivity in your app’s AndroidManifest.xml:

  • APP_LOGO: Drawable` resource for app logo.
  • PARSE_LOGIN_ENABLED: Boolean for whether to enable the Parse username/password login (default = false)
  • PARSE_LOGIN_BUTTON_TEXT: String to display on the login button (default = “Log in”)
  • PARSE_SIGNUP_BUTTON_TEXT: String to display on the signup button on the login screen (default = “Sign up”)
  • PARSE_LOGIN_HELP_TEXT: String to display on the password-reset link (default = “Forgotten password”)
  • PARSE_LOGIN_INVALID_CREDENTIALS_TEXT: String to show on the toast when the user login fails (default = “The username and password you entered don’t match”)
  • PARSE_LOGIN_EMAIL_AS_USERNAME: Boolean for whether to prompt for the user’s email as the username on the login and signup form (default = false)
  • PARSE_SIGNUP_MIN_PASSWORD_LENGTH: Integer for the minimum required password length on the signup form (default = 6)
  • PARSE_SIGNUP_SUBMIT_BUTTON_TEXT: String to display on the submit button on the signup screen (default = “Submit”)
  • FACEBOOK_LOGIN_ENABLED: Boolean for whether to show the Facebook login button (default = false)
  • FACEBOOK_LOGIN_BUTTON_TEXT: String to display on the Facebook login button (default = “Log in with Facebook”)
  • FACEBOOK_LOGIN_PERMISSIONS: String array resource containing requested Facebook permissions (default = empty)
  • TWITTER_LOGIN_ENABLED: Boolean for whether to show the Twitter login button (default = false)
  • TWITTER_LOGIN_BUTTON_TEXT: String to display on the Twitter login button (default = “Log in with Twitter”)

Please note that PARSE_LOGIN_ENABLED, FACEBOOK_LOGIN_ENABLED, and TWITTER_LOGIN_ENABLED are all false by default. You need to explicitly set them to true for those components to show up on the screen. For APP_LOGO and FACEBOOK_LOGIN_PERMISSIONS, make sure you use android:resource instead of android:value.

Example configuration:

<activity
    android:name="com.parse.ui.ParseLoginActivity"
    android:label="@string/my_app_name"
    android:launchMode="singleTop">
    <!-- We reference a drawable resource here, so we must use android:resource -->
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.APP_LOGO"
        android:resource="@drawable/my_app_logo"/>
    <!-- For these non-resource options, use android:value -->
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.PARSE_LOGIN_ENABLED"
        android:value="true"/>
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.PARSE_LOGIN_EMAIL_AS_USERNAME"
        android:value="true"/>
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.PARSE_LOGIN_HELP_TEXT"
        android:value="@string/password_reset_text"/>
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.MIN_PASSWORD_LENGTH"
        android:value="8"/>
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.FACEBOOK_LOGIN_ENABLED"
        android:value="true"/>
    <!-- We reference a string-array resource here, so we must use android:resource -->
    <meta-data
        android:name="com.parse.ui.ParseLoginActivity.FACEBOOK_LOGIN_PERMISSIONS"
        android:resource="@array/my_facebook_permissions"/>
</activity>

For the Facebook permission array, you also need to have this in your res/values/strings.xml:

<resources>
    <string-array name="my_facebook_permissions">
        <item>public_profile</item>
        <item>user_friends</item>
    </string-array>
</resources>

Configure by Code

You can configure the ParseLoginActivity by code using the ParseLoginBuilder. You specify the options on the builder, and then call build() to generate an Intent that can be used to start the ParseLoginActivity. We’ve provided a sample app, ParseLoginSampleCodeCustomization demonstrating this use case. The options in ParseLoginBuilder are the same as those in activity metadata customization. If you specify options in both code and activity metadata, the options in code take precedence.

ParseLoginBuilder builder = new ParseLoginBuilder(ProfileActivity.this);
Intent parseLoginIntent = builder.setAppLogo(R.drawable.my_app_logo)
    .setParseLoginEnabled(true)
    .setParseLoginButtonText("Go")
    .setParseSignupButtonText("Register")
    .setParseLoginHelpText("Forgot password?")
    .setParseLoginInvalidCredentialsToastText("You email and/or password is not correct")
    .setParseLoginEmailAsUsername(true)
    .setParseSignupSubmitButtonText("Submit registration")
    .setFacebookLoginEnabled(true)
    .setFacebookLoginButtonText("Facebook")
    .setFacebookLoginPermissions(Arrays.asList("public_profile", "user_friends"))
    .setTwitterLoginEnabled(true)
    .setTwitterLoginButtontext("Twitter")
    .build();
startActivityForResult(parseLoginIntent, 0);

Configure by Overriding Layout Resource Files

You can override any layout resources by having files with the same name as those in the ParseLoginUI library project. This is useful if you want to add a background image, or reposition the login components on the screen. The Android build process will automatically merge resource files with the same name, giving your app project’s files precedence. The top-level layout files are:

  • com_parse_ui_parse_login_fragment.xml: If you do not use certain login methods (username/password, Facebook, or Twitter), you can remove the corresponding UI elements from this layout.
  • com_parse_ui_parse_signup_fragment.xml: You can add additional input fields in the signup form here. If you do, you also need add code to ParseSignupFragment to copy that data into the ParseUser object.
  • com_parse_ui_parse_login_help_fragment.xml: You can change the message for password reset.

We’ve provided another sample app, ParseLoginSampleLayoutOverride showing how to do this. This sample app only has a Facebook login button in com_parse_ui_parse_login_fragment.xml, and adds a background image to the login screens.

ParseQueryAdapter

To display collections of data, we provide an implementation of Adapter in the Parse Android SDK. Instead of using a basic ListAdapter backed by a static array of objects, our ParseQueryAdapter provides a layer of abstraction and allows you to easily display data from one of your Parse classes in your AdapterView of choice (e.g. ListView or GridView).

To use a ParseQueryAdapter to display data in an Activity, follow the steps outlined below in your Activity’s onCreate:

  1. Instantiate a ParseQueryAdapter.
  2. Customize it as necessary (see the below subsections for detailed instructions to display data from specific queries, change the UI of the Views to be displayed, and more).
  3. Set your new Adapter on your AdapterView with setAdapter().

When the AdapterView is attached to the window, your ParseQueryAdapter will automatically fetch the first set of data. This subclass simplifies the code that would otherwise be involved with:

  1. Pagination, with a row that can be tapped to load the next page.
  2. Configurable downloading and displaying of remote images within rows.
  3. Automatic loading and management of the Parse objects array.
  4. Callbacks from major events in the data cycle.

Consider the following code, which sets up a very simple ParseQueryAdapter to display data in a ListView. You can be up and running with a functional ListView full of data with very little configuration.

// Inside an Activity
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  // Uses a layout with a ListView (id: "listview"), which uses our Adapter.
  setContentView(R.layout.main);

  ParseQueryAdapter<ParseObject> adapter = new ParseQueryAdapter<ParseObject>(this, "Instrument");
  adapter.setTextKey("name");
  adapter.setImageKey("photo");

  ListView listView = (ListView) findViewById(R.id.listview);
  listView.setAdapter(adapter);
}

This view will display a list of Instruments by name. Notice all the code that we’re not writing: we can skip the logic to fetch each consecutive page of results, to manually update and maintain the backing data array, to download images in the background and set the image data on UI widgets, and to handle touch events to trigger loading the next page of results.

The ParseQueryAdapter can be configured to customize what data to use, how to display it, and what to do before and after it’s been fetched. Read on to see what you can do, and how to tweak a ParseQueryAdapter to fit all of your needs.

Customizing the Query

By default, the simplest ParseQueryAdapter constructor takes a Context and a Parse class name. All ParseObjects in that class are then fetched and displayed in order of their createdAt timestamps.

To change this behavior, we drew from the functionality of an ArrayAdapter: but instead of taking in a vanilla array of objects to be displayed by the adapter, ParseQueryAdapter can also take a QueryFactory class which returns a ParseQuery you define. Pass that into the constructor, and the adapter will then use that query to determine which objects to fetch and display.

See below for an example setting up a ParseQueryAdapter to display only punk and metal bands with four or more members, ordered by number of records sold:

ParseQueryAdapter<ParseObject> adapter =
  new ParseQueryAdapter<ParseObject>(this, new ParseQueryAdapter.QueryFactory<ParseObject>() {
    public ParseQuery<ParseObject> create() {
      // Here we can configure a ParseQuery to our heart's desire.
      ParseQuery query = new ParseQuery("Band");
      query.whereContainedIn("genre", Arrays.asList({ "Punk", "Metal" }));
      query.whereGreaterThanOrEqualTo("memberCount", 4);
      query.orderByDescending("albumsSoldCount");
      return query;
    }
  });

Customizing the Rows

The default layout for the individual Views in your AdapterView is a simple LinearLayout with a ParseImageView and a TextView. If setTextKey(String) is used with the ParseQueryAdapter, its parameter will be used to select which key on your ParseObject is displayed in the TextView. Similarly, if setImageKey(String) is used, its parameter will be used to determine the image displayed in the ImageView.

One way to customize the rows is to override getItemView(ParseObject, View, ViewGroup) or getNextPageView(View, ViewGroup) and call the superclass’s implementation of the appropriate method to do the heavy lifting. If you provide your own layout to the superclass’s implementation, note that getItemView(ParseObject, View, ViewGroup) and getNextPageView(View, ViewGroup) expect a TextView (id: android.R.id.text1) if the textKey is set and a ParseImageView (id: android.R.id.icon) if the imageKey is set.

Here, we inflate and configure a layout of our own, with a TextView, a ParseImageView, and an extra “description” TextView (id: R.id.description):

@Override
public View getItemView(ParseObject object, View v, ViewGroup parent) {
  if (v == null) {
    v = View.inflate(getContext(), R.layout.adapter_item, null);
  }

  // Take advantage of ParseQueryAdapter's getItemView logic for
  // populating the main TextView/ImageView.
  // The IDs in your custom layout must match what ParseQueryAdapter expects
  // if it will be populating a TextView or ImageView for you.
  super.getItemView(object, v, parent);

  // Do additional configuration before returning the View.
  TextView descriptionView = (TextView) v.findViewById(R.id.description);
  descriptionView.setText(object.getString("description"));
  return v;
}

Another way to customize the rows is to have complete control over the look of the rows by overriding ParseQueryAdapter’s methods and ignoring the superclass’s implementation entirely. In this example, our item views are simply rows where the color is defined by the ParseObject:

@Override
public View getItemView(ParseObject object, View v, ViewGroup parent) {
  if (v == null) {
    v = View.inflate(getContext(), R.layout.adapter_item, null);
  }
  v.setBackgroundColor(object.getInt("color"));
  return v;
}

@Override
public View getNextPageView(View v, ViewGroup parent) {
  if (v == null) {
    // R.layout.adapter_next_page contains an ImageView with a custom graphic
    // and a TextView.
    v = View.inflate(getContext(), R.layout.adapter_next_page, null);
  }
  TextView textView = (TextView) v.findViewById(R.id.nextPageTextViewId);
  textView.setText("Loaded " + getCount() + " rows. Get more!");
  return v;
}

Loading Remote Images in Rows

ParseQueryAdapter makes it simple to display remote images. By calling setImageKey(String), you can pass in a key name on your ParseObject which should contain a ParseFile containing an image to be fetched from Parse and loaded into the ParseImageView of the corresponding row.

The image will download asynchronously, and the appropriate ParseImageView will be updated in the background. As the user scrolls and rows are recycled by the adapter, images will be fetched as rows become visible and assigned ParseObjects.

You can define a placeholder image to be used when the image fetch has not yet completed. Call setPlaceholder(Drawable) on your ParseQueryAdapter to use the specified Drawable as a fallback image.

Lifecycle Methods

We expose two hooks in the data lifecycle of the Adapter for you to execute custom logic — right before we query Parse for your data and right after the fetched objects have been loaded from the query. These methods are particularly useful for toggling some loading UI.

An OnQueryLoadListener can be set via setOnQueryLoadListener(OnQueryLoadListener), which provides onLoading() and onLoaded(List<ParseObject>, Exception) methods for implementation.

Pagination

Pagination ensures that the table only gets one page of objects at a time. You can set the number of objects are in a page by calling setObjectsPerPage(int).

The query is automatically altered to apply pagination, and a pagination row appears at the bottom of the AdapterView to allow users to load the next page.

Pagination is turned on by default. To turn it off, call setPaginationEnabled(false). With pagination turned off, the ParseQueryAdapter will use the default ParseQuery limit of 100 items.

Auto-loading of Data

When the AdapterView that your ParseQueryAdapter is set on is attached to a window, the ParseQueryAdapter’s loadObjects() method is automatically called, triggering the fetching of the first page of results. To disable this behavior (perhaps to delay the fetching of data, or run some custom logic ahead of time), just call setAutoload(false) and call loadObjects() manually if autoload is disabled.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Data

We’ve designed the Parse SDKs so that you typically don’t need to worry about how data is saved while using the client SDKs. Simply add data to the Parse Object, and it’ll be saved correctly.

Nevertheless, there are some cases where it’s useful to be aware of how data is stored on the Parse platform.

Data Storage

Internally, Parse stores data as JSON, so any datatype that can be converted to JSON can be stored on Parse. Refer to the Data Types in Objects section of this guide to see platform-specific examples.

Keys including the characters $ or ., along with the key __type key, are reserved for the framework to handle additional types, so don’t use those yourself. Key names must contain only numbers, letters, and underscore, and must start with a letter. Values can be anything that can be JSON-encoded.

Data Type Lock-in

When a class is initially created, it doesn’t have an inherent schema defined. This means that for the first object, it could have any types of fields you want.

However, after a field has been set at least once, that field is locked into the particular type that was saved. For example, if a User object is saved with field name of type String, that field will be restricted to the String type only (the server will return an error if you try to save anything else).

One special case is that any field can be set to null, no matter what type it is.

The Data Browser

The Data Browser is the web UI where you can update and create objects in each of your apps. Here, you can see the raw JSON values that are saved that represents each object in your class.

When using the interface, keep in mind the following:

  • The objectId, createdAt, updatedAt fields cannot be edited (these are set automatically).
  • The value “(empty)” denotes that the field has not been set for that particular object (this is different than null).
  • You can remove a field’s value by hitting your Delete key while the value is selected.

The Data Browser is also a great place to test the Cloud Code validations contained in your Cloud Code functions (such as beforeSave). These are run whenever a value is changed or object is deleted from the Data Browser, just as they would be if the value was changed or deleted from your client code.

Importing Data

You may import data into your Parse app by using CSV or JSON files. To create a new class with data from a CSV or JSON file, go to the Data Browser and click the “Import” button on the left hand column.

The JSON format is an array of objects in our REST format or a JSON object with a results that contains an array of objects. It must adhere to the JSON standard. A file containing regular objects could look like:

{ "results": [
  {
    "score": 1337,
    "playerName": "Sean Plott",
    "cheatMode": false,
    "createdAt": "2012-07-11T20:56:12.347Z",
    "updatedAt": "2012-07-11T20:56:12.347Z",
    "objectId": "fchpZwSuGG"
  }]
}

Objects in either format should contain keys and values that also satisfy the following:

  • Key names must contain only numbers, letters, and underscore, and must start with a letter.
  • No value may contain a hard newline ‘\n’.

Normally, when objects are saved to Parse, they are automatically assigned a unique identifier through the objectId field, as well as a createdAt field and updatedAt field which represent the time that the object was created and last modified in the Parse Cloud. These fields can be manually set when data is imported from a JSON file. Please keep in mind the following:

  • Use a unique 10 character alphanumeric string as the value of your objectId fields.
  • Use a UTC timestamp in the ISO 8601 format when setting a value for the createdAt field or the updatedAt field.

In addition to the exposed fields, objects in the Parse User class can also have the bcryptPassword field set. The value of this field is a String that is the bcrypt hashed password + salt in the modular crypt format described in this StackOverflow answer. Most OpenSSL based bcrypt implementations should have built-in methods to produce these strings.

A file containing a User object could look like:

{ "results":
  [{
    "username": "cooldude",
    "createdAt": "1983-09-13T22:42:30.548Z",
    "updatedAt": "2015-09-04T10:12:42.137Z",
    "objectId": "ttttSEpfXm",
    "sessionToken": "dfwfq3dh0zwe5y2sqv514p4ib",
    "bcryptPassword": "$2a$10$ICV5UeEf3lICfnE9W9pN9.O9Ved/ozNo7G83Qbdk5rmyvY8l16MIK"
  }]
}

Note that in CSV the import field types are limited to String, Boolean, and Number.

Exporting your Data

You can request an export of your data at any time from your app’s Settings page. The data export runs at a lower priority than production queries, so if your app is still serving queries, production traffic will always be given a higher priority, which may slow down the delivery of your data export.

Export Formats

Each collection will be exported in the same JSON format used by our REST API and delivered in a single zipped file. Since data is stored internally as JSON, this allows us to ensure that the export closely matches how the data is saved to Parse. Other formats such as CSV cannot represent all of the data types supported by Parse without losing information. If you’d like to work with your data in CSV format, you can use any of the JSON-to-CSV converters available widely on the web.

Offline Analysis

For offline analysis of your data, we highly recommend using alternate ways to access your data that do not require extracting the entire collection at once. For example, you can try exporting only the data that has changed since your last export. Here are some ways of achieving this:

  • Use the JavaScript SDK in a node app. Parse.Query.each() will allow you to extract every single object that matches a query. You can use date constraints to make sure the query only matches data that has been updated since you last ran this app. Your node app can write this data to disk for offline analysis.

  • Use the REST API in a script. You can run queries against your class and use skip/limit to page through results, which can then be written to disk for offline analysis. You can again use date constraints to make sure only newly updated data is extracted.

  • If the above two options do not fit your needs, you can try using the Data Browser to export data selectively. Use the Funnel icon to create a filter for the specific data that you need to export, such as newly updated objects. Once the filter has been applied, click on the Export data icon on the upper right of your Data Browser. This type of export will only include the objects that match your criteria.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Relations

There are three kinds of relationships. One-to-one relationships enable one object to be associated with another object. One-to-many relationships enable one object to have many related objects. Finally, many-to-many relationships enable complex relationships among many objects.

There are four ways to build relationships in Parse:

One-to-Many

When you’re thinking about one-to-many relationships and whether to implement Pointers or Arrays, there are several factors to consider. First, how many objects are involved in this relationship? If the “many” side of the relationship could contain a very large number (greater than 100 or so) of objects, then you have to use Pointers. If the number of objects is small (fewer than 100 or so), then Arrays may be more convenient, especially if you typically need to get all of the related objects (the “many” in the “one-to-many relationship”) at the same time as the parent object.

Using Pointers

Let’s say we have a game app. The game keeps track of the player’s score and achievements every time she chooses to play. In Parse, we can store this data in a single Game object. If the game becomes incredibly successful, each player will store thousands of Game objects in the system. For circumstances like this, where the number of relationships can be arbitrarily large, Pointers are the best option.

Suppose in this game app, we want to make sure that every Game object is associated with a Parse User. We can implement this like so:

ParseObject game = new ParseObject("Game");
game.put("createdBy", ParseUser.getCurrentUser());

PFObject *game= [PFObject objectWithClassName:@"Game"];
[game setObject:[PFUser currentUser] forKey:@"createdBy"];

let game = PFObject(className:"Game")
game["createdBy"] = PFUser.currentUser()
$game = ParseObject::create("Game");
$game->set("createdBy", ParseUser::getCurrentUser());
var game = new ParseObject("Game");
game["createdBy"] = ParseUser.CurrentUser;
var game = new Parse.Object("Game");
game.set("createdBy", Parse.User.current());
# No REST API example
// No C++ example

We can obtain all of the Game objects created by a Parse User with a query:

ParseQuery<ParseObject> gameQuery = ParseQuery.getQuery("Game");
gameQuery.whereEqualTo("createdBy", ParseUser.getCurrentUser());

PFQuery *gameQuery = [PFQuery queryWithClassName:@"Game"];
[gameQuery whereKey:@"createdBy" equalTo:[PFUser currentUser]];

let gameQuery = PFQuery(className:"Game")
if let user = PFUser.currentUser() {
  gameQuery.whereKey("createdBy", equalTo: user)
}
$gameQuery = new ParseQuery("Game");
$gameQuery->equalTo("createdBy", ParseUser::getCurrentUser());
var query = ParseObject.getQuery("Game").WhereEqualTo("createdBy", ParseUser.CurrentUser);
var query = new Parse.Query("Game");
query.equalTo("createdBy", Parse.User.current());
# No REST API example
// No C++ example

And, if we want to find the Parse User who created a specific Game, that is a lookup on the createdBy key:

// say we have a Game object
ParseObject game = ...

// getting the user who created the Game
ParseUser createdBy = game.getUser("createdBy");

// say we have a Game object
PFObject *game = ...

// getting the user who created the Game
PFUser *createdBy = [game objectForKey@"createdBy"];

// say we have a Game object
let game = ...

// getting the user who created the Game
let createdBy = game["createdBy"]
// say we have a Game object
$game = ...

// getting the user who created the Game
$user = $game["createdBy"];
// say we have a Game object
ParseObject game = ...

// getting the user who created the Game
ParseUser user = game["createdBy"];
// say we have a Game object
var game = ...

// getting the user who created the Game
var user = game.get("createdBy");
# No REST API example
// No C++ example

For most scenarios, Pointers will be your best bet for implementing one-to-many relationships.

Using Arrays

Arrays are ideal when we know that the number of objects involved in our one-to-many relationship are going to be small. Arrays will also provide some productivity benefit via the includeKey parameter. Supplying the parameter will enable you to obtain all of the “many” objects in the “one-to-many” relationship at the same time that you obtain the “one” object. However, the response time will be slower if the number of objects involved in the relationship turns out to be large.

Suppose in our game, we enabled players to keep track of all the weapons their character has accumulated as they play, and there can only be a dozen or so weapons. In this example, we know that the number of weapons is not going to be very large. We also want to enable the player to specify the order in which the weapons will appear on screen. Arrays are ideal here because the size of the array is going to be small and because we also want to preserve the order the user has set each time they play the game:

Let’s start by creating a column on our Parse User object called weaponsList.

Now let’s store some Weapon objects in the weaponsList:

// let's say we have four weapons
ParseObject scimitar = ...
ParseObject plasmaRifle = ...
ParseObject grenade = ...
ParseObject bunnyRabbit = ...

// stick the objects in an array
ArrayList<ParseObject> weapons = new ArrayList<ParseObject>();
weapons.add(scimitar);
weapons.add(plasmaRifle);
weapons.add(grenade);
weapons.add(bunnyRabbit);

// store the weapons for the user
ParseUser.getCurrentUser().put("weaponsList", weapons);

// let's say we have four weapons
PFObject *scimitar = ...
PFObject *plasmaRifle = ...
PFObject *grenade = ...
PFObject *bunnyRabbit = ...

// stick the objects in an array
NSArray *weapons = @[scimitar, plasmaRifle, grenade, bunnyRabbit];

// store the weapons for the user
[[PFUser currentUser] setObject:weapons forKey:@weaponsList"];

// let's say we have four weapons
let scimitar = ...
let plasmaRifle = ...
let grenade = ...
let bunnyRabbit = ...

// stick the objects in an array
let weapons = [scimitar, plasmaRifle, grenade, bunnyRabbit]

// store the weapons for the user
let user = PFUser.currentUser()
user["weaponsList"] = weapons
// let's say we have four weapons
$scimitar = ...
$plasmaRifle = ...
$grenade = ...
$bunnyRabbit = ...

// stick the objects in an array
$weapons = [$scimitar, $plasmaRifle, $grenade, $bunnyRabbit];

// store the weapons for the user
$user = ParseUser::getCurrentUser();
$user->set("weaponsList", weapons);
// let's say we have four weapons
var scimitar = ...
var plasmaRifle = ...
var grenade = ...
var bunnyRabbit = ...

// stick the objects in an array
var weapons = new List<ParseObject>();
weapons.Add(scimitar);
weapons.Add(plasmaRifle);
weapons.Add(grenade);
weapons.Add(bunnyRabbit);

// store the weapons for the user
var user = ParseUser.CurrentUser;
user.AddRangeToList("weaponsList", weapons);
// let's say we have four weapons
var scimitar = ...
var plasmaRifle = ...
var grenade = ...
var bunnyRabbit = ...

// stick the objects in an array
var weapons = [scimitar, plasmaRifle, grenade, bunnyRabbit];

// store the weapons for the user
var user = Parse.User.current();
user.set("weaponsList", weapons);
# No REST API example
// No C++ example

Later, if we want to retrieve the Weapon objects, it’s just one line of code:

ArrayList<ParseObject> weapons = ParseUser.getCurrentUser().get("weaponsList");

NSArray *weapons = [[PFUser currentUser] objectForKey:@"weaponsList"];

let weapons = PFUser.currentUser()?.objectForKey("weaponsList")
$weapons = ParseUser::getCurrentUser()->get("weaponsList");
var weapons = ParseUser.CurrentUser.Get<IList<Object>>("weaponsList");
var weapons = Parse.User.current().get("weaponsList")
# No REST API example
// No C++ example

Sometimes, we will want to fetch the “many” objects in our one-to-many relationship at the same time as we fetch the “one” object. One trick we could employ is to use the includeKey (or include in Android) parameter whenever we use a Parse Query to also fetch the array of Weapon objects (stored in the weaponsList column) along with the Parse User object:

// set up our query for a User object
ParseQuery<ParseUser> userQuery = ParseUser.getQuery();

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you
// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery.include("weaponsList");

// execute the query
userQuery.findInBackground(new FindCallback<ParseUser>() {
  public void done(List<ParseUser> userList, ParseException e) {
    // userList contains all of the User objects, and their associated Weapon objects, too
  }
});

// set up our query for a User object
PFQuery *userQuery = [PFUser query];

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you

// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
[userQuery includeKey:@"weaponsList"];

// execute the query
[userQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // objects contains all of the User objects, and their associated Weapon objects, too
}];

// set up our query for a User object
let userQuery = PFUser.query();

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you

// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery?.includeKey("weaponsList");

// execute the query
userQuery?.findObjectsInBackgroundWithBlock {
    (objects: [AnyObject]?, error: NSError?) -> Void in
    // objects contains all of the User objects, and their associated Weapon objects, too
}
// set up our query for a User object
$userQuery = ParseUser::query();

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you

// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
$userQuery->includeKey("weaponsList");

// execute the query
$results = $userQuery->find();
// results contains all of the User objects, and their associated Weapon objects, too
// set up our query for a User object
var userQuery = ParseUser.Query;

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you

// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery = userQuery.Include("weaponsList");

// execute the query
IEnumerable<ParseUser> results = await userQuery.FindAsync();
// results contains all of the User objects, and their associated Weapon objects, too
// set up our query for a User object
var userQuery = new Parse.Query(Parse.User);

// configure any constraints on your query...
// for example, you may want users who are also playing with or against you

// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery.include("weaponsList");

// execute the query
userQuery.find({
  success: function(results){
    // results contains all of the User objects, and their associated Weapon objects, too
  }
});
# No REST API example
// No C++ example

You can also get the “one” side of the one-to-many relationship from the “many” side. For example, if we want to find all Parse User objects who also have a given Weapon, we can write a constraint for our query like this:

// add a constraint to query for whenever a specific Weapon is in an array
userQuery.whereEqualTo("weaponsList", scimitar);

// or query using an array of Weapon objects...
userQuery.whereEqualTo("weaponsList", arrayOfWeapons);

// add a constraint to query for whenever a specific Weapon is in an array
[userQuery whereKey:@"weaponsList" equalTo:scimitar];

// or query using an array of Weapon objects...
[userQuery whereKey:@"weaponsList" containedIn:arrayOfWeapons];

// add a constraint to query for whenever a specific Weapon is in an array
userQuery?.whereKey("weaponsList", equalTo: scimitar);

// or query using an array of Weapon objects...
userQuery?.whereKey("weaponsList", containedIn: arrayOfWeapons)
// add a constraint to query for whenever a specific Weapon is in an array
$userQuery->equalTo("weaponsList", $scimitar);

// or query using an array of Weapon objects...
$userQuery->containedIn("weaponsList", $arrayOfWeapons);
// add a constraint to query for whenever a specific Weapon is in an array
userQuery = userQuery.WhereEqualTo("weaponsList", scimitar);

// or query using an array of Weapon objects...
userQuery = userQuery.WhereContainedIn("weaponsList", arrayOfWeapons);
// add a constraint to query for whenever a specific Weapon is in an array
userQuery.equalTo("weaponsList", scimitar);

// or query using an array of Weapon objects...
userQuery.containedIn("weaponsList", arrayOfWeapons);
# No REST API example
// No C++ example

Many-to-Many

Now let’s tackle many-to-many relationships. Suppose we had a book reading app and we wanted to model Book objects and Author objects. As we know, a given author can write many books, and a given book can have multiple authors. This is a many-to-many relationship scenario where you have to choose between Arrays, Parse Relations, or creating your own Join Table.

The decision point here is whether you want to attach any metadata to the relationship between two entities. If you don’t, Parse Relation or using Arrays are going to be the easiest alternatives. In general, using arrays will lead to higher performance and require fewer queries. If either side of the many-to-many relationship could lead to an array with more than 100 or so objects, then, for the same reason Pointers were better for one-to-many relationships, Parse Relation or Join Tables will be better alternatives.

On the other hand, if you want to attach metadata to the relationship, then create a separate table (the “Join Table”) to house both ends of the relationship. Remember, this is information about the relationship, not about the objects on either side of the relationship. Some examples of metadata you may be interested in, which would necessitate a Join Table approach, include:

Using Parse Relations

Using Parse Relations, we can create a relationship between a Book and a few Author objects. In the Data Browser, you can create a column on the Book object of type relation and name it authors.

After that, we can associate a few authors with this book:

// let’s say we have a few objects representing Author objects
ParseObject authorOne =
ParseObject authorTwo =
ParseObject authorThree =

// now we create a book object
ParseObject book = new ParseObject("Book");

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
ParseRelation<ParseObject> relation = book.getRelation("authors");
relation.add(authorOne);
relation.add(authorTwo);
relation.add(authorThree);

// now save the book object
book.saveInBackground();

// let’s say we have a few objects representing Author objects
PFObject *authorOne = …
PFObject *authorTwo = …
PFObject *authorThree = …

// now we create a book object
PFObject *book= [PFObject objectWithClassName:@"Book"];

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
PFRelation *relation = [book relationForKey:@"authors"];
[relation addObject:authorOne];
[relation addObject:authorTwo];
[relation addObject:authorThree];

// now save the book object
[book saveInBackground];

// let’s say we have a few objects representing Author objects
let authorOne = ...
let authorTwo = ...
let authorThree = ...

// now we create a book object
let book = PFObject(className: "Book")

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
let relation = book.relationForKey("authors")
relation.addObject(authorOne)
relation.addObject(authorTwo)
relation.addObject(authorThree)

// now save the book object
book.saveInBackground()
// let’s say we have a few objects representing Author objects
$authorOne = ...
$authorTwo = ...
$authorThree = ...

// now we create a book object
$book = new ParseObject("Book");

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
$relation = $book->getRelation("authors");
$relation->add($authorOne);
$relation->add($authorTwo);
$relation->add($authorThree);

// now save the book object
$book->save();
// let’s say we have a few objects representing Author objects
var authorOne = ...
var authorTwo = ...
var authorThree = ...

// now we create a book object
var book = new ParseObject("Book");

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
var relation = book.GetRelation<ParseObject>("authors");
relation.Add(authorOne);
relation.Add(authorTwo);
relation.Add(authorThree);

// now save the book object
await book.SaveAsync();
// let’s say we have a few objects representing Author objects
var authorOne = ...
var authorTwo = ...
var authorThree = ...

// now we create a book object
var book = new Parse.Object("Book");

// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
var relation = book.relation("authors");
relation.add(authorOne);
relation.add(authorTwo);
relation.add(authorThree);

// now save the book object
book.save();
# No REST API example
// No C++ example

To get the list of authors who wrote a book, create a query:

// suppose we have a book object
ParseObject book = ...

// create a relation based on the authors key
ParseRelation relation = book.getRelation("authors");

// generate a query based on that relation
ParseQuery query = relation.getQuery();

// now execute the query

// suppose we have a book object
PFObject *book = ...

// create a relation based on the authors key
PFRelation *relation = [book relationForKey:@"authors"];

// generate a query based on that relation
PFQuery *query = [relation query];

// now execute the query

// suppose we have a book object
let book = ...

// create a relation based on the authors key
let relation = book.relationForKey("authors")

// generate a query based on that relation
let query = relation.query()

// now execute the query
// suppose we have a book object
$book = ...

// create a relation based on the authors key
$relation = $book->getRelation("authors");

// generate a query based on that relation
$query = $relation->getQuery();

// now execute the query
// suppose we have a book object
var book = ...

// create a relation based on the authors key
var relation = book.GetRelation<ParseObject>("authors");

// generate a query based on that relation
var query = relation.Query;

// now execute the query
// suppose we have a book object
var book = ...

// create a relation based on the authors key
var relation = book.relation("authors");

// generate a query based on that relation
var query = relation.query();

// now execute the query
# No REST API example
// No C++ example

Perhaps you even want to get a list of all the books to which an author contributed. You can create a slightly different kind of query to get the inverse of the relationship:

// suppose we have a author object, for which we want to get all books
ParseObject author = ...

// first we will create a query on the Book object
ParseQuery<ParseObject> query = ParseQuery.getQuery("Book");

// now we will query the authors relation to see if the author object we have
// is contained therein
query.whereEqualTo("authors", author);

// suppose we have a author object, for which we want to get all books
PFObject *author = ...

// first we will create a query on the Book object
PFQuery *query = [PFQuery queryWithClassName:@"Book"];

// now we will query the authors relation to see if the author object
// we have is contained therein
[query whereKey:@"authors" equalTo:author];

// suppose we have a author object, for which we want to get all books
let author = ...

// first we will create a query on the Book object
let query = PFQuery(className: "Book")

// now we will query the authors relation to see if the author object
// we have is contained therein
query?.whereKey("authors", equalTo: author)
// suppose we have a author object, for which we want to get all books
$author = ...

// first we will create a query on the Book object
$query = new ParseQuery("Book");

// now we will query the authors relation to see if the author object we have
// is contained therein
$query->equalTo("authors", $author);
// suppose we have a author object, for which we want to get all books
var author = ...

// first we will create a query on the Book object
var query = ParseObject.GetQuery("Book");

// now we will query the authors relation to see if the author object we have
// is contained therein
query = query.WhereEqualTo("authors", author);
// suppose we have a author object, for which we want to get all books
var author = ...

// first we will create a query on the Book object
var query = new Parse.Query("Book");

// now we will query the authors relation to see if the author object we have
// is contained therein
query.equalTo("authors", author);
# No REST API example
// No C++ example

Using Join Tables

There may be certain cases where we want to know more about a relationship. For example, suppose we were modeling a following/follower relationship between users: a given user can follow another user, much as they would in popular social networks. In our app, we not only want to know if User A is following User B, but we also want to know when User A started following User B. This information could not be contained in a Parse Relation. In order to keep track of this data, you must create a separate table in which the relationship is tracked. This table, which we will call Follow, would have a from column and a to column, each with a pointer to a Parse User. Alongside the relationship, you can also add a column with a Date object named date.

Now, when you want to save the following relationship between two users, create a row in the Follow table, filling in the from, to, and date keys appropriately:

// suppose we have a user we want to follow
ParseUser otherUser = ...

// create an entry in the Follow table
ParseObject follow = new ParseObject("Follow");
follow.put("from", ParseUser.getCurrentUser());
follow.put("to", otherUser);
follow.put("date", Date());
follow.saveInBackground();

// suppose we have a user we want to follow
PFUser *otherUser = ...

// create an entry in the Follow table
PFObject *follow = [PFObject objectWithClassName:@"Follow"];
[follow setObject:[PFUser currentUser]  forKey:@"from"];
[follow setObject:otherUser forKey:@"to"];
[follow setObject:[NSDate date] forKey@"date"];
[follow saveInBackground];

// suppose we have a user we want to follow
let otherUser = ...

// create an entry in the Follow table
let follow = PFObject(className: "Follow")
follow.setObject(PFUser.currentUser()!, forKey: "from")
follow.setObject(otherUser, forKey: "to")
follow.setObject(NSDate(), forKey: "date")
follow.saveInBackground()
// suppose we have a user we want to follow
$otherUser = ...

// create an entry in the Follow table
$follow = new ParseObject("Follow");
$follow->set("from", ParseUser::getCurrentUser());
$follow->set("to", $otherUser);
$follow->set("date", new DateTime());
$follow->save();
// suppose we have a user we want to follow
ParseUser otherUser = ...

// create an entry in the Follow table
var follow = new ParseObject("Follow");
follow["from"] = ParseUser.CurrentUser;
follow["to"] = otherUser;
follow["date"] = DateTime.UtcNow;
await follow.SaveAsync();
var otherUser = ...

// create an entry in the Follow table
var follow = new Parse.Object("Follow");
follow.set("from", Parse.User.current());
follow.set("to", otherUser);
follow.set("date", Date());
follow.save();
# No REST API example
// No C++ example

If we want to find all of the people we are following, we can execute a query on the Follow table:

// set up the query on the Follow table
ParseQuery<ParseObject> query = ParseQuery.getQuery("Follow");
query.whereEqualTo("from", ParseUser.getCurrentUser());

// execute the query
query.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> followList, ParseException e) {

    }
});

// set up the query on the Follow table
PFQuery *query = [PFQuery queryWithClassName:@"Follow"];
[query whereKey:@"from" equalTo:[PFUser currentUser]];

// execute the query
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  for(PFObject *o in objects) {
    // o is an entry in the Follow table
    // to get the user, we get the object with the to key
    PFUser *otherUser = [o objectForKey@"to"];

    // to get the time when we followed this user, get the date key
    PFObject *when = [o objectForKey@"date"];
  }
}];

// set up the query on the Follow table
let query = PFQuery(className: "Follow")
query.whereKey("from", equalTo: PFUser.currentUser()!)

// execute the query
query.findObjectsInBackgroundWithBlock{
	(objects: [AnyObject]?, error: NSError?) -> Void in
    if let objects = objects {
        for o in objects {
            // o is an entry in the Follow table
            // to get the user, we get the object with the to key
            let otherUse = o.objectForKey("to") as? PFUser

            // to get the time when we followed this user, get the date key
            let when = o.objectForKey("date") as? PFObject
        }
    }
}
// set up the query on the Follow table
$query = new ParseQuery("Follow");
$query->equalTo("from", ParseUser::getCurrentUser());

// execute the query
$results = $query->find();
// set up the query on the Follow table
ParseQuery<ParseObject> query = ParseQuery.getQuery("Follow");
query = query.WhereEqualTo("from", ParseUser.CurrentUser);

// execute the query
IEnumerable<ParseObject> results = await query.FindAsync();
var query = new Parse.Query("Follow");
query.equalTo("from", Parse.User.current());
query.find({
  success: function(users){
    ...
  }
});
# No REST API example
// No C++ example

It’s also pretty easy to find all the users that are following the current user by querying on the to key:

// set up the query on the Follow table
ParseQuery<ParseObject> query = ParseQuery.getQuery("Follow");
query.whereEqualTo("to", ParseUser.getCurrentUser());

// execute the query
query.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> followList, ParseException e) {

    }
});

// set up the query on the Follow table
PFQuery *query = [PFQuery queryWithClassName:@"Follow"];
[query whereKey:@"to" equalTo:[PFUser currentUser]];

[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  for(PFObject *o in objects) {
     // o is an entry in the Follow table
     // to get the user, we get the object with the from key
    PFUser *otherUser = [o objectForKey@"from"];

    // to get the time the user was followed, get the date key
    PFObject *when = [o objectForKey@"date"];
  }
}];

// set up the query on the Follow table
let query = PFQuery(className: "Follow")
query.whereKey("to", equalTo: PFUser.currentUser()!)

query.findObjectsInBackgroundWithBlock{
	(objects: [AnyObject]?, error: NSError?) -> Void in
    if let objects = objects {
        for o in objects {
            // o is an entry in the Follow table
            // to get the user, we get the object with the to key
            let otherUse = o.objectForKey("to") as? PFUser

            // to get the time when we followed this user, get the date key
            let when = o.objectForKey("date") as? PFObject
        }

    }
}
// create an entry in the Follow table
$query = new ParseQuery("Follow");
$query->equalTo("to", ParseUser::getCurrentUser());
$results = $query->find();
// create an entry in the Follow table
var query = ParseObject.GetQuery("Follow")
    .WhereEqualTo("to", ParseUser.CurrentUser);
IEnumerable<ParseObject> results = await query.FindAsync();
// create an entry in the Follow table
var query = new Parse.Query("Follow");
query.equalTo("to", Parse.User.current());
query.find({
  success: function(users){
    ...
  }
});
# No REST API example
// No C++ example

Using an Array

Arrays are used in Many-to-Many relationships in much the same way that they are for One-to-Many relationships. All objects on one side of the relationship will have an Array column containing several objects on the other side of the relationship.

Suppose we have a book reading app with Book and Author objects. The Book object will contain an Array of Author objects (with a key named authors). Arrays are a great fit for this scenario because it’s highly unlikely that a book will have more than 100 or so authors. We will put the Array in the Book object for this reason. After all, an author could write more than 100 books.

Here is how we save a relationship between a Book and an Author.

// let's say we have an author
ParseObject author = ...

// and let's also say we have an book
ParseObject book = ...

// add the author to the authors list for the book
book.put("authors", author);

// let's say we have an author
PFObject *author = ...

// and let's also say we have an book
PFObject *book = ...

// add the author to the authors list for the book
[book addObject:author forKey:@"authors"];

// let's say we have an author
let author = ...

// and let's also say we have an book
let book = ...

// add the author to the authors list for the book
book.addObject(author, forKey: "authors")
// let's say we have an author
$author = ...

// and let's also say we have an book
$book = ...

// add the author to the authors list for the book
$book->addUnique("authors", array($author));
// let's say we have an author
var author = ...

// and let's also say we have an book
var book = ...

// add the author to the authors list for the book
book.AddToList("authors", author);
// let's say we have an author
var author = ...

// and let's also say we have an book
var book = ...

// add the author to the authors list for the book
book.add("authors", author);
# No REST API example
// No C++ example

Because the author list is an Array, you should use the includeKey (or include on Android) parameter when fetching a Book so that Parse returns all the authors when it also returns the book:

// set up our query for the Book object
ParseQuery bookQuery = ParseQuery.getQuery("Book");

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");

// execute the query
bookQuery.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> bookList, ParseException e) {
    }
});

// set up our query for the Book object
PFQuery *bookQuery = [PFQuery queryWithClassName:@"Book"];

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
[bookQuery includeKey:@"authors"];

// execute the query
[bookQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // objects is all of the Book objects, and their associated
    // Author objects, too
}];

// set up our query for the Book object
let bookQuery = PFQuery(className: "Book")

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery.includeKey("authors")

// execute the query
bookQuery.findObjectsInBackgroundWithBlock{
    (objects: [AnyObject]?, error: NSError?) -> Void in
    // objects is all of the Book objects, and their associated
    // Author objects, too
}
// set up our query for the Book object
$bookQuery = new ParseQuery("Book");

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
$bookQuery->includeKey("authors");

// execute the query
$books= $bookQuery->find();
// set up our query for the Book object
var bookQuery = ParseObject.GetQuery("Book");

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery = bookQuery.Include("authors");

// execute the query
IEnumerable<ParseObject> books= await bookQuery.FindAsync();
// set up our query for the Book object
var bookQuery = new Parse.Query("Book");

// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");

// execute the query
bookQuery.find({
  success: function(books){
    ...
  }
});
# No REST API example
// No C++ example

At that point, getting all the Author objects in a given Book is a pretty straightforward call:

ArrayList<ParseObject> authorList = book.getList("authors");

NSArray *authorList = [book objectForKey@"authors"];

let authorList = book.objectForKey("authors") as? NSArray
$authorList = $book->get("authors");
var authorList = book.Get<List<ParseObject>>("authors");
var authorList = book.get("authors")
# No REST API example
// No C++ example

Finally, suppose you have an Author and you want to find all the Book objects in which she appears. This is also a pretty straightforward query with an associated constraint:

// set up our query for the Book object
ParseQuery bookQuery = ParseQuery.getQuery("Book");

// configure any constraints on your query...
booKQuery.whereEqualTo("authors", author);

// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");

// execute the query
bookQuery.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> bookList, ParseException e) {

    }
});

// suppose we have an Author object
PFObject *author = ...

// set up our query for the Book object
PFQuery *bookQuery = [PFQuery queryWithClassName:@"Book"];

// configure any constraints on your query...
[bookQuery whereKey:@"authors" equalTo:author];

// tell the query to fetch all of the Author objects along with the Book
[bookQuery includeKey:@"authors"];

// execute the query
[bookQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // objects is all of the Book objects, and their associated Author objects, too
}];

// suppose we have an Author object
let author = ...

// set up our query for the Book object
let bookQuery = PFQuery(className: "Book")

// configure any constraints on your query...
bookQuery.whereKey("authors", equalTo: author)

// tell the query to fetch all of the Author objects along with the Book
bookQuery.includeKey("authors")

// execute the query
bookQuery.findObjectsInBackgroundWithBlock{
	(objects: [AnyObject]?, error: NSError?) -> Void in
    // objects is all of the Book objects, and their associated Author objects, too
}
// set up our query for the Book object
$bookQuery = new ParseQuery("Book");

// configure any constraints on your query...
$bookQuery->equalTo("authors", $author);

// tell the query to fetch all of the Author objects along with the Book
$bookQuery->includeKey("authors");

// execute the query
$books = $bookQuery->find();
// set up our query for the Book object
var bookQuery = ParseObject.GetQuery("Book");

// configure any constraints on your query...
bookQuery = bookQuery.WhereEqualTo("authors", author);

// tell the query to fetch all of the Author objects along with the Book
bookQuery = bookQuery.Include("authors");

// execute the query
IEnumerable<ParseObject> books = await bookQuery.FindAsync();
// set up our query for the Book object
var bookQuery = new Parse.Query("Book");

// configure any constraints on your query...
bookQuery.equalTo("authors", author);

// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");

// execute the query
bookQuery.find({
  success: function(books){
    ...
  }
});
# No REST API example
// No C++ example

One-to-One

In Parse, a one-to-one relationship is great for situations where you need to split one object into two objects. These situations should be rare, but two examples include:

  • Limiting visibility of some user data. In this scenario, you would split the object in two, where one portion of the object contains data that is visible to other users, while the related object contains data that is private to the original user (and protected via ACLs).
  • Splitting up an object for size. In this scenario, your original object is greater than the 128K maximum size permitted for an object, so you decide to create a secondary object to house extra data. It is usually better to design your data model to avoid objects this large, rather than splitting them up. If you can’t avoid doing so, you can also consider storing large data in a Parse File.

Thank you for reading this far. We apologize for the complexity. Modeling relationships in data is a hard subject, in general. But look on the bright side: it’s still easier than relationships with people.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Handling Errors

Many of the methods on ParseObject, including save(), delete(), and get() will throw a ParseException on an invalid request, such as deleting or editing an object that no longer exists in the cloud, or when there is a network failure preventing communication with the Parse Cloud. You will need to catch and deal with these exceptions.

For more details, look at the Android API.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Security

As your app development progresses, you will want to use Parse’s security features in order to safeguard data. This document explains the ways in which you can secure your apps.

If your app is compromised, it’s not only you as the developer who suffers, but potentially the users of your app as well. Continue reading for our suggestions for sensible defaults and precautions to take before releasing your app into the wild.

Client vs. Server

When an app first connects to Parse, it identifies itself with an Application ID and a Client key (or REST Key, or .NET Key, or JavaScript Key, depending on which platform you’re using). These are not secret and by themselves they do not secure an app. These keys are shipped as a part of your app, and anyone can decompile your app or proxy network traffic from their device to find your client key. This exploit is even easier with JavaScript — one can simply “view source” in the browser and immediately find your client key.

This is why Parse has many other security features to help you secure your data. The client key is given out to your users, so anything that can be done with just the client key is doable by the general public, even malicious hackers.

The master key, on the other hand, is definitely a security mechanism. Using the master key allows you to bypass all of your app’s security mechanisms, such as class-level permissions and ACLs. Having the master key is like having root access to your app’s servers, and you should guard your master key with the same zeal with which you would guard your production machines’ root password.

The overall philosophy is to limit the power of your clients (using client keys), and to perform any sensitive actions requiring the master key in Cloud Code. You’ll learn how to best wield this power in the section titled Implementing Business Logic in Cloud Code.

A final note: All connections are made with HTTPS and SSL, and Parse will reject all non-HTTPS connections. As a result, you don’t need to worry about man-in-the-middle attacks.

Class-Level Permissions

The second level of security is at the schema and data level. Enforcing security measures at this level will restrict how and when client applications can access and create data on Parse. When you first begin developing your Parse application, all of the defaults are set so that you can be a more productive developer. For example:

  • A client application can create new classes on Parse
  • A client application can add fields to classes
  • A client application can modify or query for objects on Parse

You can configure any of these permissions to apply to everyone, no one, or to specific users or roles in your app. Roles are groups that contain users or other roles, which you can assign to an object to restrict its use. Any permission granted to a role is also granted to any of its children, whether they are users or other roles, enabling you to create an access hierarchy for your apps. Each of the Parse guides includes a detailed description of employing Roles in your apps.

Once you are confident that you have the right classes and relationships between classes in your app, you should begin to lock it down by doing the following:

Almost every class that you create should have these permissions tweaked to some degree. For classes where every object has the same permissions, class-level settings will be most effective. For example, one common use case entails having a class of static data that can be read by anyone but written by no one.

Restricting class creation

As a start, you can configure your application so that clients cannot create new classes on Parse. This is done from the Settings tab on the Data Browser. Scroll down to the App Permissions section and turn off Allow client class creation. Once enabled, classes may only be created from the Data Browser. This will prevent attackers from filling your database with unlimited, arbitrary new classes.

Configuring Class-Level Permissions

Parse lets you specify what operations are allowed per class. This lets you restrict the ways in which clients can access or modify your classes. To change these settings, go to the Data Browser, select a class, and click the “Security” button.

You can configure the client’s ability to perform each of the following operations for the selected class:

  • Read:

    • Get: With Get permission, users can fetch objects in this table if they know their objectIds.

    • Find: Anyone with Find permission can query all of the objects in the table, even if they don’t know their objectIds. Any table with public Find permission will be completely readable by the public, unless you put an ACL on each object.

  • Write:

    • Update: Anyone with Update permission can modify the fields of any object in the table that doesn’t have an ACL. For publicly readable data, such as game levels or assets, you should disable this permission.

    • Create: Like Update, anyone with Create permission can create new objects of a class. As with the Update permission, you’ll probably want to turn this off for publicly readable data.

    • Delete: With this permission, people can delete any object in the table that doesn’t have an ACL. All they need is its objectId.

  • Add fields: Parse classes have schemas that are inferred when objects are created. While you’re developing your app, this is great, because you can add a new field to your object without having to make any changes on the backend. But once you ship your app, it’s very rare to need to add new fields to your classes automatically. You should pretty much always turn off this permission for all of your classes when you submit your app to the public.

For each of the above actions, you can grant permission to all users (which is the default), or lock permissions down to a list of roles and users. For example, a class that should be available to all users would be set to read-only by only enabling get and find. A logging class could be set to write-only by only allowing creates. You could enable moderation of user-generated content by providing update and delete access to a particular set of users or roles.

Object-Level Access Control

Once you’ve locked down your schema and class-level permissions, it’s time to think about how data is accessed by your users. Object-level access control enables one user’s data to be kept separate from another’s, because sometimes different objects in a class need to be accessible by different people. For example, a user’s private personal data should be accessible only to them.

Parse also supports the notion of anonymous users for those apps that want to store and protect user-specific data without requiring explicit login.

When a user logs into an app, they initiate a session with Parse. Through this session they can add and modify their own data but are prevented from modifying other users’ data.

Access Control Lists

The easiest way to control who can access which data is through access control lists, commonly known as ACLs. The idea behind an ACL is that each object has a list of users and roles along with what permissions that user or role has. A user needs read permissions (or must belong to a role that has read permissions) in order to retrieve an object’s data, and a user needs write permissions (or must belong to a role that has write permissions) in order to update or delete that object.

Once you have a User, you can start using ACLs. Remember: Users can be created through traditional username/password signup, through a third-party login system like Facebook or Twitter, or even by using Parse’s automatic anonymous users functionality. To set an ACL on the current user’s data to not be publicly readable, all you have to do is:


PFUser *user = [PFUser currentUser];
user.ACL = [PFACL ACLWithUser:user];

if let user = PFUser.currentUser() {
    user.ACL = PFACL(user: user)
}
ParseUser user = ParseUser.getCurrentUser();
user.setACL(new ParseACL(user));
var user = Parse.User.current();
user.setACL(new Parse.ACL(user));
var user = ParseUser.CurrentUser;
user.ACL = new ParseACL(user);
$user = ParseUser::getCurrentUser();
$user->setACL(new ParseACL($user))
# No command line example
// No C++ example

Most apps should do this. If you store any sensitive user data, such as email addresses or phone numbers, you need to set an ACL like this so that the user’s private information isn’t visible to other users. If an object doesn’t have an ACL, it’s readable and writeable by everyone. The only exception is the _User class. We never allow users to write each other’s data, but they can read it by default. (If you as the developer need to update other _User objects, remember that your master key can provide the power to do this.)

To make it super easy to create user-private ACLs for every object, we have a way to set a default ACL that will be used for every new object you create:


[PFACL setDefaultACL:[PFACL ACL] withAccessForCurrentUser:YES];

PFACL.setDefaultACL(PFACL(), withAccessForCurrentUser: true)
ParseACL.setDefaultACL(new ParseACL(), true);
// not available in the JavaScript SDK
// not available in the .NET SDK
ParseACL::setDefaultACL(new ParseACL(), true);
# No command line example
// No C++ example

If you want the user to have some data that is public and some that is private, it’s best to have two separate objects. You can add a pointer to the private data from the public one.


PFObject *privateData = [PFObject objectWithClassName:@"PrivateUserData"];
privateData.ACL = [PFACL ACLWithUser:[PFUser currentUser]];
[privateData setObject:@"555-5309" forKey:@"phoneNumber"];

[[PFUser currentUser] setObject:privateData forKey:@"privateData"];

if let currentUser = PFUser.currentUser() {
    let privateData = PFObject(className: "PrivateUserData")
    privateData.ACL = PFACL(user: currentUser)
    privateData.setObject("555-5309", forKey: "phoneNumber")
    currentUser.setObject(privateData, forKey: "privateData")
}
ParseObject privateData = new ParseObject("PrivateUserData");
privateData.setACL(new ParseACL(ParseUser.getCurrentUser()));
privateData.put("phoneNumber", "555-5309");

ParseUser.getCurrentUser().put("privateData", privateData);
var privateData = Parse.Object.extend("PrivateUserData");
privateData.setACL(new Parse.ACL(Parse.User.current()));
privateData.set("phoneNumber", "555-5309");

Parse.User.current().set("privateData", privateData);
var privateData = new ParseObject("PrivateUserData");
privateData.ACL = new ParseACL(ParseUser.CurrentUser);
privateData["phoneNumber"] = "555-5309";

ParseUser.CurrentUser["privateData"] =  privateData;
$privateData = ParseObject::create("PrivateUserData");
$privateData->setACL(new ParseACL(ParseUser::getCurrentUser()));
$privateData->set("phoneNumber", "555-5309");

ParseUser::getCurrentUser()->set("privateData", $privateData);
# No command line example
// No C++ example

Of course, you can set different read and write permissions on an object. For example, this is how you would create an ACL for a public post by a user, where anyone can read it:


PFACL *acl = [PFACL ACL];
[acl setPublicReadAccess:true];
[acl setWriteAccess:true forUser:[PFUser currentUser]];

let acl = PFACL()
acl.setPublicReadAccess(true)
if let currentUser = PFUser.currentUser() {
    acl.setWriteAccess(true, forUser: currentUser)
}
ParseACL acl = new ParseACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(ParseUser.getCurrentUser(), true);
var acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(Parse.User.current().id, true);
var acl = new ParseACL();
acl.PublicReadAccess = true;
acl.SetRoleWriteAccess(ParseUser.CurrentUser.ObjectId, true);
$acl = new ParseACL();
$acl->setPublicReadAccess(true);
$acl->setWriteAccess(ParseUser::getCurrentUser(), true);
# No command line example
// No C++ example

Sometimes it’s inconvenient to manage permissions on a per-user basis, and you want to have groups of users who get treated the same (like a set of admins with special powers). Roles are are a special kind of object that let you create a group of users that can all be assigned to the ACL. The best thing about roles is that you can add and remove users from a role without having to update every single object that is restricted to that role. To create an object that is writeable only by admins:


// Assuming you've already created a role called "admins"...
PFACL *acl = [PFACL ACL];
[acl setPublicReadAccess:true];
[acl setWriteAccess:true forRoleWithName:@"admins"];

let acl = PFACL()
acl.setPublicReadAccess(true)
acl.setWriteAccess(true, forRoleWithName: "admins")
// Assuming you've already created a role called "admins"...
ParseACL acl = new ParseACL();
acl.setPublicReadAccess(true);
acl.setRoleWriteAccess("admins", true);
var acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setRoleWriteAccess("admins", true);
var acl = new ParseACL();
acl.PublicReadAccess = true;
acl.SetRoleWriteAccess("admins", true);
$acl = new ParseACL();
$acl->setPublicReadAccess(true);
$acl->setRoleWriteAccessWithName("admins", true);
# No command line example
// No C++ example

Of course, this snippet assumes you’ve already created a role named “admins”. This is often reasonable when you have a small set of special roles set up while developing your app. Roles can also be created and updated on the fly — for example, adding new friends to a “friendOf___” role after each connection is made.

All this is just the beginning. Applications can enforce all sorts of complex access patterns through ACLs and class-level permissions. For example:

  • For private data, read and write access can be restricted to the owner.
  • For a post on a message board, the author and members of the “Moderators” role can have “write” access, and the general public can have “read” access.
  • For logging data that will only be accessed by the developer through the REST API using the master key, the ACL can deny all permissions.
  • Data created by a privileged group of users or the developer, like a global message of the day, can have public read access but restrict write access to an “Administrators” role.
  • A message sent from one user to another can give “read” and “write” access just to those users.

For the curious, here’s the format for an ACL that restricts read and write permissions to the owner (whose objectId is identified by "aSaMpLeUsErId") and enables other users to read the object:

{
    "*": { "read":true },
    "aSaMpLeUsErId": { "read" :true, "write": true }
}

And here’s another example of the format of an ACL that uses a Role:

{
    "role:RoleName": { "read": true },
    "aSaMpLeUsErId": { "read": true, "write": true }
}

Pointer Permissions

Pointer permissions are a special type of class-level permission that create a virtual ACL on every object in a class, based on users stored in pointer fields on those objects. For example, given a class with an owner field, setting a read pointer permission on owner will make each object in the class only readable by the user in that object’s owner field. For a class with a sender and a reciever field, a read pointer permission on the receiver field and a read and write pointer permission on the sender field will make each object in the class readable by the user in the sender and receiver field, and writable only by the user in the sender field.

Given that objects often already have pointers to the user(s) that should have permissions on the object, pointer permissions provide a simple and fast solution for securing your app using data which is already there, that doesn’t require writing any client code or cloud code.

Pointer permissions are like virtual ACLs. They don’t appear in the ACL column, buf if you are familiar with how ACLs work, you can think of them like ACLs. In the above example with the sender and receiver, each object will act as if it has an ACL of:

{
    "<SENDER_USER_ID>": {
        "read": true,
        "write": true
    },
    "<RECEIVER_USER_ID>": {
        "read": true
    }
}

Note that this ACL is not actually created on each object. Any existing ACLs will not be modified when you add or remove pointer permissions, and any user attempting to interact with an object can only interact with the object if both the virtual ACL created by the pointer permissions, and the real ACL already on the object allow the interaction. For this reason, it can sometimes be confusing to combine pointer permissions and ACLs, so we recommend using pointer permissions for classes that don’t have many ACLs set. Fortunately, it’s easy to remove pointer permissions if you later decide to use Cloud Code or ACLs to secure your app.

Requires Authentication permission (requires parse-server >= 2.3.0)

Starting version 2.3.0, parse-server introduces a new Class Level Permission requiresAuthentication. This CLP prevents any non authenticated user from performing the action protected by the CLP.

For example, you want to allow your authenticated users to find and get Announcement’s from your application and your admin role to have all privileged, you would set the CLP:

// POST http://my-parse-server.com/schemas/Announcement
// Set the X-Parse-Application-Id and X-Parse-Master-Key header
// body: 
{
  classLevelPermissions: 
  {
    "find": {
      "requireAuthentication": true,
      "role:admin": true
    },
    "get": {
      "requireAuthentication": true,
      "role:admin": true
    },
    "create": { "role:admin": true },
    "update": { "role:admin": true },
    "delete": { "role:admin": true },
  }
}

Effects:

  • Non authenticated users won’t be able to do anything.
  • Authenticated users (any user with a valid sessionToken) will be able to read all the objects in that class
  • Users belonging to the admin role, will be able to perform all operations.

:warning: Note that this is in no way securing your content, if you allow anyone to login to your server, every client will still be able to query this object.

CLP and ACL interaction

Class-Level Permissions (CLPs) and Access Control Lists (ACLs) are both powerful tools for securing your app, but they don’t always interact exactly how you might expect. They actually represent two separate layers of security that each request has to pass through to return the correct information or make the intended change. These layers, one at the class level, and one at the object level, are shown below. A request must pass through BOTH layers of checks in order to be authorized. Note that despite acting similarly to ACLs, Pointer Permissions are a type of class level permission, so a request must pass the pointer permission check in order to pass the CLP check.

As you can see, whether a user is authorized to make a request can become complicated when you use both CLPs and ACLs. Let’s look at an example to get a better sense of how CLPs and ACLs can interact. Say we have a Photo class, with an object, photoObject. There are 2 users in our app, user1 and user2. Now lets say we set a Get CLP on the Photo class, disabling public Get, but allowing user1 to perform Get. Now let’s also set an ACL on photoObject to allow Read - which includes GET - for only user2.

You may expect this will allow both user1 and user2 to Get photoObject, but because the CLP layer of authentication and the ACL layer are both in effect at all times, it actually makes it so neither user1 nor user2 can Get photoObject. If user1 tries to Get photoObject, it will get through the CLP layer of authentication, but then will be rejected because it does not pass the ACL layer. In the same way, if user2 tries to Get photoObject, it will also be rejected at the CLP layer of authentication.

Now lets look at example that uses Pointer Permissions. Say we have a Post class, with an object, myPost. There are 2 users in our app, poster, and viewer. Lets say we add a pointer permission that gives anyone in the Creator field of the Post class read and write access to the object, and for the myPost object, poster is the user in that field. There is also an ACL on the object that gives read access to viewer. You may expect that this will allow poster to read and edit myPost, and viewer to read it, but viewer will be rejected by the Pointer Permission, and poster will be rejected by the ACL, so again, neither user will be able to access the object.

Because of the complex interaction between CLPs, Pointer Permissions, and ACLs, we recommend being careful when using them together. Often it can be useful to use CLPs only to disable all permissions for a certain request type, and then using Pointer Permissions or ACLs for other request types. For example, you may want to disable Delete for a Photo class, but then put a Pointer Permission on Photo so the user who created it can edit it, just not delete it. Because of the especially complex way that Pointer Permissions and ACLs interact, we usually recommend only using one of those two types of security mechanisms.

Security Edge Cases

There are some special classes in Parse that don’t follow all of the same security rules as every other class. Not all classes follow Class-Level Permissions (CLPs) or Access Control Lists (ACLs) exactly how they are defined, and here those exceptions are documented. Here “normal behavior” refers to CLPs and ACLs working normally, while any other special behaviors are described in the footnotes.

  _User _Installation
Get normal behaviour [1, 2, 3] ignores CLP, but not ACL
Find normal behavior [3] master key only [6]
Create normal behavior [4] ignores CLP
Update normal behavior [5] ignores CLP, but not ACL [7]
Delete normal behavior [5] master key only [7]
Add Field normal behavior normal behavior
  1. Logging in, or /1/login in the REST API, does not respect the Get CLP on the user class. Login works just based on username and password, and cannot be disabled using CLPs.

  2. Retrieving the current user, or becoming a User based on a session token, which are both /1/users/me in the REST API, do not respect the Get CLP on the user class.

  3. Read ACLs do not apply to the logged in user. For example, if all users have ACLs with Read disabled, then doing a find query over users will still return the logged in user. However, if the Find CLP is disabled, then trying to perform a find on users will still return an error.

  4. Create CLPs also apply to signing up. So disabling Create CLPs on the user class also disables people from signing up without the master key.

  5. Users can only Update and Delete themselves. Public CLPs for Update and Delete may still apply. For example, if you disable public Update for the user class, then users cannot edit themselves. But no matter what the write ACL on a user is, that user can still Update or Delete itself, and no other user can Update or Delete that user. As always, however, using the master key allows users to update other users, independent of CLPs or ACLs.

  6. Get requests on installations follow ACLs normally. Find requests without master key is not allowed unless you supply the installationId as a constraint.

  7. Update requests on installations do adhere to the ACL defined on the installation, but Delete requests are master-key-only. For more information about how installations work, check out the installations section of the REST guide.

Data Integrity in Cloud Code

For most apps, care around keys, class-level permissions, and object-level ACLs are all you need to keep your app and your users’ data safe. Sometimes, though, you’ll run into an edge case where they aren’t quite enough. For everything else, there’s Cloud Code.

Cloud Code allows you to upload JavaScript to Parse’s servers, where we will run it for you. Unlike client code running on users’ devices that may have been tampered with, Cloud Code is guaranteed to be the code that you’ve written, so it can be trusted with more responsibility.

One particularly common use case for Cloud Code is preventing invalid data from being stored. For this sort of situation, it’s particularly important that a malicious client not be able to bypass the validation logic.

To create validation functions, Cloud Code allows you to implement a beforeSave trigger for your class. These triggers are run whenever an object is saved, and allow you to modify the object or completely reject a save. For example, this is how you create a Cloud Code beforeSave trigger to make sure every user has an email address set:

Parse.Cloud.beforeSave(Parse.User, function(request, response) {
  var user = request.object;
  if (!user.get("email")) {
    response.error("Every user must have an email address.");
  } else {
    response.success();
  }
});

Our Cloud Code guide provides instructions on how to upload this trigger to our servers.

Validations can lock down your app so that only certain values are acceptable. You can also use afterSave validations to normalize your data (e.g. formatting all phone numbers or currency identically). You get to retain most of the productivity benefits of accessing Parse data directly from your client applications, but you can also enforce certain invariants for your data on the fly.

Common scenarios that warrant validation include:

  • Making sure phone numbers have the right format
  • Sanitizing data so that its format is normalized
  • Making sure that an email address looks like a real email address
  • Requiring that every user specifies an age within a particular range
  • Not letting users directly change a calculated field
  • Not letting users delete specific objects unless certain conditions are met

Implementing Business Logic in Cloud Code

While validation often makes sense in Cloud Code, there are likely certain actions that are particularly sensitive, and should be as carefully guarded as possible. In these cases, you can remove permissions or the logic from clients entirely and instead funnel all such operations to Cloud Code functions.

When a Cloud Code function is called, it can invoke the useMasterKey function to gain the ability to modify user data. With the master key, your Cloud Code function can override any ACLs and write data. This means that it’ll bypass all the security mechanisms you’ve put in place in the previous sections.

Say you want to allow a user to “like” a Post object without giving them full write permissions on the object. You can do this by having the client call a Cloud Code function instead of modifying the Post itself:

The master key should be used carefully. When invoked, the master key is in effect for the duration of the Cloud Code function in which it is called:

Parse.Cloud.define("like", function(request, response) {
  Parse.Cloud.useMasterKey();
  // Everything after this point will bypass ACLs and other security
  // even if I do things besides just updating a Post object.
});

A more prudent way to use the master key would be to pass it as a parameter on a per-function basis. For example, instead of the above, set useMasterKey to true in each individual API function:

Parse.Cloud.define("like", function(request, response) {
  var post = new Parse.Object("Post");
  post.id = request.params.postId;
  post.increment("likes");
  post.save(null, { useMasterKey: true }).then(function() {
    // If I choose to do something else here, it won't be using
    // the master key and I'll be subject to ordinary security measures.
    response.success();
  }, function(error) {
    response.error(error);
  });
});

One very common use case for Cloud Code is sending push notifications to particular users. In general, clients can’t be trusted to send push notifications directly, because they could modify the alert text, or push to people they shouldn’t be able to. Your app’s settings will allow you to set whether “client push” is enabled or not; we recommend that you make sure it’s disabled. Instead, you should write Cloud Code functions that validate the data to be pushed and sent before sending a push.

Parse Security Summary

Parse provides a number of ways for you to secure data in your app. As you build your app and evaluate the kinds of data you will be storing, you can make the decision about which implementation to choose.

It is worth repeating that that the Parse User object is readable by all other users by default. You will want to set the ACL on your User object accordingly if you wish to prevent data contained in the User object (for example, the user’s email address) from being visible by other users.

Most classes in your app will fall into one of a couple of easy-to-secure categories. For fully public data, you can use class-level permissions to lock down the table to put publicly readable and writeable by no one. For fully private data, you can use ACLs to make sure that only the user who owns the data can read it. But occasionally, you’ll run into situations where you don’t want data that’s fully public or fully private. For example, you may have a social app, where you have data for a user that should be readable only to friends whom they’ve approved. For this you’ll need to a combination of the techniques discussed in this guide to enable exactly the sharing rules you desire.

We hope that you’ll use these tools to do everything you can to keep your app’s data and your users’ data secure. Together, we can make the web a safer place.

想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Performance

As your app scales, you will want to ensure that it performs well under increased load and usage. This document provides guidelines on how you can optimize your app’s performance. While you can use Parse Server for quick prototyping and not worry about performance, you will want to keep our performance guidelines in mind when you’re initially designing your app. We strongly advise that you make sure you’ve followed all suggestions before releasing your app.

You can improve your app’s performance by looking at the following:

  • Writing efficient queries.
  • Writing restrictive queries.
  • Using client-side caching.
  • Using Cloud Code.
  • Avoiding count queries.
  • Using efficient search techniques.

Keep in mind that not all suggestions may apply to your app. Let’s look into each one of these in more detail.

Write Efficient Queries

Parse objects are stored in a database. A Parse query retrieves objects that you are interested in based on conditions you apply to the query. To avoid looking through all the data present in a particular Parse class for every query, the database can use an index. An index is a sorted list of items matching a given criteria. Indexes help because they allow the database to do an efficient search and return matching results without looking at all of the data. Indexes are typically smaller in size and available in memory, resulting in faster lookups.

Indexing

You are responsible for managing your database and maintaining indexes when using Parse Server. If your data is not indexed, every query will have to go through the the entire data for a class to return a query result. On the other hand, if your data is indexed appropriately, the number of documents scanned to return a correct query result should be low.

The order of a query constraint’s usefulness is:

  • Equal to
  • Contained In
  • Less than, Less than or Equal to, Greater than, Greater than or Equal to
  • Prefix string matches
  • Not equal to
  • Not contained in
  • Everything else

Take a look at the following query to retrieve GameScore objects:

var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.equalTo("score", 50);
query.containedIn("playerName",
    ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"score" equalTo:@50];
[query whereKey:@"playerName"
    containedIn:@[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"]];

let query = PFQuery.queryWithClassName("GameScore")
query.whereKey("score", equalTo: 50)
query.whereKey("playerName", containedIn: ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"])
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("score", 50);
query.whereContainedIn("playerName", Arrays.asList("Jonathan Walsh", "Dario Wunsch", "Shawn Simon"));
var names = new[] { "Jonathan Walsh", "Dario Wunsch", "Shawn Simon" };
var query = new ParseObject.GetQuery("GameScore")
    .WhereEqualTo("score", 50)
    .WhereContainedIn("playerName", names);
# No REST API example
// No C++ example

Creating an index query based on the score field would yield a smaller search space in general than creating one on the playerName field.

When examining data types, booleans have a very low entropy and and do not make good indexes. Take the following query constraint:

query.equalTo("cheatMode", false);

[query whereKey:@"cheatMode" equalTo:@NO];

query.whereKey("cheatMode", equalTo: false)
query.whereEqualTo("cheatMode", false);
query.WhereEqualTo("cheatMode", false);
# No REST API example
// No C++ example

The two possible values for "cheatMode" are true and false. If an index was added on this field it would be of little use because it’s likely that 50% of the records will have to be looked at to return query results.

Data types are ranked by their expected entropy of the value space for the key:

  • GeoPoints
  • Array
  • Pointer
  • Date
  • String
  • Number
  • Other

Even the best indexing strategy can be defeated by suboptimal queries.

Efficient Query Design

Writing efficient queries means taking full advantage of indexes. Let’s take a look at some query constraints that negate the use of indexes:

  • Not Equal To
  • Not Contained In

Additionally, the following queries under certain scenarios may result in slow query responses if they can’t take advantage of indexes:

  • Regular Expressions
  • Ordered By

Not Equal To

For example, let’s say you’re tracking high scores for a game in a GameScore class. Now say you want to retrieve the scores for all players except a certain one. You could create this query:

var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.notEqualTo("playerName", "Michael Yabuti");
query.find().then(function(results) {
  // Retrieved scores successfully
});

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" notEqualTo:@"Michael Yabuti"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Retrieved scores successfully
  }
}];

let query = PFQuery.queryWithClassName("GameScore")
query.whereKey("playerName", notEqualTo: "Michael Yabuti")
query.findObjectsInBackgroundWithBlock {
  (objects, error) in
  if !error {
    // Retrieved scores successfully
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereNotEqualTo("playerName", "Michael Yabuti");
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if ( e == null) {
      // Retrieved scores successfully
    }
  }
});
var results = await ParseObject.GetQuery("GameScore")
    .WhereNotEqualTo("playerName", "Michael Yabuti")
    .FindAsync();
# No REST API example
// No C++ example

This query can’t take advantage of indexes. The database has to look at all the objects in the "GameScore" class to satisfy the constraint and retrieve the results. As the number of entries in the class grows, the query takes longer to run.

Luckily, most of the time a “Not Equal To” query condition can be rewritten as a “Contained In” condition. Instead of querying for the absence of values, you ask for values which match the rest of the column values. Doing this allows the database to use an index and your queries will be faster.

For example if the User class has a column called state which has values “SignedUp”, “Verified”, and “Invited”, the slow way to find all users who have used the app at least once would be to run the query:

var query = new Parse.Query(Parse.User);
query.notEqualTo("state", "Invited");

PFQuery *query = [PFUser query];
[query whereKey:@"state" notEqualTo:@"Invited"];

var query = PFUser.query()
query.whereKey("state", notEqualTo: "Invited")
ParseQuery<ParseUser> query = ParseQuery.getQuery(ParseUser.class);
query.whereNotEqualTo("state", "Invited");
var query = ParseUser.Query
    .WhereNotEqualTo("state", "Invited");
# No REST API example
// No C++ example

It would be faster to use the “Contained In” condition when setting up the query:

query.containedIn("state", ["SignedUp", "Verified"]);

[query whereKey:@"state"
    containedIn:@[@"SignedUp", @"Verified"]];

query.whereKey("state", containedIn: ["SignedUp", "Verified"])
query.whereContainedIn("state", Arrays.asList("SignedUp", "Verified"));
query.WhereContainedIn("state", new[] { "SignedUp", "Verified" });
# No REST API example
// No C++ example

Sometimes, you may have to completely rewrite your query. Going back to the "GameScore" example, let’s say we were running that query to display players who had scored higher than the given player. We could do this differently, by first getting the given player’s high score and then using the following query:

var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
// Previously retrieved highScore for Michael Yabuti
query.greaterThan("score", highScore);
query.find().then(function(results) {
  // Retrieved scores successfully
});

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
// Previously retrieved highScore for Michael Yabuti
[query whereKey:@"score" greaterThan:highScore];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Retrieved scores successfully
  }
}];

let query = PFQuery.queryWithClassName("GameScore")
// Previously retrieved highScore for Michael Yabuti
query.whereKey("score", greaterThan: highScore)
query.findObjectsInBackgroundWithBlock {
  (objects, error) in
  if !error {
    // Retrieved scores successfully
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
// Previously retrieved highScore for Michael Yabuti
query.whereGreaterThan("score", highScore);
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if (e == null) {
      // Retrieved scores successfully
    }
  }
});
// Previously retrieved highScore for Michael Yabuti
var results = await ParseObject.GetQuery("GameScore")
    .WhereGreaterThan("score", highScore)
    .FindAsync();
# No REST API example
// No C++ example

The new query you use depends on your use case. This may sometimes mean a redesign of your data model.

Not Contained In

Similar to “Not Equal To”, the “Not Contained In” query constraint can’t use an index. You should try and use the complementary “Contained In” constraint. Building on the User example, if the state column had one more value, “Blocked”, to represent blocked users, a slow query to find active users would be:

var query = new Parse.Query(Parse.User);
query.notContainedIn("state", ["Invited", "Blocked"];

PFQuery *query = [PFUser query];
[query whereKey:@"state" notContainedIn:@[@"Invited", @"Blocked"]];

var query = PFUser.query()
query.whereKey("state", notContainedIn: ["Invited", "Blocked"])
ParseQuery<ParseUser> query = ParseQuery.getQuery(ParseUser.class);
query.whereNotContainedIn("state", Arrays.asList("Invited", "Blocked"));
var query = ParseUser.Query
    .WhereNotContainedIn("state", new[] { "Invited", "Blocked" });
# No REST API example
// No C++ example

Using a complimentary “Contained In” query constraint will always be faster:

query.containedIn("state", ["SignedUp", "Verified"]);

[query whereKey:@"state" containedIn:@[@"SignedUp", @"Verified"]];

query.whereKey("state", containedIn: ["SignedUp", "Verified"])
query.whereContainedIn("state", Arrays.asList("SignedUp", "Verified"));
query.WhereContainedIn("state", new[] { "SignedUp", "Verified"});
# No REST API example
// No C++ example

This means rewriting your queries accordingly. Your query rewrites will depend on your schema set up. It may mean redoing that schema.

Regular Expressions

Regular expression queries should be avoided due to performance considerations. MongoDB is not efficient for doing partial string matching except for the special case where you only want a prefix match. Queries that have regular expression constraints are therefore very expensive, especially for classes with over 100,000 records. Consider restricting how many such operations can be run on a particular app at any given time.

You should avoid using regular expression constraints that don’t use indexes. For example, the following query looks for data with a given string in the "playerName" field. The string search is case insensitive and therefore cannot be indexed:

query.matches("playerName", "Michael", i);

[query whereKey:@"playerName" matchesRegex:@"Michael" modifiers:@"i"];

query.whereKey("playerName", matchesRegex: "Michael", modifiers: "i")
query.whereMatches("playerName", "Michael", "i");
query.WhereMatches("playerName", "Michael", "i")
# No REST API example
// No C++ example

The following query, while case sensitive, looks for any occurrence of the string in the field and cannot be indexed:

query.contains("playerName", "Michael");

[query whereKey:@"playerName" containsString:@"Michael"];

query.whereKey("playerName", containsString: "Michael")
query.whereContains("playerName", "Michael");
query.WhereContains("playerName", "Michael")
# No REST API example
// No C++ example

These queries are both slow. In fact, the matches and contains query constraints are not covered in our querying guides on purpose and we do not recommend using them. Depending on your use case, you should switch to using the following constraint that uses an index, such as:

query.startsWith("playerName", "Michael");

[query whereKey:@"playerName" hasPrefix:@"Michael"];

query.whereKey("playerName", hasPrefix: "Michael")
query.whereStartsWith("playerName", "Michael");
query.WhereStartsWith("playerName", "Michael")
# No REST API example
// No C++ example

This looks for data that starts with the given string. This query will use the backend index, so it will be faster even for large datasets.

As a best practice, when you use regular expression constraints, you’ll want to ensure that other constraints in the query reduce the result set to the order of hundreds of objects to make the query efficient. If you must use the matches or contains constraints for legacy reasons, then use case sensitive, anchored queries where possible, for example:

query.matches("playerName", "^Michael");

[query whereKey:@"playerName" matchesRegex:@"^Michael"];

query.whereKey("playerName", matchesRegex: "^Michael")
query.whereMatches("playerName", "^Michael");
query.WhereMatches("playerName", "^Michael")
# No REST API example
// No C++ example

Most of the use cases around using regular expressions involve implementing search. A more performant way of implementing search is detailed later.

Write Restrictive Queries

Writing restrictive queries allows you to return only the data that the client needs. This is critical in a mobile environment were data usage can be limited and network connectivity unreliable. You also want your mobile app to appear responsive and this is directly affected by the objects you send back to the client. The Querying section shows the types of constraints you can add to your existing queries to limit the data returned. When adding constraints, you want to pay attention and design efficient queries.

You can limit the number of query results returned. The limit is 100 by default but anything from 1 to 1000 is a valid limit:

query.limit(10); // limit to at most 10 results

query.limit = 10; // limit to at most 10 results

query.limit = 10 // limit to at most 10 results
query.setLimit(10); // limit to at most 10 results
query.Limit(10); // limit to at most 10 results
# No REST API example
// No C++ example

If you’re issuing queries on GeoPoints, make sure you specify a reasonable radius:

var query = new Parse.Query(PlaceObject);
query.withinMiles("location", userGeoPoint, 10.0);
query.find().then(function(placesObjects) {
  // Get a list of objects within 10 miles of a user's location
});

PFQuery *query = [PFQuery queryWithClassName:@"Place"];
[query whereKey:@"location" nearGeoPoint:userGeoPoint withinMiles:10.0];
[query findObjectsInBackgroundWithBlock:^(NSArray *places, NSError *error) {
  if (!error) {
    // List of objects within 10 miles of a user's location
  }
}];

let query = PFQuery.queryWithClassName("Place")
query.whereKey("location", nearGeoPoint: userGeoPoint, withinMiles: 10.0)
query.findObjectsInBackgroundWithBlock {
  (places, error) in
  if !error {
    // List of places within 10 miles of a user's location
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("Place");
query.whereWithinMiles("location", userGeoPoint, 10.0);
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if (e == null) {
      // List of places within 10 miles of a user's location
    }
  }
});
var results = await ParseObject.GetQuery("GameScore")
    .WhereWithinDistance("location", userGeoPoint, ParseGeoDistance.FromMiles(10.0))
    .FindAsync();
# No REST API example
// No C++ example

You can further limit the fields returned by calling select:

var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.select("score", "playerName");
query.find().then(function(results) {
  // each of results will only have the selected fields available.
});

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query selectKeys:@[@"score", @"playerName"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // each of results will only have the selected fields available.
  }
}];

let query = PFQuery.queryWithClassName("GameScore")
query.selectKeys(["score", "playerName"])
query.findObjectsInBackgroundWithBlock {
  (objects, error) in
  if !error {
    // each of results will only have the selected fields available.
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.selectKeys(Arrays.asList("score", "playerName"));
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if (e == null) {
      // each of results will only have the selected fields available.
    }
  }
});
var results = await ParseObject.GetQuery("GameScore")
     .Select(new[] { "score", "playerName" })
     .FindAsync();
// each of results will only have the selected fields available.
# No REST API example
// No C++ example

Client-side Caching

For queries run from iOS and Android, you can turn on query caching. See the iOS and Android guides for more details. Caching queries will increase your mobile app’s performance especially in cases where you want to display cached data while fetching the latest data from Parse.

Use Cloud Code

Cloud Code allows you to run custom JavaScript logic on Parse Server instead of on the client.

You can use this to offload processing to the Parse servers thus increasing your app’s perceived performance. You can create hooks that run whenever an object is saved or deleted. This is useful if you want to validate or sanitize your data. You can also use Cloud Code to modify related objects or kick off other processes such as sending off a push notification.

We saw examples of limiting the data returned by writing restrictive queries. You can also use Cloud Functions to help limit the amount of data returned to your app. In the following example, we use a Cloud Function to get a movie’s average rating:

Parse.Cloud.define("averageStars", function(request, response) {
  var Review = Parse.Object.extend("Review");
  var query = new Parse.Query(Review);
  query.equalTo("movie", request.params.movie);
  query.find().then(function(results) {
    var sum = 0;
    for (var i = 0; i < results.length; ++i) {
      sum += results[i].get("stars");
    }
    response.success(sum / results.length);
  }, function(error) {
    response.error("movie lookup failed");
  });
});

You could have ran a query on the Review class on the client, returned only the stars field data and computed the result on the client. As the number of reviews for a movie increases you can see that the data being returned to the device using this methodology also increases. Implementing the functionality through a Cloud Function returns the one result if successful.

As you look at optimizing your queries, you’ll find that you may have to change the queries - sometimes even after you’ve shipped your app to the App Store or Google Play. The ability to change your queries without a client update is possible if you use Cloud Functions. Even if you have to redesign your schema, you could make all the changes in your Cloud Functions while keeping the client interface the same to avoid an app update. Take the average stars Cloud Function example from before, calling it from a client SDK would look like this:

Parse.Cloud.run("averageStars", { "movie": "The Matrix" }).then(function(ratings) {
  // ratings is 4.5
});

[PFCloud callFunctionInBackground:@"averageStars"
                  withParameters:@{@"movie": @"The Matrix"}
                           block:^(NSNumber *ratings, NSError *error) {
  if (!error) {
    // ratings is 4.5
  }
}];

PFCloud.callFunctionInBackground("averageStars", withParameters: ["movie": "The Matrix"]) {
  (ratings, error) in
  if !error {
    // ratings is 4.5
  }
}
HashMap<String, String> params = new HashMap();
params.put("movie", "The Matrix");
ParseCloud.callFunctionInBackground("averageStars", params, new FunctionCallback<Float>() {
  @Override
  public void done(Float aFloat, ParseException e) {
    if (e == null) {
      // ratings is 4.5
    }
  }
});
IDictionary<string, object> dictionary = new Dictionary<string, object>
{
    { "movie", "The Matrix" }
};

ParseCloud.CallFunctionAsync<float>("averageStars", dictionary).ContinueWith(t => {
  var result = t.Result;
  // result is 4.5
});
# No REST API example
// No C++ example

If later on, you need to modify the underlying data model, your client call can remain the same, as long as you return back a number that represents the ratings result.

Avoid Count Operations

For classes with over 1,000 objects, count operations are limited by timeouts. Thus, it is preferable to architect your application to avoid this count operation.

Suppose you are displaying movie information in your app and your data model consists of a Movie class and a Review class that contains a pointer to the corresponding movie. You might want to display the review count for each movie on the top-level navigation screen using a query like this:

var Review = Parse.Object.extend("Review");
var query = new Parse.Query("Review");
// movieId corresponds to a given movie's id
query.equalTo(movie, movieId);
query.count().then(function(count) {
  // Request succeeded
});

PFQuery *query = [PFQuery queryWithClassName:@"Review"];
// movieId corresponds to a given movie's id
[query whereKey:@"movie" equalTo:movieId];
[query countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
  if (!error) {
    // Request succeeded
  }
}];

let query = PFQuery.queryWithClassName("Review")
// movieId corresponds to a given movie's id
query.whereKey("movie", equalTo: movieId)
query.countObjectsInBackgroundWithBlock {
  (number, error) in
  if !error {
    // Request succeeded
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("Review");
// movieId corresponds to a given movie's id
query.whereEqualTo("movie", movieId);
query.countInBackground(new CountCallback() {
  @Override
  public void done(int i, ParseException e) {
    if ( e == null) {
      // Request succeeded
    }
  }
});
var count = await ParseObject.GetQuery("Review")
// movieId corresponds to a given movie's id
    .WhereEqualTo("movie", movieId)
    .CountAsync();
# No REST API example
// No C++ example

If you run the count query for each of the UI elements, they will not run efficiently on large data sets. One approach to avoid using the count() operator could be to add a field to the Movie class that represents the review count for that movie. When saving an entry to the Review class you could increment the corresponding movie’s review count field. This can be done in an afterSave handler:

Parse.Cloud.afterSave("Review", function(request) {
  // Get the movie id for the Review
  var movieId = request.object.get("movie").id;
  // Query the Movie represented by this review
  var Movie = Parse.Object.extend("Movie");
  var query = new Parse.Query(Movie);
  query.get(movieId).then(function(movie) {
    // Increment the reviews field on the Movie object
    movie.increment("reviews");
    movie.save();
  }, function(error) {
    throw "Got an error " + error.code + " : " + error.message;
  });
});

Your new optimized query would not need to look at the Review class to get the review count:

var Movie = Parse.Object.extend("Movie");
var query = new Parse.Query(Movie);
query.find().then(function(results) {
  // Results include the reviews count field
}, function(error) {
  // Request failed
});

PFQuery *query = [PFQuery queryWithClassName:@"Movie"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Results include the reviews count field
  }
}];

let query = PFQuery.queryWithClassName("Movie")
query.findObjectsInBackgroundWithBlock {
  (objects, error) in
  if !error {
    // Results include the reviews count field
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("Movie");
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if (e == null) {
      // Results include the reviews count field
    }
  }
});
var results = await ParseObject.GetQuery("Movie")
    .FindAsync();
// Results include the reviews count field
# No REST API example
// No C++ example

You could also use a separate Parse Object to keep track of counts for each review. Whenever a review gets added or deleted, you can increment or decrement the counts in an afterSave or afterDelete Cloud Code handler. The approach you choose depends on your use case.

Implement Efficient Searches

As mentioned previously, MongoDB is not efficient for doing partial string matching. However, this is an important use case when implementing search functionality that scales well in production.

Simplistic search algorithms simply scan through all the class data and executes the query on each entry. The key to making searches run efficiently is to minimize the number of data that has to be examined when executing each query by using an index as we’ve outlined earlier. You’ll need to build your data model in a way that it’s easy for us to build an index for the data you want to be searchable. For example, string matching queries that don’t match an exact prefix of the string won’t be able to use an index leading to timeout errors as the data set grows.

Let’s walk through an example of how you could build an efficient search. You can apply the concepts you learn in this example to your use case. Say your app has users making posts, and you want to be able to search those posts for hashtags or particular keywords. You’ll want to pre-process your posts and save the list of hashtags and words into array fields. You can do this processing either in your app before saving the posts, or you can use a Cloud Code beforeSave hook to do this on the fly:

var _ = require("underscore");
Parse.Cloud.beforeSave("Post", function(request, response) {
  var post = request.object;
  var toLowerCase = function(w) { return w.toLowerCase(); };
  var words = post.get("text").split(/\b/);
  words = _.map(words, toLowerCase);
  var stopWords = ["the", "in", "and"]
  words = _.filter(words, function(w) {
    return w.match(/^\w+$/) && !   _.contains(stopWords, w);
  });
  var hashtags = post.get("text").match(/#.+?\b/g);
  hashtags = _.map(hashtags, toLowerCase);
  post.set("words", words);
  post.set("hashtags", hashtags);
  response.success();
});

This saves your words and hashtags in array fields, which MongoDB will store with a multi-key index. There are some important things to notice about this. First of all it’s converting all words to lower case so that we can look them up with lower case queries, and get case insensitive matching. Secondly, it’s filtering out common words like ‘the’, ‘in’, and ‘and’ which will occur in a lot of posts, to additionally reduce useless scanning of the index when executing the queries.

Once you’ve got the keywords set up, you can efficiently look them up using “All” constraint on your query:

var Post = Parse.Object.extend("Post");
var query = new Parse.Query(Post);
query.containsAll("hashtags", [“#parse, “#ftw]);
query.find().then(function(results) {
  // Request succeeded
}, function(error) {
  // Request failed
});

PFQuery *query = [PFQuery queryWithClassName:@"Post"];
[query whereKey:@"hashtags" containsAllObjectsInArray:@[@"#parse", @"#ftw"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Request succeeded
  }
}];

let query = PFQuery.queryWithClassName("Post")
query.whereKey("hashtags", containsAllObjectsInArray: ["#parse", "#ftw"])
query.findObjectsInBackgroundWithBlock {
  (objects, error) in
  if !error {
    // Request succeeded
  }
}
ParseQuery<ParseObject> query = ParseQuery.getQuery("Post");
query.whereContainsAll("hashtags", Arrays.asList("#parse", "#ftw"));
query.findInBackground(new FindCallback<ParseObject>() {
  @Override
  public void done(List<ParseObject> list, ParseException e) {
    if (e == null) {
      // Request succeeded
    }
  }
});
var results = await ParseObject.GetQuery("Post")
    .WhereContainsAll("hashtags", new[] { "#parse", "#ftw" })
    .FindAsync();
# No REST API example
// No C++ example

Limits and Other Considerations

There are some limits in place to ensure the API can provide the data you need in a performant manner. We may adjust these in the future. Please take a moment to read through the following list:

Objects

  • Parse Objects are limited in size to 128 KB.
  • We recommend against creating more than 64 fields on a single Parse Object to ensure that we can build effective indexes for your queries.
  • We recommend against using field names that are longer than 1,024 characters, otherwise an index for the field will not be created.

Queries

  • Queries return 100 objects by default. Use the limit parameter to change this, up to a value of 1,000.
  • Queries can only return up to 1,000 objects in a single result set. This includes any resolved pointers. You can use skip and limit to page through results.
  • The maximum value accepted by skip is 10,000. If you need to get more objects, we recommend sorting the results and then using a constraint on the sort column to filter out the first 10,000 results. You will then be able to continue paging through results starting from a skip value of 0. For example, you can sort your results by createdAt ASC and then filter out any objects older than the createdAt value of the 10,000th object when starting again from 0.
  • Alternatively, you may use the each() method in the JavaScript SDK to page through all objects that match the query.
  • Skips and limits can only be used on the outer query.
  • You may increase the limit of a inner query to 1,000, but skip cannot be used to get more results past the first 1,000.
  • Constraints that collide with each other will result in only one of the constraint being applied. An example of this would be two equalTo constraints over the same key with two different values, which contradicts itself (perhaps you’re looking for ‘contains’).
  • No geo-queries inside compound OR queries.
  • Using $exists: false is not advised.
  • The each query method in the JavaScript SDK cannot be used in conjunction with queries using geo-point constraints.
  • A maximum of 500,000 objects will be scanned per query. If your constraints do not successfully limit the scope of the search, this can result in queries with incomplete results.
  • A containsAll query constraint can only take up to 9 items in the comparison array.

Push Notifications

Cloud Code

  • The params payload that is passed to a Cloud Function is limited to 50 MB.
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。

Error Codes

The following is a list of all the error codes that can be returned by the Parse API. You may also refer to RFC2616 for a list of http error codes. Make sure to check the error message for more details.

API Issues

Name Code Description
UserInvalidLoginParams 101 Invalid login parameters. Check error message for more details.
ObjectNotFound 101 The specified object or session doesn’t exist or could not be found. Can also indicate that you do not have the necessary permissions to read or write this object. Check error message for more details.
InvalidQuery 102 There is a problem with the parameters used to construct this query. This could be an invalid field name or an invalid field type for a specific constraint. Check error message for more details.
InvalidClassName 103 Missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters.
MissingObjectId 104 An unspecified object id.
InvalidFieldName 105 An invalid field name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters. Some field names may be reserved. Check error message for more details.
InvalidPointer 106 A malformed pointer was used. You would typically only see this if you have modified a client SDK.
InvalidJSON 107 Badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly. Can also indicate an invalid utf-8 string or use of multiple form encoded values. Check error message for more details.
CommandUnavailable 108 The feature you tried to access is only available internally for testing purposes.
NotInitialized 109 You must call Parse.initialize before using the Parse library. Check the Quick Start guide for your platform.
ObjectTooLarge 116 The object is too large. Parse Objectss have a max size of 128 kilobytes.
ExceededConfigParamsError 116 You have reached the limit of 100 config parameters.
InvalidLimitError 117 An invalid value was set for the limit. Check error message for more details.
InvalidSkipError 118 An invalid value was set for skip. Check error message for more details.
OperationForbidden 119 The operation isn’t allowed for clients due to class-level permissions. Check error message for more details.
CacheMiss 120 The result was not found in the cache.
InvalidNestedKey 121 An invalid key was used in a nested JSONObject. Check error message for more details.
InvalidACL 123 An invalid ACL was provided.
InvalidEmailAddress 125 The email address was invalid.
DuplicateValue 137 Unique field was given a value that is already taken.
InvalidRoleName 139 Role’s name is invalid.
ReservedValue 139 Field value is reserved.
ExceededCollectionQuota 140 You have reached the quota on the number of classes in your app. Please delete some classes if you need to add a new class.
ScriptFailed 141 Cloud Code script failed. Usually points to a JavaScript error. Check error message for more details.
FunctionNotFound 141 Cloud function not found. Check that the specified Cloud function is present in your Cloud Code script and has been deployed.
JobNotFound 141 Background job not found. Check that the specified job is present in your Cloud Code script and has been deployed.
SuccessErrorNotCalled 141 success/error was not called. A cloud function will return once response.success() or response.error() is called. A background job will similarly finish execution once status.success() or status.error() is called. If a function or job never reaches either of the success/error methods, this error will be returned. This may happen when a function does not handle an error response correctly, preventing code execution from reaching the success() method call.
MultupleSuccessErrorCalls 141 Can’t call success/error multiple times. A cloud function will return once response.success() or response.error() is called. A background job will similarly finish execution once status.success() or status.error() is called. If a function or job calls success() and/or error() more than once in a single execution path, this error will be returned.
ValidationFailed 142 Cloud Code validation failed.
WebhookError 143 Webhook error.
InvalidImageData 150 Invalid image data.
UnsavedFileError 151 An unsaved file.
InvalidPushTimeError 152 An invalid push time was specified.
HostingError 158 Hosting error.
InvalidEventName 160 The provided analytics event name is invalid.
ClassNotEmpty 255 Class is not empty and cannot be dropped.
AppNameInvalid 256 App name is invalid.
MissingAPIKeyError 902 The request is missing an API key.
InvalidAPIKeyError 903 The request is using an invalid API key.
Name Code Description
IncorrectType 111 A field was set to an inconsistent type. Check error message for more details.
InvalidChannelName 112 Invalid channel name. A channel name is either an empty string (the broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter.
InvalidSubscriptionType 113 Bad subscription type. Check error message for more details.
InvalidDeviceToken 114 The provided device token is invalid.
PushMisconfigured 115 Push is misconfigured in your app. Check error message for more details.
PushWhereAndChannels 115 Can’t set channels for a query-targeted push. You can fix this by moving the channels into your push query constraints.
PushWhereAndType 115 Can’t set device type for a query-targeted push. You can fix this by incorporating the device type constraints into your push query.
PushMissingData 115 Push is missing a ‘data’ field.
PushMissingChannels 115 Non-query push is missing a ‘channels’ field. Fix by passing a ‘channels’ or ‘query’ field.
ClientPushDisabled 115 Client-initiated push is not enabled. Check your Parse app’s push notification settings.
RestPushDisabled 115 REST-initiated push is not enabled. Check your Parse app’s push notification settings.
ClientPushWithURI 115 Client-initiated push cannot use the “uri” option.
PushQueryOrPayloadTooLarge 115 Your push query or data payload is too large. Check error message for more details.
InvalidExpirationError 138 Invalid expiration value.
MissingPushIdError 156 A push id is missing. Deprecated.
MissingDeviceTypeError 157 The device type field is missing. Deprecated.
Name Code Description
InvalidFileName 122 An invalid filename was used for Parse File. A valid file name contains only a-zA-Z0-9_. characters and is between 1 and 128 characters.
MissingContentType 126 Missing content type.
MissingContentLength 127 Missing content length.
InvalidContentLength 128 Invalid content length.
FileTooLarge 129 File size exceeds maximum allowed.
FileSaveError 130 Error saving a file.
FileDeleteError 131 File could not be deleted.
Name Code Description
InvalidInstallationIdError 132 Invalid installation id.
InvalidDeviceTypeError 133 Invalid device type.
InvalidChannelsArrayError 134 Invalid channels array value.
MissingRequiredFieldError 135 Required field is missing.
ChangedImmutableFieldError 136 An immutable field was changed.
Name Code Description
ReceiptMissing 143 Product purchase receipt is missing.
InvalidPurchaseReceipt 144 Product purchase receipt is invalid.
PaymentDisabled 145 Payment is disabled on this device.
InvalidProductIdentifier 146 The product identifier is invalid.
ProductNotFoundInAppStore 147 The product is not found in the App Store.
InvalidServerResponse 148 The Apple server response is not valid.
ProductDownloadFilesystemError 149 The product fails to download due to file system error.
Name Code Description
UsernameMissing 200 The username is missing or empty.
PasswordMissing 201 The password is missing or empty.
UsernameTaken 202 The username has already been taken.
UserEmailTaken 203 Email has already been used.
UserEmailMissing 204 The email is missing, and must be specified.
UserWithEmailNotFound 205 A user with the specified email was not found.
SessionMissing 206 A user object without a valid session could not be altered.
MustCreateUserThroughSignup 207 A user can only be created through signup.
AccountAlreadyLinked 208 An account being linked is already linked to another user.
InvalidSessionToken 209 The device’s session token is no longer valid. The application should ask the user to log in again.

Linked services errors

Name Code Description
LinkedIdMissing 250 A user cannot be linked to an account because that account’s id could not be found.
InvalidLinkedSession 251 A user with a linked (e.g. Facebook or Twitter) account has an invalid session. Check error message for more details.
InvalidGeneralAuthData 251 Invalid auth data value used.
BadAnonymousID 251 Anonymous id is not a valid lowercase UUID.
FacebookBadToken 251 The supplied Facebook session token is expired or invalid.
FacebookBadID 251 A user with a linked Facebook account has an invalid session.
FacebookWrongAppID 251 Unacceptable Facebook application id.
TwitterVerificationFailed 251 Twitter credential verification failed.
TwitterWrongID 251 Submitted Twitter id does not match the id associated with the submitted access token.
TwitterWrongScreenName 251 Submitted Twitter handle does not match the handle associated with the submitted access token.
TwitterConnectFailure 251 Twitter credentials could not be verified due to problems accessing the Twitter API.
UnsupportedService 252 A service being linked (e.g. Facebook or Twitter) is unsupported. Check error message for more details.
UsernameSigninDisabled 252 Authentication by username and password is not supported for this application. Check your Parse app’s authentication settings.
AnonymousSigninDisabled 252 Anonymous users are not supported for this application. Check your Parse app’s authentication settings.
FacebookSigninDisabled 252 Authentication by Facebook is not supported for this application. Check your Parse app’s authentication settings.
TwitterSigninDisabled 252 Authentication by Twitter is not supported for this application. Check your Parse app’s authentication settings.
InvalidAuthDataError 253 An invalid authData value was passed. Check error message for more details.
LinkingNotSupportedError 999 Linking to an external account not supported yet with signup_or_login. Use update instead.

Client-only errors

Name Code Description
ConnectionFailed 100 The connection to the Parse servers failed.
AggregateError 600 There were multiple errors. Aggregate errors have an “errors” property, which is an array of error objects with more detail about each error that occurred.
FileReadError 601 Unable to read input for a Parse File on the client.
XDomainRequest 602 A real error code is unavailable because we had to use an XDomainRequest object to allow CORS requests in Internet Explorer, which strips the body from HTTP responses that have a non-2XX status code.

Operational issues

Name Code Description
RequestTimeout 124 The request was slow and timed out. Typically this indicates that the request is too expensive to run. You may see this when a Cloud function did not finish before timing out, or when a Parse.Cloud.httpRequest connection times out.
InefficientQueryError 154 An inefficient query was rejected by the server. Refer to the Performance Guide and slow query log.
RequestLimitExceeded 155 This application has exceeded its request limit (legacy Parse.com apps only).
TemporaryRejectionError 159 An application’s requests are temporary rejected by the server (legacy Parse.com apps only).
DatabaseNotMigratedError 428 You should migrate your database as soon as possible (legacy Parse.com apps only).

Other issues

Name Code Description
OtherCause -1 An unknown error or an error unrelated to Parse occurred.
InternalServerError 1 Internal server error. No information available.
ServiceUnavailable 2 The service is currently unavailable.
ClientDisconnected 4 Connection failure.
想要帮助一起完善这份文档嘛? 点击编辑此部分即可。