Clifton Labrum
Clifton Labrum

Reputation: 14070

How to Find Orphaned List Items in Realm

Let's say I have a Realm schema where I have a parent class and kids underneath it in a List. Like this:

class Parent: Object{
  @objc dynamic var name = ""
  let kids = List<Kid>()
}

class Kid: Object{
  @objc dynamic var name = ""
}

Let's say that, over time, whenever a Kid was deleted, it was only removed from the Parent object and wasn't deleted from the Realm:

let realm = try! Realm()
let parent = realm.objects(Parent.self)

realm.beginWrite() 

for kid in parent.kids{
  if let index = parent.kids.index(of: kid){
    parent.kids.remove(at: index)
  }
}

try! realm.commitWrite()

I know I can delete the kids from the Realm in the same write transaction as the removal from the parent:

let kids = parent.kids
realm.delete(kids)

...but I have reasons not to.

Is there a way to query the Realm database for all kids that don't belong to a parent? For example, if you were to open my Realm, you could see 100 kid objects, but if you look at the parents, only 5 of the kid objects are actually attached to a parent object.

I have a special use case of Realm where I don't actually want to delete the child List items unless I know they don't have a parent. Is it possible to query a Realm for all parentless kids?

Upvotes: 0

Views: 441

Answers (2)

Jay
Jay

Reputation: 35658

This question is

Is there a way to query the Realm database for all kids that don't belong to a parent?

Sure! Super easy with one line of code.

let results = realm.objects(Parent.self).filter("ANY kids == %@", thisKid)

of course, evaluate to see if there are any results

if results.count == 0 {
    print("kid: \(thisKid?.name) has no parents.")
} else {
    print("found parents for kid: \(thisKid?.name)")
    for kid in results {
        print(kid)
    }
}

EDIT:

In the above code, you can iterate over each kid and see if it has a parent and if not remove it. As a challenge, how about some code that will remove all kids that don't have parents instead of iterating. allKids is a realm List.

let foundParents = realm.objects(Parent.self).filter("ANY kids IN %@", allKids)
let kidsThatHaveParents: [Kid] = allKids.compactMap { kid in
    let x = foundParents.first { $0.kids.contains( kid ) }
    if x != nil { //this is just for clarity, could be shortened
        return kid
    }
    return nil
}

let haveParentSet = Set(kidsThatHaveParents)
let kidsToCheckSet = Set(allKids)

let kidsToRemove = kidsToCheckSet.subtracting(haveParentSet)

the one downside of this is that I utilized a Set to subtract out the kids that have parents. That would load all of the kids into memory, bypassing the lazy loading aspect of realm. Another option would be just just remove each kid that has a parent from the kidsToLook for realm list.

Upvotes: 0

Rob C
Rob C

Reputation: 5073

Unless you use LinkingObjects there's no way to query realm for kids without parents directly. If it's not too late to change your data model then I would suggest using them.

class Parent: Object{
  @objc dynamic var name = ""
  let kids = List<Kid>()
}

class Kid: Object{
  @objc dynamic var name = ""
  let parents = LinkingObjects(fromType: Parent.self, property: "kids")
}

When you add a Kid to Parent.kids realm automatically handles the Kid.parents relationship for you. When you delete a Parent, Kid.parents will be empty (assuming the Kid only had one Parent). The great thing about LinkingObjects is that you can incorporate them into your queries. So to find all Kid objects without parents the query would be:

func fetchKidsWithoutParents1() -> Results<Kid> {
    let realm = try! Realm()
    return realm.objects(Kid.self).filter("parents.@count == 0")
}

If you don't use LinkingObjects, you have to query for all Kid objects and all Parent objects and see if the Kid exists in any Parent.kids List. There are two downsides to this approach. The first is that that all Kid objects will be loaded into memory as you filter them manually. The other is that you can't take advantage of realm notifications because the resulting kids would not be stored in a realm Result object. Note that the following example assumes that there are no two Kid objects with the same name:

func fetchKidsWithoutParents2() -> [Kid] {
    let realm = try! Realm()
    let kids = realm.objects(Kid.self)
    let parents = realm.objects(Parent.self)
    return kids.filter { kid in
        parents.filter("SUBQUERY(kids, $kid, $kid.name == %@).@count > 0", kid.name).count == 0
    }
}

Upvotes: 3

Related Questions