多个 NSEntityDescriptions 声明 NSManagedObject 子类

Multiple NSEntityDescriptions Claim NSManagedObject Subclass

我正在创建一个允许我使用 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)
    }

}

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

Post-自动缓存

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

预自动缓存

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

第一个只是一个方便的初始化程序,它使用从磁盘加载的模型调用第二个。问题在于,从同一 app/test invocation 中的磁盘两次加载相同的 NSManagedObjectModel 会导致上述错误,因为每次加载模型都会导致外部注册调用,第二次调用时会打印错误同样的app/test invocationinit(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))
    }
}

旧答案

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

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

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

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

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

我无法修复以下警告:

  • 之前我删除了整个持久存储并在应用程序生命周期中创建了一个新容器。我无法找到如何修复此后收到的警告。

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

  • 主要核心数据堆栈在您的应用程序中加载的模型
  • 加载到内存堆栈单元测试中的模型。

这会导致问题,因为显然 + [NSManagedObjectModel entity] 查看所有可用模型以找到与您的 NSManagedObject 匹配的实体。由于它找到两个模型,它会抱怨。

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

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

正如@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, so full credit should go to @shaps

我在尝试针对以下目标进行 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
    }
}

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

当存在多个对象模型实例时,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)

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

class CoreDataStack {

    static let shared = CoreDataStack()

    private init() {}

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

//...
}

我通过在我的 CoreData 管理器 class:

上将 ManagedObjectModel 公开为 class 属性 来解决这个问题
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
}()

这还有一个好处,即不需要将妈妈或实体 classes 直接添加到测试包中,我发现我之前需要这样做。

我正在访问 persistentContainer 两个 times.I 已删除 one.It 修复了警告并且工作正常。

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

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

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

下面的代码片段是基于Xcode生成的核心数据模板代码

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

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

我用过这个:

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

而不是:

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

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

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

在我的例子中,奇怪的是“representedClassName”值附加了 .(点)。 当我用新模型替换时,问题得到解决,因为现在“representedClassName”值没有附加 .(点)。 这救了我的命。 可能对你有帮助。

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

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) 生成了一个与我们自己的不匹配的额外实体描述。此扩展强制实体描述与我们的 class.

的名称相匹配

Source