-
-
Save krodak/b47ea81b3ae25ca2f10c27476bed450c to your computer and use it in GitHub Desktop.
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) | |
} | |
} |
@anfriis you just use it like normal delete
function from RealmSwift
SDK. You can ignore cascadeDelete()
and resolve
as these are only private implementations that the user (programmer) shouldn't need to be aware of.
The protocol is just for the sake of Protocol Oriented Programming, if you tend to write your tests using mock objects, it's much easier if everything is covered by protocols.
But for the sake of example, assuming that you have a ContactEntity
model:
final class ContactEntity: Object {
@objc dynamic var id: String = ""
let phoneNumbers = List<PhoneNumberEntity>() // this needs to be deleted every time `ContactEntity` is deleted
let profiles = List<SocialProfileEntity>() // this needs to be deleted every time `ContactEntity` is deleted
}
You can fetch and delete its elements similar to standard delete
function:
guard let database = try? Realm() else { return }
// to delete Result<ContactEntity>
let contacts = database.objects(ContactEntity.self)
do {
try database.write {
database.delete(contacts, cascading: true)
}
} catch {
// handle write error here
}
// to delete ContactEntity
guard let contact = Array(database.objects(ContactEntity.self)).first else { return }
do {
try database.write {
database.delete(contact, cascading: true)
}
} catch {
// handle write error here
}
@anfriis let me know if you run into any issues with the example
I'm using it in production.
It works like a charm.
Thanks!
I had a bit of trouble with this in Realm 3 / Swift 4.
Here's a slightly more generic version which works fine and also allows you to delete any kind of Sequence
of Object
s as well.
import RealmSwift
import Realm
protocol CascadeDeleting: class {
func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object
func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}
extension Realm: CascadeDeleting {
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? RealmSwift.ListBase {
for index in 0..<list._rlmArray.count {
toBeDeleted.insert(list._rlmArray.object(at: index) as! RLMObjectBase)
}
}
}
delete(element)
}
}
@goa it works nicely, although I would recommend you to avoid that force unwrap, because if you are storing a List of primitives such as List<String>
it will crash since it is not a RLMObjectBase
for index in 0 ..< list._rlmArray.count {
if let realmObject = list._rlmArray.object(at: index) as? RLMObjectBase {
toBeDeleted.insert(realmObject)
}
}
Hi,
Would this take into consideration the number of references an object has before deleting it in a cascade? For example, if we have something like
class Ingredient: Object {
}
class Pie: Object {
var ingredients: List<Ingredient>?
}
class Bread: Object {
var ingredients: List<Ingredient>?
}
let flour = Ingredient()
let applePie = Pie()
applePie.ingredients.append(flour)
let whiteBread = Bread()
whiteBread.append(flour)
delete(applePie, cascading: true)
If I'm reading the code right, when we delete applePie, the ingredient flour gets deleted as well (even though it is referenced in the list of ingredients for whiteBread). Could you confirm this is the case? Thanks
@georgemp Yes thats true behavior.
So now how can we avoid this?
One way is to check for LinkingObjects when deleting the List
So Ingredients will have a property owner which will be LinkingObjects to Pie and Bread.
We need to check if Ingredients object had more than 1 LinkingObjects before deleting.
I am trying to work on this. If anyone has better solution already please update it.
One quick workaround is to update resolve func as follow:
class Ingredient: Object {
let owners = LinkingObjects(fromType: BaseBakedObject.self, property: "ingredients")
}
for index in 0 ..< list._rlmArray.count {
if let realmObject = list._rlmArray.object(at: index) as? RLMObjectBase {
if let object = realmObject as? Ingredient {
if let owners = object.owners as? LinkingObjects<Object> {
if owners.count > 1 {
continue
}
}
}
toBeDeleted.insert(realmObject)
}
}
Thanks.
thank you all guys for answers! for swift 4 @goa version works perfect, but @robertofrontado is right about primitives
@robertofrontado and how would i delete a list of strings?
To give a more fine grained approach I have made a small change to the solution that @goa suggested.
import RealmSwift
import Realm
protocol CascadeDeleting: class {
func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object
func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}
extension Realm: CascadeDeleting {
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>) {
if let cascadingObject = element as? CascadeDeletable {
element.objectSchema.properties.forEach {
if cascadingObject.propertiesToCascadeDelete.contains($0.name) {
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 {
if let realmObject = list._rlmArray.object(at: index) as? RLMObjectBase {
toBeDeleted.insert(realmObject)
}
}
}
}
}
}
delete(element)
}
}
protocol CascadeDeletable: class {
var propertiesToCascadeDelete: [String] { get }
}
Only objects that conform to the CascadeDeletable protocol will have cascade delete enabled and you can select which properties to cascade delete. Any other properties that are realm objects or collections won't be cascaded and will remain after the deletion.
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)
}
}
}
}
}
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.
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 }
}
Can you give an example on how to use this in objective c?
In new Realm version just remove : class
after protocol declaration (Realm object is struct now).
LinkingObjectsBase
not found in Realm 4.x.x
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
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.
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) }
}
}
}
}
}
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)
}
}
@cpd, thanks for letting us know! I didn't know the cascading delete was implemented. Thanks!
@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.
Can you give an example on how to use this?