核心数据迁移:将关系从一个实体更改为其父实体后的异常

Core Data migration: exceptions after changing a relationship from one entity to its parent entity

我有一个核心数据模型,其中包含一个 Car 实体,假设是一个 EngineData 实体。这是一对一的关系。

我想在我的应用程序的新版本中添加卡车。所以我创建了一个新版本的 Core Data 模型。我现在有一个 Vehicle 实体,它是 Car 的父实体。我添加了一个新的 Truck 实体,它的父实体也是 Vehicle。对于 EngineData,反向关系通常被命名为 object,因此目标实体只是从 Car 更改为 Vehicle

我不完全确定这是否适用于轻量级迁移,但几周前我第一次更改它,直到现在它看起来还不错。我有使用 CarengineData 属性 从 EngineData 获取和更新现有数据的代码,我没有发现任何问题。但是,我的应用程序中有一个核心数据提取每次都会导致崩溃。导致崩溃我所要做的就是简单地获取我所有的 EngineData 对象:

do {
    let request: NSFetchRequest<EngineData> = EngineData.fetchRequest()
    let objects = try context.fetch(request)
} catch {
    NSLog("Error fetching data: \(error)")
}

context.fetch 行,我得到一个异常:

[error] error: Background Core Data task threw an exception.  Exception = *** -[__NSArrayM objectAtIndex:]: index 18446744073709551615 beyond bounds [0 .. 10] and userInfo = (null)
CoreData: error: Background Core Data task threw an exception.  Exception = *** -[__NSArrayM objectAtIndex:]: index 18446744073709551615 beyond bounds [0 .. 10] and userInfo = (null)

如果我尝试对这些对象做任何事情,我会收到更多异常,直到应用程序崩溃:

[General] An uncaught exception was raised
[General] *** -[__NSArrayM objectAtIndex:]: index 18446744073709551615 beyond bounds [0 .. 10]

0   CoreFoundation                      0x00007fff8861937b __exceptionPreprocess + 171
1   libobjc.A.dylib                     0x00007fff9d40d48d objc_exception_throw + 48
2   CoreFoundation                      0x00007fff88532b5c -[__NSArrayM objectAtIndex:] + 204
3   CoreData                            0x00007fff881978ed -[NSSQLRow newObjectIDForToOne:] + 253
4   CoreData                            0x00007fff8819770f -[NSSQLRow _validateToOnes] + 399
5   CoreData                            0x00007fff88197571 -[NSSQLRow knownKeyValuesPointer] + 33
6   CoreData                            0x00007fff88191868 _prepareResultsFromResultSet + 4312
7   CoreData                            0x00007fff8818e47b newFetchedRowsForFetchPlan_MT + 3387
8   CoreData                            0x00007fff8835f6d7 _executeFetchRequest + 55
9   CoreData                            0x00007fff8828bb35 -[NSSQLFetchRequestContext executeRequestUsingConnection:] + 53
10  CoreData                            0x00007fff8832c9c8 __52-[NSSQLDefaultConnectionManager handleStoreRequest:]_block_invoke + 216
11  libdispatch.dylib                   0x000000010105478c _dispatch_client_callout + 8
12  libdispatch.dylib                   0x00000001010555ad _dispatch_barrier_sync_f_invoke + 307
13  CoreData                            0x00007fff8832c89d -[NSSQLDefaultConnectionManager handleStoreRequest:] + 237
14  CoreData                            0x00007fff88286c86 -[NSSQLCoreDispatchManager routeStoreRequest:] + 310
15  CoreData                            0x00007fff88261189 -[NSSQLCore dispatchRequest:withRetries:] + 233
16  CoreData                            0x00007fff8825e21d -[NSSQLCore processFetchRequest:inContext:] + 93
17  CoreData                            0x00007fff8817d218 -[NSSQLCore executeRequest:withContext:error:] + 568
18  CoreData                            0x00007fff882436de __65-[NSPersistentStoreCoordinator executeRequest:withContext:error:]_block_invoke + 5486
19  CoreData                            0x00007fff8823a347 -[NSPersistentStoreCoordinator _routeHeavyweightBlock:] + 407
20  CoreData                            0x00007fff8817cd9e -[NSPersistentStoreCoordinator executeRequest:withContext:error:] + 654
21  CoreData                            0x00007fff8817af51 -[NSManagedObjectContext executeFetchRequest:error:] + 593
22  libswiftCoreData.dylib              0x0000000100c91648 _TFE8CoreDataCSo22NSManagedObjectContext5fetchuRxSo20NSFetchRequestResultrfzGCSo14NSFetchRequestx_GSax_ + 56

我认为标志 -com.apple.CoreData.SQLDebug 1 可能会给我一些有用的信息,但我在这里没有看到任何有用的信息:

CoreData: annotation: Connecting to sqlite database file at "/Users/name/Library/Group Containers/Folder/Database.sqlite"
CoreData: sql: pragma recursive_triggers=1
CoreData: sql: pragma journal_mode=wal
CoreData: sql: SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_MODELCACHE'
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZDATA, t0.ZOBJECT, t0.Z9_OBJECT FROM ZENGINEDATA t0 
2017-04-28 16:18:41.693548-0400 AppName[95979:11442888] [error] error: Background Core Data task threw an exception.  Exception = *** -[__NSArrayM objectAtIndex:]: index 18446744073709551615 beyond bounds [0 .. 10] and userInfo = (null)
CoreData: error: Background Core Data task threw an exception.  Exception = *** -[__NSArrayM objectAtIndex:]: index 18446744073709551615 beyond bounds [0 .. 10] and userInfo = (null)
CoreData: annotation: sql connection fetch time: 0.0065s
CoreData: annotation: fetch using NSSQLiteStatement <0x6080004805a0> on entity 'ENGINEDATA' with sql text 'SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZDATA, t0.ZOBJECT, t0.Z9_OBJECT FROM ZENGINEDATA t0 ' returned 1185 rows with values: (
    "<JUNENGINEDATA: 0x608000480eb0> (entity: ENGINEDATA; id: 0x40000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p1> ; data: <fault>)",
    "<JUNENGINEDATA: 0x608000480f00> (entity: ENGINEDATA; id: 0x80000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p2> ; data: <fault>)",
    "<JUNENGINEDATA: 0x608000480f50> (entity: ENGINEDATA; id: 0xc0000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p3> ; data: <fault>)",
    ...
    "<JUNENGINEDATA: 0x600000288110> (entity: ENGINEDATA; id: 0x128c0000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p1187> ; data: <fault>)",
    "<JUNENGINEDATA: 0x600000288160> (entity: ENGINEDATA; id: 0x12900000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p1188> ; data: <fault>)",
    "<JUNENGINEDATA: 0x6000002881b0> (entity: ENGINEDATA; id: 0x12940000b <x-coredata://10C884E2-18EF-4DA2-BC5D-4CBD0CE7D1A6/ENGINEDATA/p1189> ; data: <fault>)"
)
CoreData: annotation: total fetch execution time: 0.1053s for 1185 rows.
2017-04-28 16:18:41.793201-0400 AppName[95979:11442649] Got results: 1185

好消息是 EngineData 中的所有内容都可以重新创建,而且我发现如果我批量删除所有 EngineData 对象,它不再崩溃(即使在创建一些新对象)。如果可能的话,我非常希望了解问题的原因,并找到一个不那么激烈的解决方案。

以下是我发现的其他一些事情:

经过大量试验后,我能够缩小范围:我有一些 EngineData 对象引用了一个不再存在的 Car 对象。看起来由于这种关系被打破,记录没有正确迁移到新的数据模型版本。如果我在 Mac app Base 中打开 .sqlite 文件,我可以看到一个新的 Z9_OBJECT 字段,该字段针对所有良好记录设置为 10,并且 NULL 损坏的。

找到原因后,解决问题就相当容易了。使用 NSBatchDeleteRequest 我能够找到并删除损坏的对象:

do {
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "EngineData")
    fetchRequest.predicate = NSPredicate(format: "object.uuid == nil")
    let batchRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    try context.execute(batchRequest)
} catch {
    NSLog("Error deleting objects: \(error)")
}

在我的谓词中,object 是父对象(以前是 Car,现在是 Vehicle),uuid 是父对象的非可选属性目的。只要属性不是可选的——这意味着它对任何未损坏的对象都有一个值——这应该成功地只删除损坏的对象。

您可能希望这会导致异常,因为它仍在获取损坏的对象!幸运的是,NSBatchDeleteRequest 直接在存储上通过 运行 SQL 语句工作,无需将任何内容加载到内存中——因此它避免了这个问题。