Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction Changes Not Merged or Triggering Publishers #514

Open
Devenias opened this issue Jan 30, 2025 · 5 comments
Open

Transaction Changes Not Merged or Triggering Publishers #514

Devenias opened this issue Jan 30, 2025 · 5 comments

Comments

@Devenias
Copy link

Devenias commented 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()
@JohnEstropia
Copy link
Owner

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

@Devenias
Copy link
Author

Devenias commented 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()
    }
}

@JohnEstropia
Copy link
Owner

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)

@Devenias
Copy link
Author

Devenias commented 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?

@JohnEstropia
Copy link
Owner

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants