Multiple NSEntityDescriptions Claim NSManagedObject Subclass
Asked Answered
N

12

81

I am creating a framework that allows me to use Core Data. In the framework's test target, I have configured a data model named MockModel.xcdatamodeld. It contains a single entity named MockManaged that has a single Date property.

So that I can test my logic, I am creating an in-memory store. When I want to validate my saving logic, I create an instance of the in-memory store and use it. However, I keep getting the following output in the console:

2018-08-14 20:35:45.340157-0400 xctest[7529:822360] [error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate.
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate.
2018-08-14 20:35:45.340558-0400 xctest[7529:822360] [error] warning:     'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'.
CoreData: warning:       'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'.
2018-08-14 20:35:45.340667-0400 xctest[7529:822360] [error] warning:     'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'.
CoreData: warning:       'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'.
2018-08-14 20:35:45.342938-0400 xctest[7529:822360] [error] error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass

Below is the object I use to create my in-memory stores:

class MockNSManagedObjectContextCreator {

    // MARK: - NSManagedObjectContext Creation

    static func inMemoryContext() -> NSManagedObjectContext {
        guard let model = NSManagedObjectModel.mergedModel(from: [Bundle(for: self)]) else { fatalError("Could not create model") }
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
        do {
            try coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
        } catch {
            fatalError("Could not create in-memory store")
        }
        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.persistentStoreCoordinator = coordinator
        return context
    }

}

Below is what makes up my MockManaged entity:

class MockManaged: NSManagedObject, Managed {

    // MARK: - Properties

    @NSManaged var date: Date

}

Below is what makes up my XCTestCase:

class Tests_NSManagedObjectContext: XCTestCase {

    // MARK: - Object Insertion

    func test_NSManagedObjectContext_InsertsManagedObject_WhenObjectConformsToManagedProtocol() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let changeExpectation = expectation(forNotification: .NSManagedObjectContextObjectsDidChange, object: context, handler: nil)
        let object: MockManaged = context.insertObject()
        object.date = Date()
        wait(for: [changeExpectation], timeout: 2)
    }

    // MARK: - Saving

    func test_NSManagedObjectContext_Saves_WhenChangesHaveBeenMade() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil)
        let object: MockManaged = context.insertObject()
        object.date = Date()
        do {
            try context.saveIfHasChanges()
        } catch {
            XCTFail("Expected successful save")
        }
        wait(for: [saveExpectation], timeout: 2)
    }

    func test_NSManagedObjectContext_DoesNotSave_WhenNoChangesHaveBeenMade() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil)
        saveExpectation.isInverted = true
        do {
            try context.saveIfHasChanges()
        } catch {
            XCTFail("Unexpected error: \(error)")
        }
        wait(for: [saveExpectation], timeout: 2)
    }

}

What am I doing that is causing the errors in my tests?

Nies answered 15/8, 2018 at 0:44 Comment(2)
I'm getting the actual managed context in the App delegate's didFinishLaunchingWithOptions. And I'm getting an in memory context in my tests. When you run a test, didFinishLaunchingWithOptions will be called. So, I ended up with two contexts and this is what causes these messages. So, check in didFinishLaunchingWithOptions to see if tests are running. You can check for the key XCInjectBundleInto in the process environment.Sleeve
I fixed this issue here in this thread https://mcmap.net/q/260407/-coredata-warning-multiple-nsentitydescriptions-claim-the-nsmanagedobject-subclassDuomo
D
72

Post-automatic-caching

This should not happen anymore with NSPersistent[CloudKit]Container(name: String), since it seems to cache the model automatically now (Swift 5.1, Xcode11, iOS13/MacOS10.15).

Pre-automatic-caching

NSPersistentContainer/NSPersistentCloudKitContainer does have two constructors:

The first is just a convenience initializer calling the second with a model loaded from disk. The trouble is that loading the same NSManagedObjectModel twice from disk inside the same app/test invocation results in the errors above, since every loading of the model results in external registration calls, which print errors once called a second time on the same app/test invocation. And init(name: String) was not smart enough to cache the model.

So if you want to load a container multiple time you have to load the NSManagedObjectModel once and store it in an attribute you then use on every init(name:managedObjectModel:) call.

Example: caching a model

import Foundation
import SwiftUI
import CoreData
import CloudKit

class PersistentContainer {
    private static var _model: NSManagedObjectModel?
    private static func model(name: String) throws -> NSManagedObjectModel {
        if _model == nil {
            _model = try loadModel(name: name, bundle: Bundle.main)
        }
        return _model!
    }
    private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
        guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
            throw CoreDataError.modelURLNotFound(forResourceName: name)
        }

        guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
            throw CoreDataError.modelLoadingFailed(forURL: modelURL)
        }
        return model
    }

    enum CoreDataError: Error {
        case modelURLNotFound(forResourceName: String)
        case modelLoadingFailed(forURL: URL)
    }

    public static func container() throws -> NSPersistentCloudKitContainer {
        let name = "ItmeStore"
        return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
    }
}

Old answer

Loading Core Data is a little bit of magic, where loading a model from disk and using it means it registers for certain types. A second loading tries to register for the type again, which obviously tells you that something registered for the type already.

You can load Core Data only once and cleanup that instance after each test. Cleanup means deleting every object entity and then saving. There is some function which gives you all entities which you can then fetch and delete. Batch delete is not available InMemory though so object-by-managed object it is there.

The (probably simpler) alternative is to load the model once, store it somewhere and reuse that model on every NSPersistentContainer call, it has a constructor to use a given model instead of loading it again from disk.

Durazzo answered 15/8, 2018 at 11:2 Comment(6)
What, exactly, do you mean by cleaning up the instance after each test? I attempted to destroy the persistent store after each test, but I soon realized that there is no URL to use when attempting to perform the destruction because the in-memory store doesn't use the store URL.Nies
This helped when I got this during some UnitTest cases where i was initializing the CoreData Stack in the setup. Moved it out of it so that it is only created once (thus only loading once) and it fixed it!Irvingirwin
@Irvingirwin Where did you move it to? Did you move it from setup() to a different function?Nies
Thanks for the solution and the explanation!Ceasefire
I spent so much time trying to figure this out. You saved the day! Making my NSManagedObjectModel a singleton for all my tests and making a new instance of NSPersistentContainer from the singleton model each test function fixed it. I made a doc to try and outline the many issues I have faced with CoreData: gist.github.com/levibostian/a7d46afec7e5cd72eadaadb2dcf7a227Bernina
Such workaround still required even for iOS14.5/ XCode Version 13.2.1Penultimate
D
41

In the context of unit tests with an in-memory store, you end up with two different models loaded:

  • The model loaded in your application by the main Core Data stack
  • The model loaded in your unit tests for the in-memory stack.

This causes a problem because apparently + [NSManagedObjectModel entity] looks at all available models to find a matching entity for your NSManagedObject. Since it finds two models, it will complain.

The solution is to insert your object in the context with insertNewObjectForEntityForName:inManagedObjectContext:. This will take in account the context (and as a consequence, the context's model) to look for the entity model and as a consequence limit its search to a single model.

To me it seems to be a bug in the NSManagedObject init(managedObjectContext:) method which seems to rely on +[NSManagedObject entity] rather than relying on the context's model.

Disentail answered 27/11, 2018 at 11:36 Comment(3)
I was using the approach of having a core data stack saving to the disk with sqlite for the main app. And for unit testing I had a in memory stack. The two stacks were being loaded to execute the unit tests. When I had to run a single test it worked but when I had multiple tests, I got the error. By implementing the solution, it fixed my problem. ThanksCircumpolar
Just for reference, before the fix I was getting the same console errors and also on context.save() I was getting this: "The operation couldn’t be completed. (Cocoa error 134020.)"Circumpolar
Great, half a day of debugging only to find out that NSManagedObject init(managedObjectContext:) doesn't work correctly.Marijane
C
29

As @Kamchatka pointed out, the warning is displayed because NSManagedObject init(managedObjectContext:) is used. Using NSManagedObject initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context dismisses the warning.

If you don't want to use the later constructor in your test, you can just simply create NSManagedObject extension in your test target to override the default behaviour:

import CoreData

public extension NSManagedObject {

    convenience init(usedContext: NSManagedObjectContext) {
        let name = String(describing: type(of: self))
        let entity = NSEntityDescription.entity(forEntityName: name, in: usedContext)!
        self.init(entity: entity, insertInto: usedContext)
    }
}

I found it here, so full credit should go to @shaps

Clevelandclevenger answered 10/1, 2019 at 10:38 Comment(1)
tnx for this extension it helped me a lot, you made a small mistake you named init's argument usedContext but you are using context in the body ;)Mamey
B
13

[error] warning: Multiple NSEntityDescriptions claim the ...

This warning is caused by multiple managed object models claimed as the same managed object subclass.

In the context of Core Data Unit Test, it is not a big deal since we know it is not going to break anything. However, it is also easy to get rid of the warning message by adding a static managed object model and use it for every persistent container we create. xcdatamodeld in the code snippet below is the file name of your Core Data model file.

Code snippet below is based on Xcode generated Core Data template code

public class PersistentContainer: NSPersistentCloudKitContainer {}

class PersistenceController {
    static let shared = PersistenceController()
    
    static var managedObjectModel: NSManagedObjectModel = {
        let bundle = Bundle(for: PersistenceController.self)
        
        guard let url = bundle.url(forResource: "xcdatamodeld", withExtension: "momd") else {
            fatalError("Failed to locate momd file for xcdatamodeld")
        }
        
        guard let model = NSManagedObjectModel(contentsOf: url) else {
            fatalError("Failed to load momd file for xcdatamodeld")
        }
        
        return model
    }()

    let container: PersistentContainer

    init(inMemory: Bool = false) {
        container = PersistentContainer(name: "xcdatamodeld", managedObjectModel: Self.managedObjectModel)
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}
Beavers answered 18/1, 2021 at 2:59 Comment(0)
E
11

I encountered this issue when trying to do CoreData related unit testing with following aims:

  • in-memory type NSPersistentContainer stack for speed
  • re-create stack for every test cases to wipe data

As Fabian's answer, the root cause of this problem is managedObjectModel being loaded multiple times. However, there might be several possible places of managedObjectModel loading:

  1. In App
  2. In test cases, every setUp calls of XCTestCase subclasses which try to re-create NSPersistentContainer

So it's two folds to solve this issues.

  1. Don't set up NSPersistentContainer stack in app.

You can add a underTesting flag to determine whether to set it up or not.

  1. Load managedObjectModel only once across all unit tests

I use a static variable for managedObjectModel and use it for re-creating in-memory NSPersistentContainer.

Some excerpt as following:

class UnitTestBase {
    static let managedObjectModel: NSManagedObjectModel = {
        let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: UnitTestBase.self)])!
        return managedObjectModel
    }()


    override func setUp() {
        // setup in-memory NSPersistentContainer
        let storeURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("store")
        let description = NSPersistentStoreDescription(url: storeURL)
        description.shouldMigrateStoreAutomatically = true
        description.shouldInferMappingModelAutomatically = true
        description.shouldAddStoreAsynchronously = false
        description.type = NSInMemoryStoreType

        let persistentContainer = NSPersistentContainer(name: "DataModel", managedObjectModel: UnitTestBase.managedObjectModel)
        persistentContainer.persistentStoreDescriptions = [description]
        persistentContainer.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Fail to create CoreData Stack \(error.localizedDescription)")
            } else {
                DDLogInfo("CoreData Stack set up with in-memory store type")
            }
        }

        inMemoryPersistentContainer = persistentContainer
    }
}

Above should be suffice for you to fix this issue happens in unit testing.

Enteric answered 22/2, 2019 at 15:54 Comment(0)
G
4

I fixed my warnings by changing the following:

  • I was loading a persistent store in my app twice which resulted in these warnings.
  • If you're doing stuff on NSManagedObjectModel make sure you're using the model from persistentStoreCoordinator or persistentStoreContainer. Before I was loading it directly from filesystem and got warnings.

I was not able to fix following warnings:

  • Earlier I deleted my whole persistent store and created a new container during app life cycle. I was not able to find out how to fix the warnings I got after this.
Gleam answered 9/10, 2018 at 9:3 Comment(0)
W
3

I got around this by exposing the ManagedObjectModel as a class property on my CoreData manager class:

class PersistenceManager {
    let storeName: String!

   static var managedObjectModel: NSManagedObjectModel = {
            let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: PersistenceManager.self)])!
            return managedObjectModel
        }()

    ...
}

...and then, in my tests, when I set up the PersistentContainer, I reference that model directly:

lazy var inMemoryContainer: NSPersistentContainer = {
    // Reference the model inside the app, rather than loading it again, to prevent duplicate errors
    let container = NSPersistentContainer(name: "TestContainer", managedObjectModel: PersistenceManager.managedObjectModel)
    let description = NSPersistentStoreDescription()
    description.type = NSInMemoryStoreType
    description.shouldAddStoreAsynchronously = false

    container.persistentStoreDescriptions = [description]
    container.loadPersistentStores { (description, error) in
        precondition(description.type == NSInMemoryStoreType)
        if let error = error {
            fatalError("Create an in-memory coordinator failed \(error)")
        }
    }
    return container
}()

This also has the benefit of not requiring the mom or the entity classes to be added to the test bundle directly, which I found I needed to do previously.

Wednesday answered 26/2, 2020 at 16:5 Comment(0)
D
1

Just use a singleton for creating your managedContext one time and then reuse it. It helped me with the same issue.

class CoreDataStack {

    static let shared = CoreDataStack()

    private init() {}

    var managedContext: NSManagedObjectContext {
        return self.storeContainer.viewContext
    }

//...
}
Diphosgene answered 11/2, 2020 at 15:21 Comment(2)
singleton wont solve the issue when u have privatequeuesSpectrometer
Thank you! It helped me.Tanah
L
1

I was accessing persistentContainer for two times.I deleted one.It fixed the warning and working fine.

Lengthen answered 11/3, 2020 at 10:29 Comment(0)
B
1

I was getting this on BatchInsert in-memory unit tests. I switched to using the constructor for entity name instead of entity the actual entity and that took away the warning.

I used this:

NSBatchInsertRequest(entityName: entityNameAlert(), objects: ...) //<- entityNameAlert() is a method that returns my entity name as a string

Instead of:

NSBatchInsertRequest(entity: Alert.entity(), objects: ...)

Also I was getting it on batchDelete in memory store, and I was able to eliminate that by creating objects with the extension provided above:

accepted answer but added extension

Brioche answered 4/3, 2021 at 17:55 Comment(0)
C
1

Check you Data Model file also, loading a same Core data class which points to the same location of persistant container and referring to the same context is perfectly fine.
Just like :- modelForSaveDate and modelForRetrieveData, these 2 might point to the same Coredata model inside a single Test Method.
Just check your data model file source code with "representedClassName" attribute.

In my case strangely "representedClassName" value was appended with .(dot). And as i replaced with fresh model issue got fixed as now "representedClassName" value was not appended with .(dot). This saved my life. Might help you.

Compander answered 19/8, 2021 at 14:45 Comment(0)
P
0

CoreData complains when there are multiple instances of object models. The best solution I have found is to just have a place where you statically define them.

struct ManagedObjectModels {

   static let main: NSManagedObjectModel = {
       return buildModel(named: "main")
   }()

   static let cache: NSManagedObjectModel = {
       return buildModel(named: "cache")
   }()

   private static func buildModel(named: String) -> NSManagedObjectModel {
       let url = Bundle.main.url(forResource: named, withExtension: "momd")!
       let managedObjectModel = NSManagedObjectModel.init(contentsOf: url)
       return managedObjectModel!
   }
}

Then make sure when you instantiate containers you pass these models explicitly.

let container = NSPersistentContainer(name: "cache", managedObjectModel: ManagedObjectModels.cache)
Panpipe answered 27/6, 2019 at 1:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.