Omega
Omega

Reputation: 3

Realm filter results based on values in child object list

This is how my Realm objects look:

class Restaurant: Object {
    @objc dynamic var name: String? = nil
    let meals = List<Meal>()
}

class Meal: Object {
    @objc dynamic var mealName: String? = nil
    let tag = RealmOptional<Int>()
}

I'm trying to fetch all meals that have some tags (I know I can filter all Realm objects of type Meal for specific tags), but the goal is to fetch all Restaurant objects and filter it's Meal child objects based on tag values.

I tried filtering like this:

restaurants = realm.objects(Restaurant.self).filter("meals.@tags IN %@", selectedTags)

but this won't work. Is there a way to filter results based on values in child object list?

To clarify the question, this is an example how filtering should work for selectedTags = [1, 2, 3] This is the whole Restaurant model that is saved in Realm.

[Restaurant {
    name = "Foo"
    meals = [
        Meal {
            mealName = "Meal 1"
            tag = 1
        },
        Meal {
            mealName = "Meal 2"
            tag = 2
        },
        Meal {
            mealName = "Meal 7"
            tag = 7
        }
    ]
}]

Filtering should return this:

[Restaurant {
    name = "Foo"
    meals = [
        Meal {
            mealName = "Meal 1"
            tag = 1
        },
        Meal {
            mealName = "Meal 2"
            tag = 2
        }
    ]
}]

Upvotes: 0

Views: 1250

Answers (2)

Jay
Jay

Reputation: 35648

Here's one possible solution - add a reverse refererence to the restaurant for each meal object

class Restaurant: Object {
    @objc dynamic var name: String? = nil
    let meals = List<Meal>()
}

class Meal: Object {
    @objc dynamic var mealName: String? = nil
    let tag = RealmOptional<Int>()
    @objc dynamic var restaurant: Restaurant?  //Add this
}

then query the meals for that restaurant with the tags you want.

let results = realm.objects(Meal.self).filter("restaurant.name == %@ AND tag IN %@", "Foo", [1,2])

LinkingObjects could also be leveraged but it depends on what kind of queries will be needed and what the relationships are between Restaurants and Meals - I am assuming 1-Many in this case.

if you want ALL restaurants, then LinkingObjects is the way to go.

Edit:

Thought of another solution. This will work without adding a reference or an inverse relationship and will return an array of restaurants that have meals with the selected tags.

let selectedTags = [1,2]
let results = realm.objects(Restaurant.self).filter( {
    for meal in $0.meals {
        if let thisTag = meal.tag.value { //optional so safely unwrap it
            if selectedTags.contains(thisTag) {
                return true //the tag for this meal was in the list, return true
            }
        } else {
            return false //tag was nil so return false
        }
    }
    return false
})

Upvotes: 0

Chris Shaw
Chris Shaw

Reputation: 1610

In short, you cannot do what you are asking. Not within a Realm query (and therefore benefit from update notifications if that is important) at least. No doubt you can make some kind of structure containing what you want though via non-Realm filtering.

To better answer, let's first consider what you're trying to produce as a query result. As you say, your attempt above won't work. But you're trying to filter Restaurants by having some Meals matching some criteria; this is probably achievable, but your resulting query on Restaurant type would then produce a list of Restaurants. Each restaurant would still have a natural property of all its Meals, and would require the same filter applied again to the meals.

It makes sense though to add a function (if you need the search criteria to be dynamic, use a computed property if the filter is always the same tags) to the Restaurant class that produces a view of its Meals matching your criteria.

e.g.

extension Restaurant
{
  var importantMeals : Results<Meal>
  {
    return meals.filter(...)
  }
}

So I think there are two options.

  1. Iterate through all Restaurant objects, and add it to a data structure of your own (Set or array) if its importantMeals property is not empty. Then use the same property to produce the meal list when needed. Or you could use a non-Realm filter to produce that query for you. E.g. realm.objects(Restaurant.self).compactMap {$0}.filter { !$0.importantMeals.isEmpty }
  2. Alternatively, filter all Meals according to your criteria (realm.objects(Meal.self).filter(...)). You could then add a LinkingObjects property to your Meal class to make the Set of Restaurants with relevant Meals.

The correct approach will depend on how you want to use the results, but I'd suggest approach 1 will see you right. Note that you might want to sort the results produced by queries before using if order is of any importance to you (e.g. for displaying in UITableView) as there is no guarantee that the order of objects will be the same for each query performed.

Upvotes: 0

Related Questions