简体   繁体   中英

NSPredicate nested relationship

I am trying to filter a mutable array of objects using NSPredicate and am having trouble access the level that contains the property I would like to filter on.

To give a simplified example consisting of the similar custom objects.

  • Grandparent
  • Parent
  • Child

I have an NSMutableArray of Grandparents and I would like to find all the Grandparent Objects that have GrandChildren of age 10. Therefore the grandchildren are two levels deep from the root. Child has an age property amongst other things.

ie. Grandparents has an array property Parents and Parents has an array property Children and Children has an integer property age.

The following NSPredicate has returned no results. "SELF.parents.children.age == 10" .

I realise that as these are nested collections this predicate is likely the wrong way to go about it but I am stuck as to how to access that level. Perhaps via a Subquery or Collection Operator but I cannot work it out.

One thing to keep in mind is that I obviously still want GrandParents that have multiple Grandchildren of different ages, one of which is aged 10.

The "obvious" solution would be the predicate:

"ANY parents.children.age == 10"

However, the "ANY" operator does not work with nested to-many relationships. Therefore, you need a SUBQUERY:

NSArray *grandParents = your array of GrandParent objects;
NSPredicate *predicate = [NSPredicate
   predicateWithFormat:@"SUBQUERY(parents, $p, ANY $p.children.age == 10).@count > 0"];
NSArray *filtered = [grandParents filteredArrayUsingPredicate:predicate];

Remarks:

  • Using SELF in the predicate is not necessary. filteredArrayUsingPredicate applies the predicate to each GrandParent object in the array.
  • The usage of SUBQUERY in predicates seems to be poorly documented. There is one example in the NSExpression class reference. See also Quick Explanation of SUBQUERY in NSPredicate Expression .
  • In this case, the predicate inside SUBQUERY is applied to each Parent of a single GrandParent . The SUBQUERY returns the parents that have any child aged 10. So "SUBQUERY(...).@count > 0" evaluates to TRUE if the grandparent has at least one parent that has any child aged 10.

ADDED : I just found out that it can actually be done without SUBQUERY:

NSPredicate *predicate = [NSPredicate
    predicateWithFormat:@"ANY parents.@unionOfArrays.children.age == 10"];

works and gives the desired result. (It could be less effective than the SUBQUERY, but I did not test that.)

As an alternative to Martin R's answer, you might consider using a block predicate instead. Something like this:

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * bindings) {
  GrandParent *grandparent = evaluatedObject;
  for (Parent *parent in grandparent.parents)
    for (Child *child in parent.children)
      if (child.age == 10)
        return YES
  return NO;
}];

Assuming GrandParent , Parent and Child are the appropriate class names of the various objects.

Personally I prefer this form, because I always feel with a string predicate that I'm mixing languages in the code, which I think makes it less readable. The choice is obviously up to you though.

Update: Having re-read the question, I now realise that the condition was more complex than I originally thought. I've updated my answer to loop over the parents and children, but Martin R's answer is now clearly a lot simpler. Still this is a possible solution to consider.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM