iOS Parse SDK: How to count elements from one-to-many relationship - ios

Description = I have a project that allows users to post things. These posts will be displayed in a tableView, (the comments will not be shown in this tableView). When a user presses a post, they will segue to a screen where they can post a comment. In the tableViewCell there will be a label like "(0) comments" to display how many comments belong to the post.
2 Queries happen. One query to populate the tableView, and another to query the amount of comments from each post at each indexPath.
Desired Result = If there are 5 comments on a post, to be able to return how many comments where made on each post so I can .count them into a string to say, "(5) Comments".
Problem = I'm not sure how to query for the results of a query, or if that is even what is supposed to happen. I'm not even sure if this is going right, please help someone who is knowledgable. I attached what I have done (not a copy and paste i typed all of that [so if something isn't right or there was a typo, please do not respond telling me that that was the problem with the code]).
What I have done =
-viewDidLoad...
PFQuery *postQuery = [PFQuery queryWithClassName:#"Post"];
[userQuery whereKey:#"postedByID" equalTo:selectedUserID];
[userQuery orderByDescending:#"createdAt"];
[userQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
tableArray = objects;
[self.tableView reloadData];
}];
-cellForRowAtIndexPath...
PFObject *post = [tableArray objectAtIndex:indexPath.row];
PFFile *imgFile = post[#"postPicture"];
[imgFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
cell.picture.image = [UIImage imageWithData:data];
}];
NSString *dateAndTime = [NSString stringWithFormat:#"%# %#", post[#"postDate"],post[#"postTime"]];
cell.categoryLabel.text = post[#"category"];
cell.subCategoryLabel.text = post[#"subCategory"];
cell.dateLabel.text = dateAndTime;
cell.descriptionLabel.text = post[#"description"]];
//below is where my headaches are coming from
PFQuery *commentsQuery = [PFQuery queryWithClassName:#"Comment"];
//WHAT DO I DO HERE? (this probably isn't right, but I feel like it should be)
[commentsQuery whereKey:#"parentID" containedIn:[tableArray objectAtIndexPath:indexPath.row][#"objectId"];
[commentsQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
NSLog(#"%#", objects);
}];
This returns an empty log.
I know for a fact that there are multiple comments in relation to some of the posts, yet still returns nothing. I believe this is because my commentsQuery isn't right, or something. I dunno, please help someone.

If I understand correctly your case, you should try with:
[commentsQuery whereKey:#"parentID" equalTo:[tableArray objectAtIndexPath:indexPath.row][#"objectId"];
since you are querying all of the comments belonging to a specific post.
containedIn would allow you to get all of the comments belonging to a given set of posts.

You should consider changing your design, as counting the comments for every post is extremely inefficient.
Take a look at the documentation on Cloud Code that runs after save. It specifically talks about a comments counter on a Post class, then you get the count as a simple property when you query the Post.
It is OK to make writes (that are less frequent) a little more expensive, to make reads much cheaper.

Related

Handle one to many relation in Parse in iOS

I have this scenario:
One post could have many comments. So I create a Post class and a Comment class in Parse.com. Here are the definitions or the class and their data:
One post:
The post have two comments:
I want to retrieve the post with the first comment from a specific author. Here is my code:
PFQuery *query = [PFQuery queryWithClassName:#"Post"];
[query orderByAscending:#"createdAt"];
[query findObjectsInBackgroundWithBlock:^(NSArray *posts, NSError *error) {
for (PFObject* obj in posts) {
PFRelation* comments = [obj objectForKey:#"comment"];
PFQuery* theQuery = [comments query];
[theQuery whereKey:#"author" equalTo:#"John"];
[theQuery getFirstObjectInBackgroundWithBlock:^(PFObject *comment, NSError *error) {
NSLog(#"Post title=%#,body=%#", [obj objectForKey:#"title" ],[obj objectForKey:#"body"]);
NSLog(#"Comment content=%#",[comment objectForKey:#"content"]);
}];
}
}];
I don't think it's efficient although it works. And it's hard to tell when the queries are finished because there are two nested asynchronized calls.
Does anybody have better solution? Thanks.
EDIT:
The reason I think it's not efficient is because there are nested queries. But I don't know how to get what I want by using Relation. Maybe I should not use Relation? Instead, I should assign ObjectId of Post to Comment class? (But this method is not as easy as Relation in inputing data)
Relation type is recommended for many to many while for one to many "pointer" and "array" are recommended

Get objectId of created book and store in a column in author using Parse

I would like to store a objectId as a pointer.
//This is supposed to get the objectId of the book
- (IBAction)clickEnterpilensgard:(id)sender {
PFQuery *query = [PFQuery queryWithClassName:#"books"];
[query includeKey:#"objectId"];
[query findObjectsInBackgroundWithBlock:^(NSArray *allBookids, NSError *error) {
for (PFObject *getoneId in allBookids) {
bookIdString = getoneId[#"objectId"];
NSLog(#"retrieved related post: %#", bookIdString);
}
}];
}
Here i want to update a column in Author "mybook" as the object ID i get from the above code
PFQuery *updatemybook = [PFQuery queryWithClassName:#"User"];
// Retrieve the object by id
[updatemybook getObjectInBackgroundWithId:#"xWMyZ4YEGZ" block:^(PFObject *updatethebook, NSError *error) {
updatethebook[#"myBook"] = bookIdString;
}];
This is the error code i get:
2014-09-07 14:38:15.974 MyApp[8397:90b] retrieved related post: (null)
2014-09-07 14:38:15.974 MyApp[8397:90b] retrieved related post: (null)
2014-09-07 14:38:16.072 MyApp[8397:90b] Error: no results matched the query (Code: 101, Version: 1.2.20)
Edited: I manage to get it working!!
Inside the "getObjectInBackgroundWithId:" I used this piece of code:
NSString *storeId = [NSString stringWithFormat:#"%#", gameScore.objectId];
NSLog(#"Id: %#", storeId);
You should use getObjectWithId for this instead. That will solve your problem.
But let me make a suggestion. I would use a relation column type instead of pointer because that is most likely what you are trying to do.
I would take a look at the API Documentation because it is really helpful and would have solved your problem quite quickly. The guide is super helpful as well.
https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/getObjectWithId:
https://parse.com/docs/ios_guide#objects/iOS

iOS Parse SDK: Multiple query results put into an Array

Scenario = I have an app where users can send each other Messages, Comments, and Pokes that are queried to populate the current user's notificationsTableView. There are 3 queries that must take place, one for Messages, two for Comments, and three for Pokes. The code I'm using is below...
PFQuery *messageQuery = [PFQuery queryWithClassName:#"Message"];
[messageQuery whereKey:#"receiverID" equalTo:[PFUser currentUser][#"userID"]];
[messageQuery orderByDescending:#"createdAt"];
[messageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
messages = objects;
}];
PFQuery *pokeQuery = [PFQuery queryWithClassName:#"Poke"];
[pokeQuery whereKey:#"receiverID" equalTo:[PFUser currentUser][#"userID"]];
[pokeQuery orderByDescending:#"createdAt"];
[pokeQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
pokes = objects;
}];
PFQuery *commentsQuery = [PFQuery queryWithClassName:#"Comment"];
[commentsQuery whereKey:#"receiverID" equalTo:[PFUser currentUser][#"userID"]];
[commentsQuery orderByDescending:#"createdAt"];
[commentsQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
comments = objects;
}];
What is desired = To consolidate the following arrays: "messages", "pokes", and "comments" into a single array (notificationsArray) that I can sort by "createdAt" and populate my notificationsTableView with notificationsArray objectAtIndexPath:indexPath.row.
Problems I have encountered = (there are two)
(1) When I NSLog the results of any of these queries like so...
PFQuery *messageQuery = [PFQuery queryWithClassName:#"Message"];
[messageQuery whereKey:#"receiverID" equalTo:[PFUser currentUser][#"userID"]];
[messageQuery orderByDescending:#"createdAt"];
[messageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
messages = objects;
}];
NSLog(#"messages = %#", messages);
It logs "messages = (null)". I can not for the life of me figure out why it is not being set. I know there are messages because when I NSLog the "objects" Array that comes from the query it gives me what I want. It's like the contents of the query will not leave the scope of the query itself. All of the queries above do this. If I can not get the contents of the query out of that block then I can not create an array of all of the arrays to populate the notificationsTableView with, so I'm screwed. Please help.
(2) Even if I do get the results from the queries into individual arrays, I am not sure how to create an array of arrays and order them by a key. Can anyone help me with this? Please.
You are probably looking for the +orQueryWithSubqueries:(NSArray *)queries method, but I don't understand what the return value description is:
a PFQuery that is the or of the passed in PFQueries
I'm thinking this means || (or) ?
You would do it like this:
NSArray *queryArray = [NSArray arrayWithObjects:messageQuery,pokeQuery,commentsQuery,nil];
PFQuery *allQueries = [PFQuery orQueryWithSubqueries:queryArray];
[allQueries findObjects... {
As for the second error, you are right, value is not retained because when the block loses scope all of the local variables inside get destroyed in the autoreleasepool. You need to retain this by using a strong property. self.messages = objects;
(1) You are logging messages outside of the callback function, and the log comes before the callback function returned. Try to log messages into your callback, just after assigning it.
[messageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
messages = objects;
NSLog(#"messages = %#", messages)
}];
(2) Before sorting, create a NSMutableArray and use the addObjectsFromArray: method with each retrieved array.
To sort notifications, you should use a NSSortDescriptor, which is a mechanism that describes how to sort an array according to the format of contained objects. Here's an example that could match your needs:
NSSortDescriptor *createdAtDescriptor = [[NSSortDescriptor alloc] initWithKey:#"createdAt" ascending:YES];
notificationsArray = [messages sortedArrayUsingDescriptors:#[createdAtDescriptor]];
Hope this help!
EDIT: you can embed your temporary NSMutableArray into an autorelease pool to avoid useless memory leaks, so that the dedicated memory is freed just after you proceed to display.
EDIT: you can use orQueryWithSubqueries Parse method to merge several requests into a single one. It's not annoying in your case cause you're sorting PFObject according to their createdAt key, which is common to every PFObject. In any case, you will have to check PFObject types to display them according to their type. Please see full documentation here. Does not work for queries returning several kind of objects!

JOIN TABLES parse.com

I have two parse tables POST and COMMENT.
I want to get all post where there are new comments. I am making checks based on viewedDate field in post table and createdAt field in Comment Table.
SELECT *
FROM POST p
INNER JOIN COMMENT c
ON p.objectId==c.pId and c.createdAt > p.viewedDate;
PFQuery *post = [ParsePost query];
[post findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
PFQuery *post = [ParsePost query];
PFQuery *comment = [ParseComment query];
//how add greater than for keys from other table?
//line below crashes
[comment whereKey:#"createdAt" greaterThan:[objects valueForKey:#"viewedDate"]];
[comment whereKey:#"post" matchesKey:#"objectId" inQuery:post];
[post whereKey:#"objectId" matchesKey:#"post" inQuery:comment];
[post findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
}];
}];
UPDATE
*This is how i do it currently*
PFQuery *postView = [ParsePost query];
[postView whereKey:#"author" equalTo:[ParseLigoUser currentUser]];
[postView findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
for (ParsePost *post in objects) {
PFQuery *comment = [ParseComment query];
[comment orderByDescending:#"createdAt"];
if (post.viewedDate) {
[comment whereKey:#"createdAt" greaterThan:post.viewedDate];
}else {
continue;
}
[comment whereKey:#"post" equalTo:post];
[comment countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
if (number>0) {
if (!self.newposts) {
self.newposts = [NSMutableDictionary dictionary];
}
[self.newposts setObject:#"NOTSEEN" forKey:[post.objectId stringByAppendingString:#"Comment"]];
self.postCount += 1;
}
}];
}
}];
UPDATE 2 with relations
PFQuery *postView = [ParsePost query];
[postView whereKey:#"author" equalTo:[ParseLigoUser currentUser]];
[postView setLimit:100];
[postView findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
for (ParsePost *post in objects) {
PFRelation *rel = [post relationForKey:#"hasComment"];
PFQuery *query = [rel query];
[query orderByDescending:#"createdAt"];
if (post.viewedDate) {
[query whereKey:#"createdAt" greaterThan:post.viewedDate];
}else {
continue;
}
[[rel query] countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
if (number>0) {
if (!self.newposts) {
self.newposts = [NSMutableDictionary dictionary];
}
[self.newposts setObject:#"NOTSEEN" forKey:[post.objectId stringByAppendingString:#"Comment"]];
self.postCount += 1;
}
}];
}
}];
Your problem isn't really the lack of join table queries on parse, but rather that you designed your database with a sql mindset instead of a NoSQL mindset (which of course is common for all who comes from the relation db world).
When designing your database "schema" for parse (or any other NoSQL database) you need to free yourself from thinking in relations and normalization etc to thinking in data access. How will you access your data? Start by thinking on how you will query your data, and then design your db to optimize for these queries. The most important for your mobile app is to minimize the number of connections to a remote server, and to minimize client side handling.
There are several ways to fix solve your current problem. You can, of course, create some queries that will work around the lack of join table queries, and handle some of this on the client. That could probably work short-term if this feature needs to be implemented quickly.
A long-term approach would be to redesign your schema to meet your requirement of easily retrieving posts that have new comments.
One solution (of several possible):
Create a new property in the Post class: “newcomments” that is a boolean.
Create a cloud code snippet that updates the newcomments property in Post whenever a new comment is created (sets it to TRUE).
This snippet should be run in the afterSave hook for Comment (https://parse.com/docs/cloud_code_guide#functions-aftersave)
Then, whenever you open a post to see the new comments, you reset this field to FALSE in the background.
Now you can query for posts where newcomments equalTo false
Or, instead of newcomments being a boolean, you could just as well use it for storing an array of pointers to the actual new comments (the afterSave hook updates this array with the pointer to the new comment). This way, you don’t need a second query for getting the new comments once you open the post. Here, you clear the newcomments property as soon as you’ve read the comments (or opened the post and have an array of the comments).
This storing of an array probably strikes a bad note in your SQL mindset, but this is one of many differences between SQL and NoSQL, since the latter is more focused on query efficiency than storage and consistency.
Or, if you don’t want to store this in the Post object, you could create a new PostTracker (or whatever) class to handle this. Maybe there are other things you would want to track (certainly, there might be in the future, even if you don’t have an idea for that at the moment).

Query on Parse relational data is not coming back ordered using orderedByAscending

I'm querying relation data on parse and I would like the objects to come back ordered by the date they were created. I've had this method work before but haven't been able to get an ordered query using relational data. The query return is in a random order. Thanks in advance! Here's my code:
PFQuery *postQuery = [PFQuery queryWithClassName:#"Post"];
[roomQuery whereKey:#"name" equalTo:self.postName];
NSError *error;
//done on main thread to have data for next query
NSArray *results = [postQuery findObjects:&error];
PFObject *post;
if ([results count]) {
post = [results objectAtIndex:0];
NSLog(#"results were found");
} else {
NSLog(#"results were not found");
}
PFRelation *commentsRelation = [#"Comments"];
[commentsRelation.query orderByAscending:#"createdAt"];
[commentsRelation.query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"Error Fetching Comments: %#", error);
} else {
NSArray *comments = objects;
}
I'm a little confused by your code,
you create a "postQuery", and call it, but never use any of its data.
There's also a roomQuery that never seems to have been allocated, or used.
You're querying a specific post by its name. Are you controlling its name? If not, you should use id's
what is PFRelation commentsRelation = [#"Comments"];
Probably because it's just a snippet, this stuff is dealt with elsewhere; however, for my answer, I'm assuming that your "comments" field is an array of "Comment" class objects.
Option 1:
PFQuery * postQuery = [PFQuery queryWithClassName:#"Post"];
[postQuery whereKey:#"name" equalTo:self.postName];
// again, possibly an id field would be more reliable
// [postQuery whereKey:#"objectId" equalTo:self.postId];
[postQuery includeKey:#"Comments"];
PFObject * post = [postQuery getFirstObject];// no need to download all if you just want object at [0]
// this will contain your post and all of it's comments with only one api call
// unfortunately, it's not sorted, so you would have to run a sort.
NSArray * comments = [post[#"Comments"] sortedArrayUsingComparator: ^(id obj1, id obj2) {
return [obj1[#"createdAt" compare: obj2[#"createdAt"];
}];
Option 2:
Perhaps a better option is to rework your data structure and instead of associating the comments to the post, you could associate the post to the comments (as in the parse docs)
PFQuery * postQuery = [PFQuery queryWithClassName:#"Post"];
[postQuery whereKey:#"name" equalTo:self.postName];
// again, possibly an id field would be more reliable
// [postQuery whereKey:#"objectId" equalTo:self.postId];
PFQuery * commentQuery = [PFQuery queryWithClassName:#"Comment"];
[commentsQuery whereKey:#"parent" matchesQuery:postQuery]; // when creating a comment, set your post as its parent
[commentsQuery addOrderDescending:#"createdAt"]
[commentQuery findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for myPost
}];
Both of the above solutions avoid making extra unnecessary api calls (parse charges based on calls after all!).

Resources