Realm filter results based on values in child object list - swift

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
}
]
}]

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
})

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.
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 }
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.

Related

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

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);
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

Swift NSPredicate SUBQUERY with NOT IN?

Is it possible to use "NOT IN" in a NSPredicate with a SUBQUERY?
In a 'normal' NSPredicate, this works:
NSPredicate(format: "NOT (uid IN %#)", uids)
However, I'm trying this "NOT IN" syntax in a NSPredicate with SUBQUERY and it doesn't work (but doesn't crash either):
NSPredicate(format:"SUBQUERY(forests.treeFamily, $tree, NOT ($tree._id IN %#)) .#count > 0", arrayOfTreesIds)
While the following "IN" syntax works fine:
NSPredicate(format:"SUBQUERY(forests.treeFamily, $tree, $tree._id IN %#) .#count > 0", arrayOfTreesIds)
Any idea how I could achieve this?
UPDATED
The answer I got from Jay made me think I wasn't clear enough on the schema/class.
So let's make an example with dogs.
We have a ShelterClass, that has a property of Dogs. It's an array of Dog objects, to keep tracks of which dogs are hosted in the shelter.
The Dog class has a "race" property, which is a reference to a Race class.
I want to filter Shelters that DON'T have certain races of dogs.
While I would use this to filter Shelters that DO have certain races of dogs:
NSPredicate(format:"SUBQUERY(dogs.race, $race, $race._id IN %#) .#count > 0", arrayOfRaceIds)
I can't find how to filter out / use a "NOT IN" syntax.
Super easy! Here's the format for a regular filter
If you have a DogClass
class DogClass: Object {
#objc dynamic var name = ""
}
and then want a list of dogs that are not named Dino and Fido, here's the code
let dogNames = ["Fido", "Dino"]
let results = realm.objects(DogClass.self).filter("NOT dog_name IN %#", dogNames)
The result of a subquery is going to be dependent on what result you expect and what the subquery is for.
For example, let's say we have a PersonClass that has property of dogs, which is a list of dogs they know. If we want all persons that do not know Fido or Dino, this is the query
let personResults = realm.objects(PersonClass.self).filter("NOT ANY dogs.dog_name in %#", dogNames)
EDIT
Based on an updated question, let's try this. Since I used a PersonClass in the above I will pose this question.
I want a list of all of the people (Shelters) that do not have a breed (race)
of Hound. Here's the Breed class to track the breeds
class BreedClass: Object {
#objc dynamic var breed = ""
}
and the DogClass that has a breed property (like 'Race' in the question)
class DogClass: Object {
#objc dynamic var dog_id = NSUUID().uuidString
#objc dynamic var dog_name = ""
#objc dynamic var dog_breed: BreedClass?
override static func primaryKey() -> String? {
return "dog_id"
}
}
and then finally the Person class that has a List of DogClass objects
class PersonClass: Object {
#objc dynamic var person_id = UUID().uuidString
#objc dynamic var first_name = ""
let dogs = List<DogClass>()
override static func primaryKey() -> String? {
return "person_id"
}
}
Then we have some populated breed objects
let b0 = BreedClass()
b0.breed = "Mut"
let b1 = BreedClass()
b1.breed = "Poodle"
let b2 = BreedClass()
b2.breed = "Hound"
and then add breeds to the dogs and add the dogs to the persons. In this case we're only going to have one dog that's a b2, Hound
let d2 = DogClass()
d2.dog_name = "Sasha"
d2.dog_breed = b2
In this case I added 4 people, Bert, Ernie, Grover and The Count. Ernie was the only person I added the hound to.
Finally a query that will return all people that do NOT have a breed of Hound.
let breed = "Hound"
let personResults = realm.objects(PersonClass.self).filter("NOT ANY dogs.dog_breed.breed == %#", breed)
for person in personResults {
print(person.first_name)
}
and the output
Bert
Grover
The Count
And Ernie is missing because he has a Hound.

Filter querying multiple objects from Realm using List of Primary Keys

I'm trying to query multiple objects from Realm using a List of Primary Key Strings. I know I can do this using a for..in loop but I'd rather use a filter if possible.
primaryKeyArray contains a number of Strings
class Item : Object {
#objc dynamic var itemKey = NSUUID().uuidString
}
var primaryKeyArray : List<String>?
//Assume Realm DB already contains multiple Item Objects
//primaryKeyArray contains "key1", "key2", "key3", etc..
let predicate = NSPredicate(format: "itemKey == %#", primaryKeyArray)
let items = realm.objects(Item.self).filter(predicate)
I know the problem is with my predicate format. Not sure whether to use some form of CONTAINS or what? Any help with the predicate syntax would be greatly appreciated!
I think you are asking how to query Realm for items that have keys that match a set of keys in an array.
So given a DogClass Realm Object
class DogClass: Object {
#objc dynamic var dog_id = NSUUID().uuidString
#objc dynamic var dog_name = ""
override static func primaryKey() -> String? {
return "dog_id"
}
}
and suppose we know we want to retrieve three dogs that match some given primary keys
let keysToMatch = ["302AC133-3980-41F3-95E8-D3E7F639B769", "54ECC485-4910-44E5-98B9-0712BB99783E", "71FE403B-30CD-4E6C-B88A-D6FDBB08C509"]
let dogResults = realm.objects(DogClass.self).filter("dog_id IN %#", keysToMatch)
for dog in dogResults {
print(dog.dog_id, dog.dog_name)
}
Note the use of IN in the filter, which will match any dogs with id's in the given array.
You can also pass in a Realm List Object instead of a Swift array and get the same result.
let listOfKeysToMatch = List<String>()
listOfKeysToMatch.append("302AC133-3980-41F3-95E8-D3E7F639B769")
listOfKeysToMatch.append("54ECC485-4910-44E5-98B9-0712BB99783E")
listOfKeysToMatch.append("71FE403B-30CD-4E6C-B88A-D6FDBB08C509")
let dogResults2 = realm.objects(DogClass.self).filter("dog_id in %#", listOfKeysToMatch)
for dog in dogResults2 {
print(dog.dog_id, dog.dog_name)
}
let predicate = NSPredicate(format: "itemKey IN %#", primaryKeyArray)

realm predicate with an object inside object

I have the following Realm Objects
class Patient: Object {
#objc dynamic var name: String?
let list = List<RString>()
}
class RString: Object {
#objc dynamic var stringValue: String?
}
I need to filter Patient objects that have an RString component in List with stringValue = "test"
Is something like this possible?
patients = realm?.objects(Patient.self).filter("name = 'name1' AND #% IN list", RString(stringValue: 'test'))
You need to use a SUBQUERY to be able to access the properties of the elements of a List in an NSPredicate. The SUBQUERY will evaluate true for every Patient whose list property includes at least 1 RString element whose stringValue matches the provided String.
patients = realm?.objects(Patient.self).filter("name = %# AND SUBQUERY(list,$element,$element.stringValue == %#).#count>0", "name1", "test")

Filtering Realm objects with Swift

I always get the following error when trying to filter my Realm database using NSPredicate:
Property 'text' is not a link in object of type 'getType'
I want to filter my Realm database to show only the items that have some specific text in them. This is what I've tried:
let realm = try! Realm()
let predicate = NSPredicate(format: "typez.text.filter = 'special'")
let filterThis = realm.objects(Publication).filter(predicate)
print(filterThis)
The relevant portion of my model classes is:
class Publication: Object, Mappable {
dynamic var id: Int = 0
var typez = List<getType>()
dynamic var url: String?
}
class getType: Object, Mappable {
dynamic var text: String = ""
}
You mentioned that the relevant portions of you model classes look like so:
class Publication: Object, Mappable {
dynamic var id: Int = 0
var typez = List<getType>()
dynamic var url: String?
}
class getType: Object, Mappable {
dynamic var text: String = ""
}
If I understand you correctly, you want to find Publication instances that have an entry in their typez list with text equal to special. You can express that as:
let realm = try! Realm()
let result = realm.objects(Publication).filter("ANY typez.text = 'special'")
print(result)
I was not liking the accepted answer here because it doesn't actually answer the question... but then it helped me more than I realized. I will now be using closures instead of NSPredicates whenever possible. The actual answer to this question should be a slightly modified version of #NSGangster's answer:
let realm = try! Realm()
//Array of publications
let realmObjects = realm.objects(Publication)
//any publication where .text property == special will be filtered. and filter out empty array
let filterThis = realmObjects.filter({ $0.typez.filter({ $0.text == "special" } != [] ) })
print(filterThis)
.. or something close to that.
But what I was looking for was a bit different. I needed a way to filter on exact words of a multi-word string, and using an NSPredicate with "CONTAINS" would match any containing substring, e.g. a search for "red" would match "fred". Realm doesn't support "LIKE" or regex yet, so using a closure was the only thing I could get to work:
//I was going for a "related terms" result for a dictionary app
let theResults = terms.filter(
{
//Looking for other terms in my collection that contained the
//title of the current term in their definition or more_info strings
$0.definition.components(separatedBy: " ").contains(term.title) ||
$0.more_info.components(separatedBy: " ").contains(term.title)
}
)
With as much of the day as I spent searching, hopefully this helps someone else with a similar issue.
I don't usually use NSPredicate's directly, instead I do an inline predicate closure within the filter paramter.
let realm = try! Realm()
//Array of publications
let realmObjects = realm.objects(Publication)
//any publication where .text property == special will be filtered. and filter out empty array
let filterThis = realmObjects.filter({ $0.getType.filter({ $0.text == "special" } != [] ) })
print(filterThis)