Fetch Request Predicate to filter a Core Data entity where a small NSSet is contained within larger NSSet

1.6k Views Asked by At

I'm writing a 100% SwiftUI app with iOS and macOS targets, using Core Data and NSPersistentCloudKitContainer to backup to iCloud and sync between devices signed into the same AppleID.

The three entities involved are: Meals, Portions, Foods

Each Meal has:

  • a many-to-many relationship with Portions
  • a many-to-many relationship with Foods

Each Portion has:

  • a many-to-many relationship with Foods

I'm attempting to prepare a predicate to filter meals where each meal portion contains a certain food OR the meal contains a certain food directly.

So I'll provide a practical example...

Meal 1

consists of...

Portions

  • Banana Smoothie
  • Egg Sandwich

Foods

  • Apple

The Portion with the name Banana Smoothie contains the following Foods:

  • Banana
  • Cows Milk
  • Honey

Meal 2

consists of...

Portions

  • Blueberry Smoothie
  • Ham Sandwich

Foods

  • Banana

For the macOS target, I'm using the relatively new Table structure to present a table that lists all Meal entities for a certain Food entity, including those Meal entities where one or more of the Portion entities contains that certain Food entity.

If I refer back to the above example, for the Food entity named "Banana", I'd want my predicate to filter my FetchRequest such that Meal entities with names "Meal 1" & "Meal 2" are in the results.

@FetchRequest var meals: FetchedResults<Meal>

Here is the current predicate for this FetchRequest...

let portions = NSSet(object: food.foodPortions as Any)
let predicatePartA = NSPredicate(format: "%@ IN mealFoods", food)
let predicatePartB = NSPredicate(format: "ANY %@ IN mealsPortions", portions)
let predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [predicatePartA, predicatePartB])

where food is @ObservedObject var food: Food and mealFoods and mealsPortions are NSSet many-to-many relationships to from every Meal object.

predicatePartA works fine, I suspect because it is one single Object IN an NSSet of objects.

predicatePartB doesn't crash, but it also doesn't resolve any meals, I suspect because I'm providing a set instead of a single object.

I've attempted to research for some time now how this might be achieved and the best I can come up with are the operators... @distinctUnionOfSets @"SUBQUERY()

...but apart from this website there is little documentation I can find on how to implement them.

UPDATE

With help from @JoakimDanielson I've attempted to use SUBQUERY...

let predicatePartB = NSPredicate(format: "SUBQUERY(mealsPortions, $portion, $portion IN %@).@count > 0", portions)

AND

let predicatePartB = NSPredicate(format: "SUBQUERY(mealsPortions, $portion, $portion IN %@).@count != 0", portions)

Again this does not crash, but it does not provide the expected results for the fetch request.

Any suggestions please?

Also worth noting that I've found some better documentation by Apple that supports this syntax although, because the predicate isn't working, I still not sure it is correct.

init(forSubquery:usingIteratorVariable:predicate:)

with the syntax

SUBQUERY(collection_expression, variable_expression, predicate);
1

There are 1 best solutions below

1
andrewbuilder On

Short answer...

let portions = food.foodPortions
let predicatePartB = NSPredicate(format: "(SUBQUERY(mealsPortions, $p, $p IN %@).@count != 0)", portions!)

OR, if I prepare a computed property for portions...

var portions: NSSet {
    if let p = food.foodPortions { return p }
    return NSSet()
}

then in the creation of the predicate I'm not required to force unwrap the optional NSSet...

let predicatePartB = NSPredicate(format: "(SUBQUERY(mealsPortions, $p, $p IN %@).@count != 0)", portions)

Most people reading this probably won't want to know the detail but nonetheless I feel compelled to write this down, so the long answer is...

... in two parts, or at least recognises two contributors who helped me solve it.

Part 1

Primarily @JoakimDanielson for confirming that SUBQUERY was the right path to a solution and for taking the time to work out the syntax for my case and also for questioning what eventually turned out to be a very basic error, the problem was not my SUBQUERY syntax but in fact the manner in which I was preparing the NSSet that I used in the predicate string.

All I needed to do was change...

let portions = NSSet(object: food.foodPortions as Any)

to...

let portions = food.foodPortions

after which I could either force unwrap it in the creation of the predicate, or otherwise prepare a computed property (the solution I chose) - as detailed above in the short answer.

This was simply an error as a result of my inadequate understanding of the collections NSSet and Set. A refresher of the swift.org docs helped me.

Part 2

Secondly this SO Q&A "How to create a CoreData SUBQUERY with BETWEEN clause?" and the reference to this clever article titled "SUBQUERY Is Not That Scary" by @MaciekCzarnik.

I went through the process of reducing the necessary iteration until I could line for line compare the SUBQUERY syntax. While it didn't actually solve my problem, this did encourage me to try numerous predicate syntax alternatives until I returned with an understanding of SUBQUERY and was able to confirm the original syntax was correct. It provided me with the type of example my brain can comprehend and work through to develop an understanding of how SUBQUERY actually works.

Because you have nothing better to read at the current moment in time...

var iterationOne: [Meal] {
    let meals: [Meal] = []//all meals
    var results = [Meal]()
    for meal in meals {
        var portionsMatchingQuery = Set<Portion>()
        if let mealPortionsToCheck = meal.mealsPortions {
            for case let portion as Portion in mealPortionsToCheck {
                if portions.contains(portion) == true {
                    portionsMatchingQuery.insert(portion)
                }
            }
            if portionsMatchingQuery.count > 0 { results.append(meal) }
        }
    }
    return results
}

can be simplified to _

var iterationTwo: [Meal] {
    let meals: [Meal] = []//all meals
    let results = meals.filter { meal in
        var portionsMatchingQuery = Set<Portion>()
        if let mealPortionsToCheck = meal.mealsPortions {
            for case let portion as Portion in mealPortionsToCheck {
                if portions.contains(portion) == true {
                    portionsMatchingQuery.insert(portion)
                }
            }
            return portionsMatchingQuery.count > 0
        }
        return false
    }
    return results
}

can be simplified to _

var iterationThree: [Meal] {
    let meals: [Meal] = []//all meals
    let results = meals.filter { meal in
        let portionsMatchingQuery = meal.mealsPortions?.filter { portion in
            for case let portion as Portion in meal.mealsPortions! {
                return portions.contains(portion) == true
            }
            return false
        }
        return portionsMatchingQuery?.count ?? 0 > 0
    }
    return results
}

can be simplified to _

var iterationFour: [Meal] {
    let meals: [Meal] = []//all meals
    let results = meals.filter { meal in
        meal.mealsPortions?.filter { portion in                
            for case let portion as Portion in meal.mealsPortions {
                return portions.contains(portion) == true
            }
            return false
        }.count ?? 0 > 0
    }
    return results
}

iterationFour == predicatePartB