Transaction Changes Not Merged or Triggering Publishers #441

Open
opened 2025-12-29 18:26:59 +01:00 by adam · 5 comments
Owner

Originally created by @Devenias on GitHub (Jan 30, 2025).

I have an issue with transactions and publishers.
When I update existing models in a .perform block with it's transaction context transaction.fetchOne the data is not correct after execution, still has the old data and does not trigger the publisher. So my UI never noticed a change, because it did not happen.

When I use DatabaseStack.instance.dataStack.fetchOne instead, I can edit the object and it also triggers the data update on the publisher. I'm I doing something wrong or is that a bug, that the transaction context is not fully merged into the main context?

I'm using CoreStore 9.2.0 because I have to support iOS 15.

I get some new data and update in an async transaction:

DatabaseStack.instance.dataStack.perform { transaction in
    for data in newData {
        let fetch = From<MyModel>().where(\.id == data.id)
        
        // this does NOT trigger updates in the publisher
        let dbModel_1: MyModel = if
            let existing = try? transaction.fetchOne(fetch),
            let editable = transaction.edit(existing)
        {
            editable
        } else {
            transaction.create(Into(MyModel.self))
        }

        dbModel_1.updateWith(entity: data)

        // this does trigger updates in the publisher
        let dbModel_2: MyModel = if
            let existing = try? DatabaseStack.instance.dataStack.fetchOne(fetch)
        {
            existing
        } else {
            transaction.create(Into(MyModel.self))
        }
        
        dbModel_2.updateWith(entity: data)
    }
} completion: { result in
    ...
}

and I observe it like that:

let query = From<MyModel>().orderBy(.ascending(\.id))
return DatabaseStack.instance.dataStack
    .publishList(query)
    .reactive
    .snapshot(emitInitialValue: true)
    .tryMap { snapshot in
        Mapper.toEntities(snapshot)
    }
    .eraseToAnyPublisher()
Originally created by @Devenias on GitHub (Jan 30, 2025). I have an issue with transactions and publishers. When I update existing models in a `.perform` block with it's transaction context `transaction.fetchOne` the data is not correct after execution, still has the old data and does not trigger the publisher. So my UI never noticed a change, because it did not happen. When I use `DatabaseStack.instance.dataStack.fetchOne` instead, I can edit the object and it also triggers the data update on the publisher. I'm I doing something wrong or is that a bug, that the transaction context is not fully merged into the main context? I'm using CoreStore 9.2.0 because I have to support iOS 15. I get some new data and update in an async transaction: ```swift DatabaseStack.instance.dataStack.perform { transaction in for data in newData { let fetch = From<MyModel>().where(\.id == data.id) // this does NOT trigger updates in the publisher let dbModel_1: MyModel = if let existing = try? transaction.fetchOne(fetch), let editable = transaction.edit(existing) { editable } else { transaction.create(Into(MyModel.self)) } dbModel_1.updateWith(entity: data) // this does trigger updates in the publisher let dbModel_2: MyModel = if let existing = try? DatabaseStack.instance.dataStack.fetchOne(fetch) { existing } else { transaction.create(Into(MyModel.self)) } dbModel_2.updateWith(entity: data) } } completion: { result in ... } ``` and I observe it like that: ```swift let query = From<MyModel>().orderBy(.ascending(\.id)) return DatabaseStack.instance.dataStack .publishList(query) .reactive .snapshot(emitInitialValue: true) .tryMap { snapshot in Mapper.toEntities(snapshot) } .eraseToAnyPublisher() ```
Author
Owner

@JohnEstropia commented on GitHub (Feb 3, 2025):

dataStack.perform does not wait for completion of dataStack.addStorage() and it's likely you are fetching earlier than the storage has been added to the DataStack

@JohnEstropia commented on GitHub (Feb 3, 2025): `dataStack.perform` does not wait for completion of `dataStack.addStorage()` and it's likely you are fetching earlier than the storage has been added to the `DataStack`
Author
Owner

@Devenias commented on GitHub (Feb 3, 2025):

Thanks for the response, but I use a singleton to make sure the DataStack is initialized properly and use .addStorageAndWait() in the apps func application(didFinishLaunchingWithOptions: ).

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    print("Database version: \(DatabaseStack.instance.dataStack.modelVersion)")
    // ...
}

class DatabaseStack {
    static let instance = DatabaseStack()

    let dataStack = DataStack(xcodeModelName: "DataModel")

    private init() { initialize() }

    private func initialize() {
        do {
            try dataStack.addStorageAndWait(
                SQLiteStore(
                    fileName: "MyDatabase.sqlite",
                    localStorageOptions: [
                        .allowSynchronousLightweightMigration,
                        .recreateStoreOnModelMismatch
                    ]
                )
            )
        } catch let error {
            fatalError("Could not initialize database: \(error.localizedDescription)")
        }
        CoreStoreDefaults.dataStack = dataStack
        CoreStoreDefaults.logger = DatabaseLogger()
    }
}
@Devenias commented on GitHub (Feb 3, 2025): Thanks for the response, but I use a singleton to make sure the `DataStack` is initialized properly and use `.addStorageAndWait()` in the apps `func application(didFinishLaunchingWithOptions: )`. ```swift func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { print("Database version: \(DatabaseStack.instance.dataStack.modelVersion)") // ... } class DatabaseStack { static let instance = DatabaseStack() let dataStack = DataStack(xcodeModelName: "DataModel") private init() { initialize() } private func initialize() { do { try dataStack.addStorageAndWait( SQLiteStore( fileName: "MyDatabase.sqlite", localStorageOptions: [ .allowSynchronousLightweightMigration, .recreateStoreOnModelMismatch ] ) ) } catch let error { fatalError("Could not initialize database: \(error.localizedDescription)") } CoreStoreDefaults.dataStack = dataStack CoreStoreDefaults.logger = DatabaseLogger() } } ```
Author
Owner

@JohnEstropia commented on GitHub (Feb 4, 2025):

Are you running parallel updates on the same entities? I'd also check the following:

  • If the perform is not emitting errors
  • If the updates are actually in store (succeeding fetches reflect new values)
@JohnEstropia commented on GitHub (Feb 4, 2025): Are you running parallel updates on the same entities? I'd also check the following: - If the `perform` is not emitting errors - If the updates are actually in store (succeeding fetches reflect new values)
Author
Owner

@Devenias commented on GitHub (Feb 6, 2025):

The perform is not emitting any errors, I get a .success in the completion.
And when I use .fetchOne in the completion, the values are not updated in the result.

And yes, the updates might run in parallel. I started a new test project to isolate the issue and I can reproduce it. Then I tried to fix it using .perform(synchronous:) and a serial DispatchQueue.

serialQueue.async {
    do {
        try DatabaseStack.instance.dataStack.perform(synchronous: { transaction in
            // ...
        })
  
        // in my existing project I tried to fetch immediately after the perform(synchronous:), but the data has not changed. 
    } catch {
        // ...
    }
}

It works in my test project and the updates are synchronised, the data is changed and the publisher triggered.
But when I want to apply the same approach to my existing project it does not work anymore (same behaviour as before). I have to figure out what the differences are.
Do you have any other idea how I can prevent parallel updates or synchronise them properly?

@Devenias commented on GitHub (Feb 6, 2025): The `perform` is not emitting any errors, I get a `.success` in the `completion`. And when I use `.fetchOne` in the `completion`, the values are not updated in the result. And yes, the updates might run in parallel. I started a new test project to isolate the issue and I can reproduce it. Then I tried to fix it using `.perform(synchronous:)` and a serial `DispatchQueue`. ```swift serialQueue.async { do { try DatabaseStack.instance.dataStack.perform(synchronous: { transaction in // ... }) // in my existing project I tried to fetch immediately after the perform(synchronous:), but the data has not changed. } catch { // ... } } ``` It works in my test project and the updates are synchronised, the data is changed and the publisher triggered. But when I want to apply the same approach to my existing project it does not work anymore (same behaviour as before). I have to figure out what the differences are. Do you have any other idea how I can prevent parallel updates or synchronise them properly?
Author
Owner

@JohnEstropia commented on GitHub (Feb 10, 2025):

@Devenias Can you try these?

  • remove the optional try? here and just call try normally?
try? transaction.fetchOne(fetch)
  • check if updateWith(entity:) method sets MyModel. id
@JohnEstropia commented on GitHub (Feb 10, 2025): @Devenias Can you try these? - remove the optional `try?` here and just call `try` normally? ```swift try? transaction.fetchOne(fetch) ``` - check if `updateWith(entity:)` method sets `MyModel. id`
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore-JohnEstropia#441