Realm-cocoa: Add support for cascading deletes

Created on 6 Dec 2014  路  67Comments  路  Source: realm/realm-cocoa

Blocked on support in core.

Blocked P-2-Expected T-Feature

Most helpful comment

Almost 4 years since the opening of this issue. For a feature that comes built-in with CoreData. It's kind of disappointing that Realm isn't making this a priority. Slowly moving away from Realm because of this.

All 67 comments

Is this still in the works?

Is this still in the works?

Yes, we'll post here as soon as we have something to share.

Anything new about this Issue / Feature. It's really hard to keep the database consistency with this feature missing.

Looks like this is working well in core now (as of https://github.com/realm/realm-core/pull/867), so it should just be a matter of exposing it through the Objective-C & Swift APIs.

Any estimation on how long until this feature is implemented and exposed?

Interested in this as well :)

We're currently actively working on a last conceptual question around this issue in core, which we want to see resolved so that we're able to keep the related APIs as intuitive as possible. We can't give yet an exact ETA, but we'll keep you posted with our further progress and are looking forward to be able to expose it as much as you.

That would definitely helpful.

I have write a Helper , you can just delete all the object with one line code. I just read all the Realm property value and delete it. maybe it will help you. check out at :https://github.com/com314159/RealmDeleteCascad

[RLMHelper deleteModelCascad:model inRealm:realm];

Looks interesting, but not exactly "safe to use" in a production environment with thousands of users :D

+1

+1

+1

Any update on this issue? This feature is one of the big factors holding me back from adopting Realm.

If that helps anyone, I'm using an alternative approach in the mean time: a kind of garbage collection. The idea is that there's a realm entity called RootClassIdentifier each entry of which contains the name of a class that definitely needs to stay in the DB. Starting from the objects of those classes the tree is gradually copied into a temporary realm. When that's done, the old realm is deleted and the new one is used. I'm aware there should be performance issues if the number of objects to be kept increases drastically, but so far in my use case it takes less than one second. If this ever becomes a problem I'll just start to perform it less often, like on each 10 app runs. Here's a sample code:

- (void)performGarbageCollection {
    NSDate *startOfGCDate = [NSDate date];
    NSError *error;
    NSString *temporaryRealmName = @"tmp";
    // Rename all realm files to tmp
    [self.realm writeCopyToPath:... error:&error]; //generate path to temporaryRealmName
    [self.realm beginWriteTransaction];
    [self.realm deleteAllObjects];
    [self.realm commitWriteTransaction];

    // move the data
    RLMRealm *sourceRealm = [OFMObjectCache realmForDomain:temporaryRealmName];

    [self.realm beginWriteTransaction];
    RLMResults *rootClasses = [RootClassIdentifier allObjectsInRealm:sourceRealm];
    for (RootClassIdentifier *rootClassIdentifier in rootClasses) {
        Class aClass = NSClassFromString(rootClassIdentifier.className);
        for (id object in [aClass allObjectsInRealm:sourceRealm]) {
            [aClass createOrUpdateInRealm:self.realm withObject:object];
        }
    }
    [self.realm commitWriteTransaction];
    sourceRealm = nil;

    NSFileManager *fm = [[NSFileManager alloc] init];
    NSString *pathForTmp = ...;//get the path to the tmp realm

    NSDirectoryEnumerator *enumerator = [fm enumeratorAtPath:pathForTmp];
    // Remove tmp files
    for (NSString *fileName in enumerator) {
        NSString *absoluteString = [pathForTmp stringByAppendingPathComponent:fileName];

        BOOL isDirectory = NO;
        [fm fileExistsAtPath:absoluteString isDirectory:&isDirectory];
        if (!isDirectory) {
            NSRange range = [fileName rangeOfString:temporaryRealmName];
            if (range.location != NSNotFound) {
                BOOL result = [fm removeItemAtPath:absoluteString error:&error];
                if (!result) {
                    // handle error
                }
            }
        } else {
            [enumerator skipDescendants];
        }
    }
    NSDate *endOfGCDate = [NSDate date];
    NSLog(@"Garbage collection took %.3lf", [endOfGCDate timeIntervalSinceDate:startOfGCDate]);
}

Adding root classes goes like this:

[self.realm beginWriteTransaction];
[self.realm addOrUpdateObject:[[OFMRootClassIdentifier alloc] initWithClassName:NSStringFromClass(self.class)]];
[self.realm commitWriteTransaction];

And happens in the init method of classes that have as data models any of those root entities.
I hope this makes sense. So far it seems to work well for me, although I'd drop this approach in a blink if cascading deletions would be available.

+1, I switch back to Core Data. At least Core Data supports cascading delete.

+1

Update on this: As some might now, we had a first concept implemented in Core and initially thought that this would be sufficient. But after further discussions across the teams, it turned out that this concept has some drawbacks and is not always intuitive in its effects. For that reason we're working on a new concept which eliminates those drawbacks, but won't feel foreign in all supported host languages.

Why are we so cautious about this?

Because once we ship a concept, any change away from this would be difficult to introduce. It would be a breaking semantic change. This means that just by updating the version of Realm's frameworks the exactly same code would still compile, but could _behave_ completely differently. This is an extremely undesirable situation, because in comparison to a conventional breaking syntactic change there is no easy way to see all the places where affected APIs changed. So we want to be very clear, that the concept we ship is the one we want to further follow.

Thank you for the update and for the great work of the whole team!

@mrackwitz Would you happen to know if there is an ETA as to when this feature is expected to be delivered to production?

@jasper-chan: No, sorry there is no ETA yet. But I'll keep you posted here as soon as there is more to tell.

Awesome. Glad to see it is being worked. Thanks guys!

I created a CascadingDeletable protocol like this:

protocol CascadingDeletable {
  func childrenToDelete() -> [AnyObject?]
}

and then got some of the objects conforming to that protocol like this:

extension Agency: CascadingDeletable {
  func childrenToDelete() -> [AnyObject?] {
    return [_address, logos]
  }
}

then I created a cascadingDelete function to delete everything recursively:

func cascadingDelete(object: AnyObject?) {
    if let deletable = object as? CascadingDeletable {
      deletable.childrenToDelete().forEach{ child in
        cascadingDelete(child)
      }
    }

    if let realmArray = object as? RLMArray {
      let array = realmArray.flatMap{$0}
      array.forEach { item in
        cascadingDelete(item)
      }
    }

    if let realmObject = object as? RLMObject {
      transactionWithBlock { [weak self] in
        self?.realm.deleteObject(realmObject)
      }
    }
  }

I am using almost the same approach ( but written in Objective-c ) in one of my projects and it works like a charm.

It's been almost 2 years since opening this feature request, any ETA? I was really surprised when I found out this feature is missing, have to stick with Core Data for now...

@xiaodao Have you tried the cascadingDelete function with List? It's not working for me. List can't be cast to RLMArray.

@meilers you can just add another check for List

@xiaodao Yeah, I tried that. The casting still doesn't work because I don't know the generic type that will be used for List. And List doesn't work even if my objects inherit from Object.

@meilers List<Object> can not store subclasses of Object, only instances of Object itself. It's a current limitation of Realm. Read more about it here https://github.com/realm/realm-cocoa/issues/1109.

@meilers Why do you not know the generic type of List?

@JadenGeller It's not that I don't know it - it's that I wish I could make @xiaodao's function reusable for any object I have.

I think if you extend Object and List to conform to CascadingDeletable in an extension (rather than putting all that logic in the function), you could make it work.

Edit: You'd also have to change the CascadingDeletable protocol slightly. Let me give an example.

@JadenGeller you're absolutely right.

Did you figure it out, or would you still like an example?

I can't test it now - but I will. No need for an example unless you want to share with others.

Definitely interested in this. For now I'll try and adopt the CascadingDeletable approach as it's better than what I'm currently doing.

An example with List would be greatly appreciated ! :)

Hello!

I've tried to implement @xiaodao's approach and @JadenGeller's extension.
How do you think about it?

func cascadingDelete(object: AnyObject?) {
    if let deletable = object as? CascadingDeletable {
      deletable.childrenToDelete().forEach{ child in
        cascadingDelete(child)
      }
    }

    if let realmObject = object as? Object {
        try! realm.write({
            realm.delete(realmObject)
        })
    }
}

extension List: CascadingDeletable {
      public func childrenToDelete() -> [AnyObject?] {
          return self.flatMap{ $0 }
      }
}

P.S. I saw a following problem in extension.

fatal error: array cannot be bridged from Objective-C

I've worked around by using 'for-in'

var array = [AnyObject?]()
for i in self {
 array.append(i)
}
return array

Thanks!

Here's my implementation in Swift 3. Supports List, Array, and Results also.
Also supports passing an optional parameter of 'cascade' to the delete method.
This allows more flexibility if you want the ability to choose upon deletion.
No guarantees of speed.

https://gist.github.com/gregpardo/93420647700d31e2a9b10ed7b309cf96

@mrackwitz is there any ETA for this feature?

@kimdv in fairness its trivial to implement yourself, others have posted solutions above 鈽濓笍 But I guess framework level support could be cool

The CascadingDeleteable solution is nice, but the problem is that you are still able to call the old delete(_:) method that doesn't cascade your deletes. If I have a cascading rule, it should guarantee that the delete is ALWAYS cascaded.

This method is error prone and can easily lead to bug later down the road.

We really need support for this internally for this reason.

The example on a Cascading deletion method that @gregpardo posted is broken in Realm v2.7.0.
If you have an object that has a List property, the objects in the List are not deleted using the extension methods.

Does anyone have a solution to this, or another approach on how to solve it, without having to do it the manual way, with is hard to maintain and error prone?

Some dirty hacks, use with care.

import Realm

let realm = try! Realm()

realm.write {        
 deleteObjectAndChilds(realmObject)
}
func deleteObjectAndChilds(_ entity: Any) {
    let realm = try! Realm()
    let mirror = Mirror(reflecting: entity)

    for property in mirror.children {
        if let entity = property.value as? Object {
            deleteObjectAndChilds(entity)
        }
        else if let list = property.value as? RealmSwift.ListBase {
            while list.count > 0 {
                let item = list._rlmArray.firstObject()
                deleteObjectAndChilds(item as Any)
            }
        }
    }

    // delete object

    if let realmEntity = entity as? Object {
        if realmEntity.isInvalidated == false {
            realm.delete(realmEntity)
        }
    }
}

Inspired by @demensdeum's solution I came up with something similar but without the use of Mirror. My solution deletes even referenced optional Objects. Feel free to give it a try and to provide feedback:
https://gist.github.com/danurna/2207f1c480e2cee85fcd3491cd9d3707

As far as I can tell, both of these approaches will result in an endless loop when there's a circular relationship. I'm not proficient in swift, but here's some pseudocode that should work around this:

func cascadeDelete(_ entity: RLMObjectBase) {
    let toDelete = Set<RLMObjectBase>()
    toDelete.add(entity)
    while(!toDelete.isEmpty) {
        let current = toDelete.remove(0)
        if (!current.invalidated) {
            foreach (let property in current.objectSchema.properties) {
                if let value = current.value(forKey: property.name) {
                    if let related = value as? RLMObjectBase {
                        toDelete.add(related)
                    } else if let list = value as? RealmSwift.ListBase {
                        foreach (let related in list) {
                            toDelete.add(related)
                        }
                    }
                }
            }

            self.delete(entity)
        }
    }
}

The only change here is that nodes are removed as soon as they are encountered rather than after their related objects have been removed. I've used a Set<> under the assumption that RLMObjectBase is hashable, but if that's not the case, any collection will do.

Firstly, thanks everyone for all the effort and code snippets 馃憦
Just to update on @nirinchev with working Swift 4 example, wrapped in protocols and with lower cyclomatic complexity. So far I found this as the most reliable workaround, but of course, it doesn't solve the problem. Hope someone will find this useful 馃檪

https://gist.github.com/krodak/b47ea81b3ae25ca2f10c27476bed450c

I cannot wait to see it exposed through the API, keep us updated.

FYI for others following this issue - it's working its way up: https://github.com/realm/realm-object-store/pull/622

Any update on cascading delete feature?

I've heard that once Realm Core 6 is in, then cascade deletion will be added afterwards too

Outside of Core6 upgrade the biggest blocker for cascading deletes is to make support for that with sync. That's a bit tricky, and we currently don't have a committed timeframe for that, unfortunately. But this issue will surely be updated ones we have more info.

Any update on this issue?

Or it could be supported for non-sync Realms only until Sync supports it :smile:

Almost 4 years since the opening of this issue. For a feature that comes built-in with CoreData. It's kind of disappointing that Realm isn't making this a priority. Slowly moving away from Realm because of this.

Almost 4 years since the opening of this issue. For a feature that comes built-in with CoreData. It's kind of disappointing that Realm isn't making this a priority. Slowly moving away from Realm because of this.

Me too.

Sybase has cascading delete triggers 30 years ago. Just sayin'.

Is there any news on the implementation of this feature?

Also very disappointed that this hasn't been implemented so far. I released my app and have had several complaints already about db being inaccessible (corrupted) so the app has to be removed and reinstalled. I try to ensure consistency by doing everything manually (cascade deletes, foreign keys, semantic uniques, migration from local to synced realm) but this is (evidently) far from ideal. This is basic functionality that the database should provide out of the box. Realm is very fragile and gets in an inconsistent state easily, resulting in unusable apps. Please improve this.

bump

Implemented Realm in my whole app, didn't know we would be encountering this issue at earliest. Creating hell lot of problem to manage the objects. How soon we will have this feature?

@keshavkishore09, we are also waiting for the feature, but in the meantime there are enough solutions that can solve your problem until they release the cascade deletion. Check the answers above. To sum it up: what we did was extend the realm models with a new method that returns the properties/objects that should be cascade-deleted. Then for each such property/object we do the same until all is deleted.

The PR for this feature is open since June 2018. This issue is being discussed since 2014. Make of that what you will, but at this point you should think about your in-house solution like many other people in this thread did.

I'm not assuming this will help anyone, but I can at least spread a bit of light over our thinking. I can confirm that we are going to do this. It is "just" a matter of time and resources. So far we have not prioritized it high as there are workarounds, and we have focused on other bugs and features without workarounds. That's of course not useful for anyone wanting this feature.
But this is one of the top features we want to add once we have rolled out Core6, which we are now actively working on again. As for timing, my best guess (as plans looks right now) is that it won't make it this year, but early next year.
We appreciate all your patience with this and understand the impatience...

Hello!
Can we have some update please?
While we wait for official support, can any of the contributors points towards a suggested workaround?
For example is this solution correct https://gist.github.com/verebes1/02950e46fff91456f2ad359b3f3ec3d9 ?

I can give a quick update. As you may have seen we have released a beta with Core6 and Frozen Objects (do try it out!) and expect the GA version in April. We are currently implementing " Embedded Objects" that will give you a similar functionality if it fits your data models. But it won't help you much for highly linked data models where you can't just embed one model in another. That is still something we will look into prioritizing this year.

Was this page helpful?
0 / 5 - 0 ratings