Skip to content

Instantly share code, notes, and snippets.

@krodak
Last active April 27, 2023 19:16
Show Gist options
  • Save krodak/b47ea81b3ae25ca2f10c27476bed450c to your computer and use it in GitHub Desktop.
Save krodak/b47ea81b3ae25ca2f10c27476bed450c to your computer and use it in GitHub Desktop.
Cascade deletion for RealmSwift
import RealmSwift
import Realm
protocol CascadeDeleting: class {
func delete<Entity>(_ list: List<Entity>, cascading: Bool)
func delete<Entity>(_ results: Results<Entity>, cascading: Bool)
func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}
extension Realm: CascadeDeleting {
func delete<Entity>(_ list: List<Entity>, cascading: Bool) {
list.forEach {
delete($0, cascading: cascading)
}
}
func delete<Entity>(_ results: Results<Entity>, cascading: Bool) {
results.forEach {
delete($0, cascading: cascading)
}
}
func delete<Entity: Object>(_ entity: Entity, cascading: Bool) {
if cascading {
cascadeDelete(entity)
} else {
delete(entity)
}
}
}
private extension Realm {
private func cascadeDelete(_ entity: RLMObjectBase) {
guard let entity = entity as? Object else { return }
var toBeDeleted = Set<RLMObjectBase>()
toBeDeleted.insert(entity)
while !toBeDeleted.isEmpty {
guard let element = toBeDeleted.removeFirst() as? Object,
!element.isInvalidated else { continue }
resolve(element: element, toBeDeleted: &toBeDeleted)
}
}
private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
element.objectSchema.properties.forEach {
guard let value = element.value(forKey: $0.name) else { return }
if let entity = value as? RLMObjectBase {
toBeDeleted.insert(entity)
} else if let list = value as? RealmSwift.ListBase {
for index in 0..<list._rlmArray.count {
toBeDeleted.insert(list._rlmArray.object(at: index))
}
}
}
delete(element)
}
}
@devpeds
Copy link

devpeds commented Dec 15, 2018

Thank you all for providing the solution and I have just refactored the @Bogsey's resolve method to reduce the number of indentations.

private extension Realm {
    private func cascadingDelete(_ object: Object) {
        var toBeDeleted = Set<RLMObjectBase>()
        toBeDeleted.insert(object)
        while !toBeDeleted.isEmpty {
            guard let element = toBeDeleted.removeFirst() as? Object, !element.isInvalidated else { continue }
            resolve(element, toBeDeleted: &toBeDeleted)
            delete(element)
        }
    }
    
    private func resolve(_ element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
        guard let deletable = element as? CascadeDeletable else { return }
        let propertiesToDelete = element.objectSchema.properties.filter {
            deletable.propertiesToCascadeDelete.contains($0.name)
        }
        propertiesToDelete.forEach {
            guard let value = element.value(forKey: $0.name) else { return }
            if let object = value as? Object {
                toBeDeleted.insert(object)
            } else if let list = value as? RealmSwift.ListBase {
                for index in 0..<list._rlmArray.count {
                    guard let object = list._rlmArray.object(at: index) as? Object else { continue }
                    toBeDeleted.insert(object)
                }
            }
        }
    }
}

@Bogsey
Copy link

Bogsey commented Mar 20, 2019

I'm returning to this as I now have a need to be able to include LinkingObjects in the cascade. Any idea how I would do that, LinkingObjectsBase class doesn't seem to expose any of the results to delete.

@Bogsey
Copy link

Bogsey commented Mar 21, 2019

Have updated the function with the refactoring suggested by @devpeds to include the cascading of linkingObjects too. Need to specify the type in the protocol to allow them to be cast to the correct type of LinkingObjects

private extension Realm {
    
    private func cascadingDelete(_ object: Object) {
        var toBeDeleted = Set<RLMObjectBase>()
        toBeDeleted.insert(object)
        while !toBeDeleted.isEmpty {
            guard let element = toBeDeleted.removeFirst() as? Object, !element.isInvalidated else { continue }
            resolve(element: element, toBeDeleted: &toBeDeleted)
            delete(element)
        }
    }
    
    private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
        guard let deletable = element as? CascadeDeletable else { return }
        let computedProperties = (type(of: element).sharedSchema()?.computedProperties ?? []).map { $0.name }
        let propertiesToDelete = ((element.objectSchema.properties.map { $0.name }) + computedProperties).filter { (propertyName) -> Bool in
            deletable.propertiesToCascadeDelete.keys.contains(where: { (name) -> Bool in
                name == propertyName
            })
        }
        propertiesToDelete.forEach {
            guard let value = element.value(forKey: $0) else { return }
            if let entity = value as? RLMObjectBase {
                toBeDeleted.insert(entity)
            } else if let list = value as? RealmSwift.ListBase {
                for index in 0..<list._rlmArray.count {
                    guard let realmObject = list._rlmArray.object(at: index) as? RLMObjectBase else { continue }
                    toBeDeleted.insert(realmObject)
                }
            } else if let linkingObjects = value as? LinkingObjectsBase {
                guard let type = deletable.propertiesToCascadeDelete[$0] else { return }
                guard let unrwappedType = type else { fatalError("Object type not specified for cascade delete of linking object") }
                guard let objects = convertLinkingBase(linkingObjects, to: unrwappedType) else { return }
                for index in 0..<objects.count {
                    toBeDeleted.insert(objects[index])
                }
            }
        }
    }
    
    private func convertLinkingBase<Element: Object>(_ linkingObjects: LinkingObjectsBase, to type: Element.Type) -> LinkingObjects<Element>? {
        let p = unsafeBitCast(linkingObjects, to: Optional<LinkingObjects<Element>>.self)
        return p
    }
}

protocol CascadeDeletable: class {
    var propertiesToCascadeDelete: [String: Object.Type?] { get }
}

@dharmendra-ios
Copy link

Can you give an example on how to use this in objective c?

@sssbohdan
Copy link

In new Realm version just remove : class after protocol declaration (Realm object is struct now).

@varyP
Copy link

varyP commented May 15, 2020

LinkingObjectsBase not found in Realm 4.x.x

@samrayner
Copy link

samrayner commented Jun 6, 2020

Fixed for Realm 4 and removed the need to specify types for the properties as long as all objects have a primaryKey().
https://gist.github.com/samrayner/44d34fdc77e7d8d766fd467d557753a9

@tontonbibi
Copy link

Hello,
Since Realm 10.8.0, RealmSwift.ListBase or RLMListBase are not reachable anymore. Does anyone have an idea how to replace them ?
Thank you very much for your help.

@iSevenDays
Copy link

iSevenDays commented Jun 24, 2021

I think the following code should be used, but I am yet to see it in production

internal protocol CascadingDeletable: RealmSwift.Object {
	static var propertiesToCascadeDelete: [String] { get }
}

extension Realm {
	internal func cascadingDelete(_ object: RealmSwift.Object) {
		var toBeDeleted: Set<RLMObjectBase> = [object]
		while let element = toBeDeleted.popFirst() as? RealmSwift.Object {
			guard !element.isInvalidated else { continue }
			if let cascadingDeletable = element as? CascadingDeletable {
				cascade(into: cascadingDeletable, toBeDeleted: &toBeDeleted)
			}
			delete(element)
		}
	}

	private func cascade(into object: CascadingDeletable, toBeDeleted: inout Set<RLMObjectBase>) {
		let objectType = type(of: object)

		guard let schema = objectType.sharedSchema() else { return }

		let primaryKey = objectType.primaryKey()
		let primaryKeyValue = primaryKey.flatMap(object.value(forKey:))

		let properties = (schema.properties + schema.computedProperties)
			.filter { objectType.propertiesToCascadeDelete.contains($0.name) }

		for property in properties {
			switch object.value(forKey: property.name) {
			case let realmObject as RLMObjectBase:
				toBeDeleted.insert(realmObject)
			case let list as RLMSwiftCollectionBase:
				for index in 0 ..< list._rlmCollection.count {
					guard let realmObject = list._rlmCollection.object(at: index) as? RLMObjectBase else { continue }
					toBeDeleted.insert(realmObject)
				}
			default: // LinkingObjects
				if let linkOriginPropertyName = property.linkOriginPropertyName,
				   let linkOriginTypeName = property.objectClassName,
				   let primaryKey = primaryKey,
				   let primaryKeyValue = primaryKeyValue {
					dynamicObjects(linkOriginTypeName)
						.filter("%K == %@", "\(linkOriginPropertyName).\(primaryKey)", primaryKeyValue)
						.forEach { toBeDeleted.insert($0) }
				}
			}
		}
	}
}

@cpd
Copy link

cpd commented Aug 5, 2021

Updated code to use RLMSwiftCollectionBase instead of RealmSwift.ListBase which looks fine at first glance but using this hack with latest release seems pretty dangerous, since Realm 10.0 theres a built in cascade delete https://www.mongodb.com/developer/article/realm-database-cascading-deletes/#cascading-deletes probably better to stick with it although I didn't tried that yet.

extension Realm {
    func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object {
        for obj in objects {
            delete(obj, cascading: cascading)
        }
    }
    
    func delete<Entity: Object>(_ entity: Entity, cascading: Bool) {
        if cascading {
            cascadeDelete(entity)
        } else {
            delete(entity)
        }
    }
}

private extension Realm {
    private func cascadeDelete(_ entity: RLMObjectBase) {
        guard let entity = entity as? Object else { return }
        
        var toBeDeleted = Set<RLMObjectBase>()
        
        toBeDeleted.insert(entity)
        
        while !toBeDeleted.isEmpty {
            guard let element = toBeDeleted.removeFirst() as? Object, !element.isInvalidated else { continue }
            
            resolve(element: element, toBeDeleted: &toBeDeleted)
        }
    }
    
    private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
        element.objectSchema.properties.forEach {
            guard let value = element.value(forKey: $0.name) else { return }
            
            if let entity = value as? RLMObjectBase {
                toBeDeleted.insert(entity)
            } else if let list = value as? RLMSwiftCollectionBase {
                for index in 0..<list._rlmCollection.count {
                    if let object = list._rlmCollection.object(at: index) as? RLMObjectBase {
                        toBeDeleted.insert(object)
                    }
                }
            }
        }
        
        delete(element)
    }
}

@iSevenDays
Copy link

@cpd, thanks for letting us know! I didn't know the cascading delete was implemented. Thanks!

@lif-wtag
Copy link

@cpd, Thanks a lot mate!
I was having trouble with Resolve function as RealmSwift.ListBase is no longer supported in RealmSwift v10.17.0.
Your code with RLMSwiftCollectionBase helped me, Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment