Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compound query for Parse.com using REST API

A project I have inherited currently uses Parse for backend. I am trying to migrate the way the data is fetched/posted, from the Parse iOS SDK to using the Parse REST API (using AFNetworking).

I was able to convert a simple PFQuery like the following to it's REST equivalent:

PFQuery

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.'999Z'"];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];

NSString *createdAt = [dateFormatter stringFromDate:createdAtDate];

PFQuery *query = [PFQuery queryWithClassName:@"Photo"];
[query whereKey:@"createdAt" lessThan:createdAt];
[query orderByDescending:@"createdAt"];
[query setLimit:10];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (!error) {
        //Etc...
    }
}];

REST API Request:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.'999Z'"];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];

NSString *createdAt = [dateFormatter stringFromDate:createdAtDate];

NSString *dateQueryString = [NSString stringWithFormat:@"{\"createdAt\":{\"$lte\":{\"__type\":\"Date\",\"iso\":\"%@\"}}}", createdAt];

    NSDictionary* parameters = @{@"order": @"-createdAt",
                                 @"limit": @10,
                                 @"include": @"user",
                                 @"where":dateQueryString};    

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer parseSerializer]; // Already has App-Id, App-key and Content-Type set.

NSMutableURLRequest* request = [manager.requestSerializer requestWithMethod:@"GET" URLString:@"https://api.parse.com/1/classes/Photo" parameters:parameters error:nil];

AFHTTPRequestOperation* operation = [manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {

    // Etc..

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

    NSLog(@"%@", error.localizedDescription);
}];

[operation start];

The problem however is with compound queries like the one below:

NSMutableArray* subqueriesArray = [NSMutableArray array];

PFQuery *followingActivitiesQuery = [PFQuery queryWithClassName:@"Activity"];
[followingActivitiesQuery whereKey:@“type” equalTo:@“Follow”];
[followingActivitiesQuery whereKey:@“fromUser” equalTo:[PFUser currentUser]];
followingActivitiesQuery.limit = 1000;

PFQuery *privateUserQuery = [PFUser query];
[privateUserQuery whereKey:@“yourMomLikedThisPhoto” equalTo:[NSNumber numberWithBool:NO]]; // fake whereKey of course

PFQuery *photosFromFollowedUsersQuery = [PFQuery queryWithClassName:@"Photo"];
[photosFromFollowedUsersQuery whereKey:@“yourMomLikedThisPhoto” equalTo:[NSNumber numberWithBool:YES]];
[photosFromFollowedUsersQuery whereKey:@“user” matchesKey:@“toUser” inQuery:followingActivitiesQuery];
[photosFromFollowedUsersQuery whereKey:@“user” matchesQuery:privateUserQuery];

PFQuery *photosFromCurrentUserQuery = [PFQuery queryWithClassName:@"Photo"];
[photosFromCurrentUserQuery whereKey:@“yourMomLikedThisPhoto” equalTo:[NSNumber numberWithBool:YES]];
[photosFromCurrentUserQuery whereKey:@“user” equalTo:[PFUser currentUser]];

[subqueriesArray addObject:photosFromFollowedUsersQuery];
[subqueriesArray addObject:photosFromCurrentUserQuery];

PFQuery *query = [PFQuery orQueryWithSubqueries:subqueriesArray];
[query includeKey:@"Photo"];
[query orderByDescending:@"createdAt"];

If someone could help me understand how to construct the parameters for this, I'd really appreciate it. A general idea on how to proceed in the right direction will also help. I've referred the Parse REST API Guide, but I can't seem to get it. I'm trying to get it right using $inQuery, but no luck yet.

An answer to this question will cover most of the common and complex issues one faces when querying with the Parse REST API (on iOS).

like image 869
n00bProgrammer Avatar asked May 13 '15 12:05

n00bProgrammer


1 Answers

Transformative use of $or

I think that what you will want to do is transform the following call into a relevant http call using the library of your choice (AFNetworking). As you can see the subquery behavior is embedded in a specific encoded url that contains an array with dictionary whose values are dictionaries with comparison statements.

curl -X GET \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
  -G \
  --data-urlencode 'where={"$or":[{"wins":{"$gt":150}},{"wins":{"$lt":5}}]}' \
  https://api.parse.com/1/classes/Player

(source)

Another approach would be use of subclass queries.

Subclass Queries

You can get a query for objects of a particular subclass using the class method query. The following example queries for armors that the user can afford:

PFQuery *query = [Armor query];
[query whereKey:@"rupees" lessThanOrEqualTo:[PFUser currentUser][@"rupees"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    Armor *firstArmor = [objects firstObject];
    // ...
  }
}];

(source)

Relational Queries

There are several ways to issue queries for relational data. If you want to retrieve objects where a field matches a particular PFObject, you can use whereKey:equalTo: 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 PFObject *myPost was previously created.
// Using PFQuery
PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" equalTo:myPost];

[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
    // comments now contains the comments for myPost
}];

// Using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"post = %@", myPost];
PFQuery *query = [PFQuery queryWithClassName:@"Comment" predicate:predicate];

[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
    // comments now contains the comments for myPost
}];

One Parse Blog Post also mentions that predicates can be further enhanced using:

At this time, we support these NSPredicate features:

Simple comparisons such as =, !=, <, >, <=, >=, and BETWEEN with a key and a constant. Containment predicates, such as x IN {1, 2, 3}. Key-existence predicates, such as x IN SELF. BEGINSWITH expressions. Compound predicates with AND, OR, and NOT. Sub-queries with "key IN %@", subquery. See our docs for more details and examples.

Specific Tips on Decomposing Parameters

I'd suggest evaluating any samples that show how REST API values are interpreted. Keeping in mind that most transmissions will likely represent a JSON-like format. In these cases '[' square braces are arrays, '{' curly braces represent dictionaries and you will want to encode the parametrized data before pushing it into your http request.

Applied Information

Taking what is discussed above, you can create a REST API call. You must encode the where clause of the various criterion specific to your use-case. The following method can be used to ensure those parameters are in line with the query properties you need:

var encodedParam = param.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())

Assuming that param above is some string with your key-values similar to:

'where={"$or":[{"wins":{"$gt":150}},{"wins":{"$lt":5}}]}'

The encoded parameters would be added after your Application ID and REST API key onto the basic REST-API query string - this should return the objects you want: api-query (note the fourth url used for queries).

It looks like the encoded parameters should go after the classname referenced above as in the following example:

encoded-params (extrapolate your values using the format above, also note the extra characters on the query string "...classname ?%s encodedParams").

Additional query operators you may want to use include:

query-operators

Additional Query Examples

another-example

One could extrapolate how to construct more complex queries by using the supplied examples on the Parse website. Packing these queries into encoded parameters should yield the results you seek. What I would do is first construct a query using the regular Parse API and then convert that to the REST-API construct identified above.

Eg:

let urlpath = NSString(format: "https://api.parse.com/1/classes/Photo?%s\(encodedParams)")

... add code to call the urlPath

Specific Answer to Your Main Issue

After re-reading the question I wanted to reply to your specific problem with needing to add multiple subqueries onto the REST-API parameter list. I think that close inspection of the $or clause would be useful. As long as you can apply related keys on the Photo object into the multiple criterion then you should be able to achieve your objective.

'where={"$or":[{"wins":{"$gt":150}},{"wins":{"$lt":5}}]}'

Compound Query Example

The $or clause is the example used on the Parse Website under compound queries. The code above can be transposed to your use-case and applied using the encoded url method specified previously.

compound-query

Nota Bene

Notice that the example below shows retrieval of multiple related objects (using $inQuery). There are limits regarding how many inner objects may be retrieved. I noticed that this is basically the equivalence of what you are doing in code so hopefully the extrapolation would be relatively straight-forward using this sample.

multiple-objects

Caution

Please note this word of caution from Parse on when to use the REST-API.

rest-api-caution

like image 83
Tommie C. Avatar answered Sep 29 '22 15:09

Tommie C.