realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.27k stars 2.14k forks source link

How can we ensure the writing process is finish when we doing it in background thread #6961

Closed gogovan-anthonychan closed 3 years ago

gogovan-anthonychan commented 3 years ago

Goals

We are going to move the writing process to the background thread and all other threads can get the updated detached result.

Expected Results

  1. Save realm object in a specific background thread
  2. Delete that realm object in the same background thread
  3. When querying the object, it returns nil
    1. Actual Results

      When querying that object, I still can get the object.

Steps for others to Reproduce

Run the unit test

Code Sample

class Database: DatabaseProtocolForConfig {
    private let queue = DispatchQueue(label: "DATABASE_QUEUE", autoreleaseFrequency: .workItem)

    func saveObject<T: Object>(config: Realm.Configuration, model: T, isTest: Bool) {
        queue.async {
            do {
                let realm = try Realm(configuration: config)
                realm.beginWrite()
                realm.add(model, update: .modified)
                try realm.commitWrite()
            } catch {
            }
        }
    }

    func deleteObject<T: Object>(config: Realm.Configuration, model: T, isTest: Bool) {
        queue.async {        
            do {
                let realm = try Realm(configuration: config)
                if let key = type(of: model).primaryKey(),
                   let value = model.value(forKey: key),
                   let obj = realm.object(ofType: type(of: model), forPrimaryKey: value),
                   !obj.isInvalidated {
                    realm.beginWrite()
                    realm.delete(obj)
                    try realm.commitWrite()
                }
            } catch {
            }
        }
    }

    func queryObject<T: Object>(config: Realm.Configuration, type: T.Type, filterQuery: String?, isTest: Bool) -> T? {
        do {
            let realm = try Realm(configuration: config)
            var objects = realm.objects(T.self)
            if let filterQuery = filterQuery {
                objects = objects.filter(filterQuery)
            }
            let object = objects.first?.detached()
            return object
        } catch {
            return nil
        }
    }

Detaching helper

protocol DetachableObject: AnyObject {
    func detached() -> Self

}

extension Object: DetachableObject {
    func detached() -> Self {
        let detached = type(of: self).init()
        for property in objectSchema.properties {
            guard let value = value(forKey: property.name) else { continue }
            if let detachable = value as? DetachableObject {
                detached.setValue(detachable.detached(), forKey: property.name)
            } else if let list = value as? DetachableObject {
                detached.setValue(list.detached(), forKey: property.name)
            } else {
                detached.setValue(value, forKey: property.name)
            }
        }
        return detached
    }
}

extension Sequence where Iterator.Element: Object {

    public var detached: [Element] {
        return self.map({ $0.detached() })
    }
}

extension List: DetachableObject where Element: Object {
    func detached() -> List<Element> {
        let detached = self.detached
        let result = List<Element>()
        result.append(objectsIn: detached)
        return result
    }
}

The failed unit test

class DatabaseTest: XCTestCase {
    func test_save_delete_query() {
        let id = "dog 1"
        let dog1 = getDog(with: id)
        guard let config = config else {
            return
        }
        database.saveObject(config: config, model: dog1, isTest: true)

        guard let detachedDog1 = database.queryObject(config: config, type: Dog.self, filterQuery: "id = 'dog 1'") else {
            XCTFail()
            return
        }

        database.deleteObject(config: config, model: detachedDog1, isTest: true)

        let object = database.queryObject(config: config, type: Dog.self, filterQuery: "id = '\(id)'", isTest: true)
        XCTAssertNil(object)
    }
}

model

class Dog: Object {
    override class func primaryKey() -> String? {
        return "id"
    }
    @objc dynamic var id = ""
    @objc dynamic var name = ""
    @objc dynamic var age = 0
}
func getDog(with uniqueId: String) -> Dog {
    let id = uniqueId
    let name = "popo"
    let age = 11

    let mockDog = Dog()
    mockDog.id = id
    mockDog.name = name
    mockDog.age = age
    return mockDog
}

Version of Realm and Tooling

Realm framework version: v5.4.3

Realm Object Server version: ---

Xcode version: Xcode 12.1

iOS/OSX version: 10.15.7

Dependency manager + version: carthage 0.36.0

leemaguire commented 3 years ago

@gogovan-anthonychan You need to observe the Realm collection for changes, as the test will continue without waiting for the write to finish. Once the change has been observed you can let your test continue. One way to implement this 'traffic light' system is to use a semaphore

Also see observing a collection a Realm: https://realm.io/docs/swift/latest/#collection-notifications

gogovan-anthonychan commented 3 years ago

@leemaguire We have thought about this approach. Sometimes, we would like to make it synchronous. Save an object and then query it from other place. How can we ensure that the saving process finish other than realm notification?

leemaguire commented 3 years ago

To make it synchronous you will need to run everything off the same queue, otherwise the use of a semaphore is needed to ensure all other tasks have completed before continuing to the next.

gogovan-anthonychan commented 3 years ago

@leemaguire if delete and save are running in the same queue, and the order is save -> delete. Does it finish save process before delete every time?

leemaguire commented 3 years ago

Yes as long as you are using a serial queue.

gogovan-anthonychan commented 3 years ago

@leemaguire I think you have solved my problem so I close this issue. Thank you.