【问题标题】:Multiple NSEntityDescriptions Claim NSManagedObject Subclass多个 NSEntityDescriptions 声明 NSManagedObject 子类
【发布时间】:2019-01-21 21:43:01
【问题描述】:

我正在创建一个允许我使用 Core Data 的框架。在框架的测试目标中,我配置了一个名为MockModel.xcdatamodeld的数据模型。它包含一个名为 MockManaged 的实体,该实体具有一个 Date 属性。

为了测试我的逻辑,我正在创建一个内存存储。当我想验证我的保存逻辑时,我会创建一个内存存储实例并使用它。但是,我在控制台中不断收到以下输出:

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

下面是我用来创建内存存储的对象:

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
    }

}

以下是构成我的MockManaged 实体的内容:

class MockManaged: NSManagedObject, Managed {

    // MARK: - Properties

    @NSManaged var date: Date

}

下面是我的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)
    }

}

我在做什么导致我的测试出现错误?

【问题讨论】:

  • 我在 App 委托的 didFinishLaunchingWithOptions 中获得了实际的托管上下文。而且我在测试中得到了内存上下文。当您运行测试时,didFinishLaunchingWithOptions 将被调用。所以,我最终得到了两个上下文,这就是导致这些消息的原因。因此,检查 didFinishLaunchingWithOptions 以查看测试是否正在运行。您可以在进程环境中检查密钥 XCInjectBundleInto。

标签: core-data nsmanagedobject nsmanagedobjectcontext


【解决方案1】:

这个扩展为我解决了这个问题。

import CoreData

public extension NSManagedObject {

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

}

在调用便利初始化程序self.init(context) 时,会生成一个与我们自己的不匹配的附加实体描述。此扩展强制实体描述与我们的类的名称相匹配。

Source

【讨论】:

    【解决方案2】:

    还要检查您的数据模型文件,加载指向持久容器的相同位置并引用相同上下文的相同核心数据类非常好。
    就像:-modelForSaveDate 和 modelForRetrieveData,这两个可能在单个测试方法中指向同一个 Coredata 模型。
    只需使用“representedClassName”属性检查您的数据模型文件源代码。

    在我的情况下,奇怪的是“representedClassName”值附加了.(点)。 当我用新模型替换时,问题得到了修复,因为现在“representedClassName”值没有附加.(点)。 这救了我的命。 可能会对你有所帮助。

    【讨论】:

      【解决方案3】:

      我在 BatchInsert 内存单元测试中得到了这个。我切换到使用实体名称的构造函数而不是实际实体的实体,这消除了警告。

      我用过这个:

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

      代替:

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

      我还在内存存储中通过 batchDelete 获得它,我能够通过使用上面提供的扩展创建对象来消除它:

      accepted answer but added extension

      【讨论】:

        【解决方案4】:

        [error] 警告:多个 NSEntityDescriptions 声明 ...

        此警告是由多个托管对象模型声称为同一托管对象子类引起的。

        在核心数据单元测试的上下文中,这没什么大不了的,因为我们知道它会破坏任何东西。但是,通过添加静态托管对象模型并将其用于我们创建的每个持久容器,也很容易摆脱警告消息。下面代码sn-p中的xcdatamodeld是你的Core Data模型文件的文件名。

        下面的代码 sn-p 是基于 Xcode 生成的 Core Data 模板代码

        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)")
                    }
                })
            }
        }
        

        【讨论】:

          【解决方案5】:

          后自动缓存

          NSPersistent[CloudKit]Container(name: String) 不会再出现这种情况,因为它现在似乎会自动缓存模型(Swift 5.1、Xcode11、iOS13/MacOS10.15)。

          预自动缓存

          NSPersistentContainer/NSPersistentCloudKitContainer 确实有两个构造函数:

          第一个只是一个方便的初始化器,它使用从磁盘加载的模型调用第二个。问题是从同一app/test invocation 中的磁盘加载相同的NSManagedObjectModel 两次会导致上述错误,因为每次加载模型都会导致外部注册调用,这会在同一@987654328 上再次调用一次打印错误@。 并且init(name: String) 不够聪明,无法缓存模型。

          因此,如果您想多次加载容器,则必须加载一次 NSManagedObjectModel 并将其存储在一个属性中,然后在每次 init(name:managedObjectModel:) 调用时使用。

          示例:缓存模型

          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))
              }
          }
          

          旧答案

          加载核心数据有点神奇,从磁盘加载模型并使用它意味着它为某些类型注册。第二次加载尝试再次注册该类型,这显然告诉您已经为该类型注册了一些东西。

          您只能加载一次 Core Data 并在每次测试后清理该实例。清理意味着删除每个对象实体然后保存。有一些功能可以为您提供所有实体,然后您可以获取和删除这些实体。批量删除在 InMemory 中不可用,但它存在逐个管理对象。

          (可能更简单的)替代方法是加载模型一次,将其存储在某处并在每次NSPersistentContainer 调用时重用该模型,它有一个构造函数来使用给定模型,而不是从磁盘再次加载它。

          【讨论】:

          • 每次测试后清理实例究竟是什么意思?我尝试在每次测试后销毁持久存储,但我很快意识到尝试执行销毁时没有可使用的 URL,因为内存中的存储不使用存储 URL。
          • 这有助于我在一些 UnitTest 案例中得到这个,我在设置中初始化 CoreData 堆栈。将其移出,使其仅创建一次(因此仅加载一次)并修复它!
          • @Sepui 你把它搬到哪里了?您是否将其从 setup() 移至其他函数?
          • 感谢您的解决方案和解释!
          • 我花了很多时间试图弄清楚这一点。你拯救了这一天!使我的NSManagedObjectModel 成为我所有测试的单例,并从单例模型中创建NSPersistentContainer 的新实例,每个测试函数都修复了它。我制作了一个文档来尝试概述我在使用 CoreData 时遇到的许多问题:gist.github.com/levibostian/a7d46afec7e5cd72eadaadb2dcf7a227
          【解决方案6】:

          我访问了 persistentContainer 两次。我删除了一次。它修复了警告并且工作正常。

          【讨论】:

            【解决方案7】:

            我通过将 ManagedObjectModel 公开为我的 CoreData 管理器类的类属性来解决这个问题:

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

            ...然后,在我的测试中,当我设置 PersistentContainer 时,我直接引用该模型:

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

            这还有一个好处是不需要将 mom 或实体类直接添加到测试包中,我发现我以前需要这样做。

            【讨论】:

              【解决方案8】:

              只需使用singleton 来创建您的 managedContext 一次,然后重复使用它。它帮助我解决了同样的问题。

              class CoreDataStack {
              
                  static let shared = CoreDataStack()
              
                  private init() {}
              
                  var managedContext: NSManagedObjectContext {
                      return self.storeContainer.viewContext
                  }
              
              //...
              }
              

              【讨论】:

              • 当你有私有队列时,单例不会解决问题
              • 谢谢!它帮助了我。
              【解决方案9】:

              正如@Kamchatka 指出的那样,显示警告是因为使用了NSManagedObject init(managedObjectContext:)。使用 NSManagedObject initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context 会消除警告。

              如果您不想在测试中使用后面的构造函数,您只需在测试目标中创建 NSManagedObject 扩展名到 override 默认行为:

              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)
                  }
              
              }
              

              我发现它here,所以完整的信用应该去@shaps

              【讨论】:

              • tnx for this extension 它对我帮助很大,你犯了一个小错误,你将 init 的参数命名为 usedContext 但你在正文中使用了 context ;)
              【解决方案10】:

              当有多个对象模型实例时,CoreData 会报错。我找到的最好的解决方案就是有一个静态定义它们的地方。

              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!
                 }
              }
              

              然后确保在实例化容器时明确传递这些模型。

              let container = NSPersistentContainer(name: "cache", managedObjectModel: ManagedObjectModels.cache)
              

              【讨论】:

                【解决方案11】:

                我在尝试使用以下目标进行 CoreData 相关单元测试时遇到了这个问题:

                • 内存类型 NSPersistentContainer 堆栈以提高速度
                • 为每个测试用例重新创建堆栈以擦除数据

                作为 Fabian 的回答,这个问题的根本原因是 managedObjectModel 被多次加载。但是,managedObjectModel 加载可能有几个可能的位置:

                1. 在应用中
                2. 在测试用例中,每次 setUp 调用 XCTestCase 子类都会尝试重新创建 NSPersistentContainer

                所以解决这个问题有两个方面。

                1. 不要在应用程序中设置 NSPersistentContainer 堆栈。

                您可以添加underTesting 标志来确定是否设置它。

                1. 在所有单元测试中只加载一次managedObjectModel

                我为managedObjectModel 使用了一个静态变量,并用它来重新创建内存中的 NSPersistentContainer。

                摘录如下:

                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
                    }
                }
                

                以上内容应该足以解决单元测试中发生的这个问题。

                【讨论】:

                  【解决方案12】:

                  在使用内存存储进行单元测试的情况下,您最终会加载两个不同的模型:

                  • 主 Core Data 堆栈在您的应用程序中加载的模型
                  • 在内存堆栈的单元测试中加载的模型。

                  这会导致一个问题,因为显然+ [NSManagedObjectModel entity] 会查看所有可用模型来为您的 NSManagedObject 找到匹配的实体。既然找到了两个模型,就会报错。

                  解决方案是使用insertNewObjectForEntityForName:inManagedObjectContext: 在上下文中插入您的对象。这将考虑上下文(因此,上下文的模型)来查找实体模型,并因此将其搜索限制为单个模型。

                  对我来说,这似乎是 NSManagedObject init(managedObjectContext:) 方法中的一个错误,它似乎依赖于 +[NSManagedObject entity] 而不是依赖于上下文的模型。

                  【讨论】:

                  • 我正在使用将核心数据堆栈保存到磁盘的方法,主应用程序使用 sqlite。对于单元测试,我有一个内存堆栈。正在加载两个堆栈以执行单元测试。当我必须运行一个测试时它可以工作,但是当我进行多个测试时,我得到了错误。通过实施解决方案,它解决了我的问题。谢谢
                  • 仅供参考,在修复之前我遇到了相同的控制台错误,并且在 context.save() 上我得到了这个:“操作无法完成。(Cocoa 错误 134020。) "
                  • 太好了,调试了半天才发现 NSManagedObject init(managedObjectContext:) 不能正常工作。
                  【解决方案13】:

                  我通过更改以下内容修复了我的警告:

                  • 我在我的应用中加载了两次持久性商店,导致出现这些警告。
                  • 如果您在 NSManagedObjectModel 上做事,请确保您使用的是来自 persistentStoreCoordinatorpersistentStoreContainer 的模型。在我直接从文件系统加载它并收到警告之前。

                  我无法修复以下警告:

                  • 之前我删除了整个持久性存储并在应用程序生命周期中创建了一个新容器。我不知道如何解决在此之后收到的警告。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2022-12-31
                    • 2017-03-12
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-03-07
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多