Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data of type NSInMemoryStoreType ignores entity's constraints

I created mocked version of CoreData stack

import Foundation
import CoreData

@testable import Companion

final class MockedDatabaseStackController: DatabaseStackControllerProtocol {

    let batchRequestsAvailable: Bool = false

    private lazy var managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: type(of: self))])!

    lazy var persistentContainer: NSPersistentContainer = {
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        description.shouldAddStoreAsynchronously = false

        let container = NSPersistentContainer(
            name: "database",
            managedObjectModel: managedObjectModel
        )
        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores { description, error in
            // Check if the data store is in memory
            precondition( description.type == NSInMemoryStoreType )

            // Check if creating container wrong
            if let error = error {
                fatalError("Create an in-mem coordinator failed \(error)")
            }
        }

        return container
    }()

    init() {
        NotificationCenter.default
            .addObserver(
                self,
                selector: #selector(didManagedObjectContextSave(notification:)),
                name: .NSManagedObjectContextDidSave,
                object: nil
            )
    }

    @objc
    private func didManagedObjectContextSave(notification: Notification) {
        DispatchQueue.main.async { [weak self] in
            self?.persistentContainer.viewContext.mergeChanges(fromContextDidSave: notification)
        }
    }
}

and I am using it to save some object:

private func executeAndSave<T>(_ executionBlock: @escaping ((NSManagedObjectContext) throws -> T)) -> Single<T> {
    let persistentContainer = stackController.persistentContainer

    return Single.create { observer in
        persistentContainer.performBackgroundTask { context in
            do {
                context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

                jsons.forEach {
                    let mo = type.init(context: context)
                    mo.update(withGatewayResponse: $0)
                }
                try context.save()

                DispatchQueue.main.async {
                    observer(.success(result))
                }
            } catch {
                DispatchQueue.main.async {
                    observer(.error(error))
                }
            }
        }

        return Disposables.create()
    }
}

func save(jsons: [JSON], as type: GatewayObjectDeserializableAndSavable.Type) -> Single<Void> {
    if jsons.isEmpty {
        log.info("(\(type)) Nothing to save.")
        return .just(())
    }

    log.info("DatabaseHelper will save \(type)")

    return executeAndSave { context in
        jsons.forEach {
            let mo = type.init(context: context)
            mo.update(withGatewayResponse: $0)
        }
    }
}

// Example of usage:
databaseHelper.save(jsons: jsons, as: Herd.self)

I created constraints in database model, for example: enter image description here

But it doesn't work. Objects duplicate in database. enter image description here

Note that everything works fine in main target where I use this CoreData's stack:

final class DatabaseStackController: DatabaseStackControllerProtocol {

    // singleton
    static let controller = DatabaseStackController()

    private static let kDatabaseName = "database"
    let persistentContainer: NSPersistentContainer = DatabaseStackController.buildDatabaseStack(onComplete: nil)

    let batchRequestsAvailable: Bool = true

    private init() {
        addNSMangedObjectContextObservers()
    }

    private static func buildDatabaseStack(onComplete: (() -> Void)?) -> NSPersistentContainer {
        let container = NSPersistentContainer(name: kDatabaseName)
        container.loadPersistentStores { _, error in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }

            onComplete?()
        }

        return container
    }
}

Why it doesn't work? Is NSInMemoryStoreType not support CoreData's constraints? Is it possible to fix it?

like image 292
Kamil Harasimowicz Avatar asked May 10 '19 12:05

Kamil Harasimowicz


1 Answers

I think you've found a bug in Core Data :(

I have a little demo project for Core Data's Unique Constraint. I confirmed that it works as expected except for some exceptions, always merging all duplicates, with the SQLite store. I then pasted in your MockedDatabaseStackController class and used its persistentContainer.viewContext with NSMergeByPropertyObjectTrumpMergePolicy set. Result: It seems to merge the first set of duplicates on the first save operation but none after that. I then switched back to my Core Data stack, except I changed the store type to NSInMemoryStoreType. Result: It also fails to work, the same as your MockedDatabaseStackController.

The SQLite database which underlies Core Data's SQLite store supports the UNIQUE constraint in its SQL. I hope someone can prove me wrong, but, sadly, I suspect that Apple used this feature of SQLite to implement the Unique Constraint feature in Core Data, but failed to add to their documentation the fact that it only works for the SQLite store.

I've submitted this to Apple Bug Reporter: 50725935.

As to your test, I think that you should modify it to create a temporary SQLite store. There are in fact some features which are on the other side of the Venn diagram – supported by the in-memory store but not supported by the SQLite store. Testing with an in-memory store could leave holes in your test coverage.

like image 143
Jerry Krinock Avatar answered Oct 06 '22 05:10

Jerry Krinock