Search code examples
swiftgrdb

How do I call a new database connection from within a read-only database connection in GRDB?


The function below returns a Ledgers record. Most of the time, it will find it in the optional _currentReceipt variable, or by searching the database, No writing needed there. I'd like to use a read-only GRDB database connection. Read-only database connections can run in parallel on different threads.

In the rare case, the first two steps fail, I can create a default Ledger. Calling try FoodyDataStack.thisDataStack.dbPool.write { writeDB in ... will throw a fatal error, Database connections are not reentrant. I'm looking for a way to save that default Ledger without having to wrap the whole function in a read-write connection.

Can I call an NSOperation on a separate queue from within a GRDB .read block?

class func getCurrentReceipt(db: Database) throws -> Ledgers {
        if let cr = FoodyDataStack.thisDataStack._currentReceipt {
            return cr
        }
        // Fall through
        do {
            if let cr = try Ledgers.filter(Ledgers.Columns.receiptClosed == ReceiptStatus.receiptOpen.rawValue).order(Ledgers.Columns.dateModified.desc).fetchOne(db) {
                FoodyDataStack.thisDataStack._currentReceipt = cr
                return cr
            } else {
                throw FoodyDataStack.myGRDBerrors.couldNotFindCurrentReceipt
            }
        } catch FoodyDataStack.myGRDBerrors.couldNotFindCurrentReceipt {
            // Create new receipt with default store
            let newReceipt = Ledgers()
            newReceipt.dateCreated = Date()
            newReceipt.dateModified = Date()
            newReceipt.receiptStatus = .receiptOpen
            newReceipt.receiptUrgency = .immediate
            newReceipt.dateLedger = Date()
            newReceipt.uuidStore = Stores.defaultStore(db).uuidKey
            FoodyDataStack.thisDataStack._currentReceipt = newReceipt
            return newReceipt
        } catch  {
            NSLog("WARNING: Unhandled error in Ledgers.getCurrentReceipt() \(error.localizedDescription)")
        }
    }

Edit: I'm leaving this question here, but I think I may be going for premature optimization. I'm going to try dbQueue instead of dbPool and see what the performance is. I'll be back to dbPool if speed requires it.


Solution

  • GRDB database access methods are not reentrant (The read and write methods of DatabaseQueue and DatabasePool).

    To help you solve your issue, try to split your database access methods in two levels.

    The first level is not exposed to the rest of the application. Its methods all take a db: Database argument.

    class MyStack {
        private var dbQueue: DatabaseQueue
    
        private func fetchFoo(_ db: Database, id: Int64) throws -> Foo? {
            return try Foo.fetchOne(db, key: id)
        }
    
        private func setBar(_ db: Database, foo: Foo) throws {
            try foo.updateChanges(db) {
                $0.bar = true
            }
        }
    }
    

    Methods from the second level are exposed to the rest of the application. They wrap first-level methods in read and write database access methods:

    class MyStack {
        func fetchFoo(id: Int64) throws -> Foo? {
            return try dbQueue.read { db in
                try fetchFoo(db, id: id)
            }
        }
    
        func setBar(id: Int64) throws {
            try dbQueue.write { db in
                guard let foo = try fetchFoo(db) else {
                    throw fooNotFound
                }
                try setBar(foo: foo)
            }
        }
    }
    

    Methods of the first level can be as low-level as needed, and can be composed.

    Methods of the second level are high level, and are not composable: they can't call each other because of the "Database connections are not reentrant" fatal error. They provide the thread-safe database methods that guarantee database consistency.

    See the Concurrency Guide for more information.