0

I'm trying out the "Sample Blog App" on Parse Server for iOS and cannot figure out what is the smartes way to fetch all child objects of another class (together with the parent objects).

The "Sample Blog App" (which creates automatically when you create a new account) contains the classes Comment and Post. The Comment class contains a relation to the Post class as shown below (from the dashboard), but there is no relation in the opposite direction.

enter image description here

Now, I want to fetch all posts and all the comments related to each post. The code below does that, but I'm assuming there must be a smarter way...? If you know how, please share. Thanks in advance!

- (void)fetchPosts {

    NSString *commentsKey = @"comments";
    NSString *postKey = @"post";

    PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
    [query includeKey:postKey];
    [query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {

        if (error == nil) {

            NSMutableArray *array = [[NSMutableArray alloc]init];

            for (PFObject *comment in objects) {

                PFObject *post = [comment objectForKey:postKey];
                NSDictionary *existingPostDict = [[array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%K = %@", @"post.objectId", post.objectId]] firstObject];

                if (existingPostDict) {
                    // update comments
                    NSArray *comments = [[existingPostDict objectForKey:commentsKey] arrayByAddingObject:comment];

                    // create new dictionary and replace the old one
                    NSDictionary *newPostDict = [[NSDictionary alloc]initWithObjectsAndKeys:[existingPostDict objectForKey:postKey], postKey, comments, commentsKey, nil];
                    [array replaceObjectAtIndex:[array indexOfObject:existingPostDict] withObject:newPostDict];
                }
                else {
                    // first post, create a new dict
                    NSDictionary *newPostDict = [[NSDictionary alloc]initWithObjectsAndKeys:post, postKey, @[comment], commentsKey, nil];
                    [array addObject:newPostDict];
                }
            }
            self.posts = array; // assuming: @property (nonatomic, strong) NSArray *posts; 
        }
        else {
            NSLog(@"Error fetching posts: %@", error.localizedDescription);
        }
    }];
}
turingtested
  • 6,356
  • 7
  • 32
  • 47

3 Answers3

0

Instead of using include on your query you should use whereKey:equals: and pass the post object as the second argument. This will filter and return only the comment objects that contain that have that post as their value for post

EmilioPelaez
  • 18,758
  • 6
  • 46
  • 50
  • I don't really follow you... First, I don't want to filter anything, as I wrote I want "all posts and all the comments related to each post", and second, what should be the first argument in `whereKey:equals:` did you mean ? – turingtested Feb 04 '19 at 15:00
  • My bad, I misread your question and thought you wanted to get the comments for a given post (a common question). You could change the model to store the comments in an array in the post, but I think the current method is less prone to errors, even if it requires a little bit more logic. – EmilioPelaez Feb 04 '19 at 16:02
0

One problem I see with your query is that there is a possibility this will not fetch every post in the database. If a post has 0 comments, none of the Comment objects will have a reference to it and thus you will not receive it.

Therefore you should actually do a query on "Post" and in its completion do a query on "Comment". This way you will not miss any posts with 0 comments. When you do this, you will not need to include the "post" key in the Comment query. This has multiple benefits.

First, each include is also another query for that object. So each new Comment object will create another query in the backend. You will get rid of this automatically.

Second, for a "Post" with multiple comments, you will be querying for the same post multiple times and that same post will be returned multiple times which consumes unnecessary bandwidth.

After getting Posts and Comments separately just combine them.

Apart from that I would do the combining like so which I find more readable but that is just personal preference.

- (void)fetchPosts {

NSString *commentsKey = @"comments";
NSString *postKey = @"post";

PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
[query includeKey:postKey];
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {

    if (error == nil) {

        NSMutableArray *array = [[NSMutableArray alloc]init];
        NSMutableDictionary *d = [NSMutableDictionary dictionary];
        for (PFObject *comment in objects) {

            PFObject *post = [comment objectForKey:postKey];
            if (d[post.objectId]) {
                [d[post.objectId][commentsKey] addObject:comment];
            }
            else{
                d[post.objectId] = [NSMutableDictionary dictionary];
                d[post.objectId][postKey]=post;
                d[post.objectId][commentsKey] = [NSMutableArray arrayWithObject:comment];
            }

        }
        for (NSString *key in [d allKeys]) {
            [array addObject:d[key]];
        }

        self.posts = array; // assuming: @property (nonatomic, strong) NSArray *posts;
    }
    else {
        NSLog(@"Error fetching posts: %@", error.localizedDescription);
    }
}];
}
0

This is how I did it, using findObjectsInBackground together with continueWithSuccessBlock: methods (for better error handling one can choose continueWithBlock: instead):

- (void)fetchPosts {
    /**
     create "post" and "comment" queries and use a BFTask-method from 
     Bolts.framework to chain downloading tasks together (bundled with Parse SDK)
     */
    NSMutableArray *posts = [NSMutableArray new];
    PFQuery *postQuery = [PFQuery queryWithClassName:@"Post"];
    [[[postQuery findObjectsInBackground] continueWithSuccessBlock:^id(BFTask * task) {

        [posts addObjectsFromArray:task.result];
        PFQuery *commentsQuery = [PFQuery queryWithClassName:@"Comment"];
        return [commentsQuery findObjectsInBackground];
    }] continueWithSuccessBlock:^id(BFTask * task) {
        /**
         loop through posts and filter out comments with the same objectId in post,
         then create a dictionary with post and related comments. done! :)
         */
        NSMutableArray *postsAndComments = [NSMutableArray new];
        for (PFObject *post in posts) {
            NSArray *comments = [task.result filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%K == %@", @"post.objectId", post.objectId]];
            [postsAndComments addObject:@{@"post":post, @"comments":comments}];
        }
        /**
         note: BFTask-blocks not running in main thread!
        */
        dispatch_async(dispatch_get_main_queue(), ^{
            self.posts = postsAndComments;  // assuming: @property (nonatomic, strong) NSArray *posts;
        });
        return nil;
    }];
}
turingtested
  • 6,356
  • 7
  • 32
  • 47