CoreData 调试警告:多个 NSEntityDescriptions 声明冲突的解决
概述
目前在苹果生态 App 的开发中,CoreData 数据库仍然是大部分中小应用的优先之选。不过,运行时 CoreData 常常产生各种“絮絮叨叨”的警告不禁让初学的秃头小码农们云里雾里。
这不,对于下面这一大段 CoreData 警告,大家是否一眼便能拔丁抽楔,立即找出真正问题之所在呢?
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass ‘XXX’ so +entity is unable to disambiguate.
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass ‘XXX’ so +entity is unable to disambiguate.
别急,我们马上为小伙伴们拨开云雾见青天。
在本篇博文中,您将学到如下内容:
- 概述
- 1. 喋喋不休:让人心烦意乱的警告
- 2. 产生警告的原因:好心办坏事
- 3. 和警告说拜拜
- 3.1 抛弃 VictoryStage 返回对象
- 3.2 指明 VictoryStage 对应的 NSEntityDescription
- 3.3 虚拟托管对象
- 总结
闲言少叙,Let‘s find out!!!😉
1. 喋喋不休:让人心烦意乱的警告
首先,让我们看一下示例中 CoreData 容器的创建代码:
struct PersistenceController {
// CoreData 持久存储容器
static let shared = PersistenceController()
// Xcode 预览使用的 CoreData 容器,永远只在内存中玩耍😁
static let preview = PersistenceController(inMemory: true)
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Store")
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)")
}
})
}
}
可以看到上面的代码平淡无奇。我们为 App 和 Xcode 预览分别创建了对应的持久存储容器(NSPersistentContainer),对于后者我们只需将存储路径指向一个“黑洞”(“/dev/null”)即可,这也是 Apple 官方标准的处理方法。
接着,简单介绍一下我们 App 数据库的表结构:
- Project 托管类存放日常项目,它含有多个 VictoryStage 对象,它们之间是一对多的关系;
- VictoryStage 托管类用来存放每个项目的执行情况;
运行 App 进入业务界面后,恼人的警告“大军”立即接踵而来:
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass ‘VictoryStage’ so +entity is unable to disambiguate.
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass ‘VictoryStage’ so +entity is unable to disambiguate.
warning: ‘VictoryStage’ (0x600003508630) from NSManagedObjectModel (0x600002128a50) claims ‘VictoryStage’.
CoreData: warning: ‘VictoryStage’ (0x600003508630) from NSManagedObjectModel (0x600002128a50) claims ‘VictoryStage’.
warning: ‘VictoryStage’ (0x6000035048f0) from NSManagedObjectModel (0x60000217d400) claims ‘VictoryStage’.
CoreData: warning: ‘VictoryStage’ (0x6000035048f0) from NSManagedObjectModel (0x60000217d400) claims ‘VictoryStage’.
error: +[VictoryStage entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[VictoryStage entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
仔细观察警告内容,它们大致在述说着同一个 NSManagedObject 托管类型被多个 NSEntityDescription 所声明,这个托管类型就是上面的 VictoryStage 类。
2. 产生警告的原因:好心办坏事
上面警告只是出现在 Xcode 的调试控制台里,并没有引起 App 执行的中断。貌似它的严重性并没有那么大。但是,要注意在这种情况下 CoreData 的存储操作可能会产生未定义行为,所以更需严肃对待。
因为警告发生在 SwiftUI 的视图中,所以我们可以立即采用“最小系统法”让罪魁祸首“原形毕露”。
很快,我们就将产生的警告定位到如下方法中:
func calcCurrentVStage() throws -> VictoryStage {
let req = VictoryStage.fetchRequest()
req.predicate = .init(format: "project == %@", self)
req.sortDescriptors = [.init(keyPath: \VictoryStage.end, ascending: false)]
req.fetchLimit = 1
// 产生一个 VictoryStage 对象用来临时保存一次性结果
let dumpContext = PersistenceController.preview.container.viewContext
let dumpVStage = VictoryStage(context: dumpContext)
dumpVStage.start = currentStageStart!
dumpVStage.end = nil
dumpVStage.daysElapsed = 0
if let recentVStage = try managedObjectContext?.fetch(req).first {
if let lastSetback = newestSetbacks(1).first, lastSetback.date! < recentVStage.realEnd {
return recentVStage
} else {
return dumpVStage
}
} else {
return dumpVStage
}
}
该方法主要做了以下几件事:
- 计算 Project 当前的 VictoryStage 对象;
- 当无法找到当前 VictoryStage 对象时,我们会产生一个临时 VictoryStage 对象,然后返回它;
- 我们希望该临时 VictoryStage 对象永远驻留在内存里,所以将它的上下文设置为前面为 Xcode 预览特供的 “In Memory Only” 上下文;
我们的本意是好的,可惜好心办了坏事。
考虑一下,当执行 calcCurrentVStage() 方法后,当前我们就同时有了两个 CoreData 持久存储容器(Container):shared 和 preview。其中,每个 Container 中都会有一个对应 VictoryStage 的实体描述对象(NSEntityDescription) ,它们重复了!这就是博文开头警告的根本原因!
再回顾一下上面的代码:
let dumpContext = PersistenceController.preview.container.viewContext
let dumpVStage = VictoryStage(context: dumpContext)
可以看到,我们创建的 dumpVStage 对象无法确定是对应 shared 或 preview 哪个容器中的 VictoryStage 描述符,这自然让 CoreData 在运行时方寸大乱。
那么,我们又该何去何从呢?
3. 和警告说拜拜
3.1 抛弃 VictoryStage 返回对象
一种解决办法是完全抛弃临时的 VictoryStage 对象,只返回一个包含 VictoryStage 对象属性的元组,毕竟我们也不是真的要读取 VictoryStage 对象,而只是想访问它内部属性的值。
3.2 指明 VictoryStage 对应的 NSEntityDescription
除此之外,我们还可以向 CoreData 运行时(Runtime)“忠告善道”指定对应的 NSEntityDescription 对象:
let previewContainer = PersistenceController.preview.container
let vstageEntity = previewContainer.managedObjectModel.entitiesByName["VictoryStage"]!
let dumpVStage = VictoryStage(entity: vstageEntity, insertInto: previewContainer.viewContext)
在上面的代码中,我们利用托管类的另一个构造器 init(entity:insertInto:) 为 dumpVStage 临时对象指明了对应的 entity 对象,并同时将其与 previewContainer.viewContext 上下文相绑定。
3.3 虚拟托管对象
既然我们的目的是创建一个只驻留于内存中的 VictoryStage 临时对象,那么为什么不再大胆一些,直接忽略 preview 容器,只借助 shared 创一个虚拟托管对象呢?
所谓虚拟托管对象就是永远不会保存到持久存储中的对象,利用这一点我们可以轻松重制上面的 dumpVStage 临时对象了:
let vstageEntity = PersistenceController.shared.container.managedObjectModel.entitiesByName["VictoryStage"]!
let dumpVStage = VictoryStage(entity: vstageEntity, insertInto: nil)
如您所见,现在 dumpVStage 对象将不会与任何上下文绑定,所以它永远不会被存储到持久数据库中。
注意,我们不能这样直接创建 VictoryStage 虚拟托管对象:
let dumpVStage = VictoryStage()
虽然编译没有任何问题,但运行会妥妥的崩溃哦,切记切记。
至此,我们完全解惑了博文开头那个警告,小伙伴们想必再遇到类似的问题一定能够胸有成竹啦,棒棒哒!💯
总结
在本篇博文中,我们讨论了 “Multiple NSEntityDescriptions claim the NSManagedObject subclass” 这一多个 NSEntityDescriptions 声明冲突警告产生的原因,并给出多个解决方案。
感谢观赏,再会啦!😎