macOS multiple document app, multiple CoreData stores

Hello, I'm building a multi document app for macOS, in which a document can be linked with exactly one Sqlite data store. All stores have the same data model (same entities, attributes, and etc) but different data. Each data store is a persistent cache for a remote database. It all works fine with one open document. Once I open a new document, the app crashes with the following error:

2021-12-02 13:37:45.991866-0800 MacOra[22562:632339] [error] error: +[DBObject entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass

The App is set up as follows:

var body: some Scene {
        DocumentGroup(newDocument: { Document() }) { config in
            DocumentView(document: config.document)               

// injecting persistent controller into the document view
                .environment(\.managedObjectContext, config.document.persistentController.container.viewContext)

The Document is a class ViewModel.

class Document: ReferenceFileDocument {
@Published var persistentController: PersistenceController

required init(configuration: ReadConfiguration) throws {

        guard let data = configuration.file.regularFileContents

        else {

            throw CocoaError(.fileReadCorruptFile)

        }

// linking data store
        persistentController = PersistenceController(name: myStoreName)
    }

Finally, here is the PersistenceController. I intentionally don't use a singleton here because I want each document to have its own independent persistence controller linking the document to its data store.

struct PersistenceController {
// don't want to make it a singleton as each document should have it's own store
//    static let shared = PersistenceController()

let container: NSPersistentContainer

    init(inMemory: Bool = false, name: String = "preview") {
        // initializing data model
        guard let modelURL = Bundle.main.url(forResource: "MyModel", withExtension: "momd")  else {
            fatalError("Error loading model from bundle")
        }

        let dataModel = NSManagedObjectModel(contentsOf: modelURL)!
        container = NSPersistentContainer(name: name, managedObjectModel: dataModel)

// <<<<<<< this it where it fails 
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

I kind of understand that CoreData creates a second set of entities when I open a new document, and it gets lost because of that. But I don't understand why as I'm setting up a new instance of everything specifically to avoid this issue.

Is this design not feasible with CoreData at all? How would one go about having an MDI app with multiple stores with the same structure?

I'd like to avoid having one big store with all data for all documents because of performance reasons as each data store can get quite large.

Thanks!

Answered by ilia_saz in 697162022

OK, if anybody ever looks for a similar question (or myself 3 years from now), here is a simple answer. Don't load your model in PersistentController initializer as this is what loads the model multiple times. Just make it static (either a let constant or a singleton) and attach it to NSPersistentContainer. Here is an example/

let modelURL = Bundle.main.url(forResource: "MyModel", withExtension: "momd")!
let dataModel = NSManagedObjectModel(contentsOf: modelURL)!

struct PersistenceController {
    let container: NSPersistentContainer

    init(inMemory: Bool = false, name: String = "preview") {
        container = NSPersistentContainer(name: name, managedObjectModel: dataModel)
        // other stuff
    }
}

Correction: the crash does NOT happen in container.loadPersistentStores call. This call completes successfully. The crash happens when a new document is opened and the document view is being constructed.

After the first document is open:

2021-12-03T11:06:05-0800 debug: in view body, context: <NSManagedObjectContext: **0x60000348dad0>** , class: , hash: **105553171372752** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x6000021a8d00>)** , name: nil , objects: [] 2021-12-03T11:06:05-0800 debug: in view onAppear, context: <NSManagedObjectContext: **0x60000348dad0>** , class: , hash: **105553171372752** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x6000021a8d00>)** , name: nil , objects: []

And then when I open a second document:

2021-12-03T11:08:02-0800 debug: in view body, context: <NSManagedObjectContext: **0x6000034a81a0>** , class: , hash: **105553171480992** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x600002198a80>)** , name: nil , objects: [] CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'DBObject' so +entity is unable to disambiguate.

So the second view definitely gets a new context and a new coordinator, but CoreData for some reason gets confused with the same entity being loaded twice, even though they belong to two different contexts.

Correction: the crash does NOT happen in container.loadPersistentStores call. This call completes successfully. The crash happens when a new document is opened and the document view is being constructed. 

After the first document is open: 

2021-12-03T11:06:05-0800 debug: in view body, context: <NSManagedObjectContext: **0x60000348dad0>** , class: , hash: **105553171372752** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x6000021a8d00>)** , name: nil , objects: [] 

2021-12-03T11:06:05-0800 debug: in view onAppear, context: <NSManagedObjectContext: **0x60000348dad0>** , class: , hash: **105553171372752** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x6000021a8d00>)** , name: nil , objects: []

And then when I open a second document: 

2021-12-03T11:08:02-0800 debug: in view body, context: <NSManagedObjectContext: **0x6000034a81a0>** , class: , hash: **105553171480992** , coordinator: Optional(<NSPersistentStoreCoordinator: **0x600002198a80>)** , name: nil , objects: [] 

CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'DBObject' so +entity is unable to disambiguate. 

So the second view definitely gets a new context and a new coordinator, but CoreData for some reason gets confused with the same entity being loaded twice, even though they belong to two different contexts.

Any idea how to disambiguate the two contexts?

Accepted Answer

OK, if anybody ever looks for a similar question (or myself 3 years from now), here is a simple answer. Don't load your model in PersistentController initializer as this is what loads the model multiple times. Just make it static (either a let constant or a singleton) and attach it to NSPersistentContainer. Here is an example/

let modelURL = Bundle.main.url(forResource: "MyModel", withExtension: "momd")!
let dataModel = NSManagedObjectModel(contentsOf: modelURL)!

struct PersistenceController {
    let container: NSPersistentContainer

    init(inMemory: Bool = false, name: String = "preview") {
        container = NSPersistentContainer(name: name, managedObjectModel: dataModel)
        // other stuff
    }
}

Hi, did you ever manage to get this to work ? If so could you provide more details on how you link the core data file to the ReferenceFileDocument.

What do you do about the following ReferenceFileDocument protocol functions:

typealias SnapShot

func snapshot(...)

func fileWrapper(snapshot:...)

Not sure I understand how this works.

DocumentGroup provides the ability to browse/create/select a file but how do you create a new core data file or save one where there have been edits made. I don't see how you set the path to the actual file that is created/selected in DocumentGroup.

Perhaps I am misunderstanding something but don't you need to create the sqlite or binary core data file at the URL obtained/created by DocumentGroup ? Otherwise doesn't NSPersistentContainer just store the core data files in the apps default location.

I am assuming use of DocumentGroup means you can create the core data file on anywhere on iCloud Drive.

var body: some Scene {

        DocumentGroup(newDocument: { Document() }) { config in

            ContentView()

            // injecting persistent controller into the document view

                .environment(\.managedObjectContext, config.document.persistenceController.container

                                .viewContext)

        }

    }

I see, yes I am familiar with the Core Data API to set the filename and path but I can't see any way to use that in conjunction with DocumentGroup and/or FileDocument/ReferenceFileDocument such that you can create/open the sqlite files in iCloud Drive or in the local app sandbox.

macOS multiple document app, multiple CoreData stores
 
 
Q