核心数据迁移:将关系从一个实体更改为其父实体后的异常
Posted
技术标签:
【中文标题】核心数据迁移:将关系从一个实体更改为其父实体后的异常【英文标题】:Core Data migration: exceptions after changing a relationship from one entity to its parent entity 【发布时间】:2017-04-29 00:07:02 【问题描述】:我有一个包含Car
实体的核心数据模型,假设是EngineData
实体。这是一对一的关系。
在我的应用程序的新版本中,我想添加卡车。所以我创建了一个新版本的核心数据模型。我现在有一个Vehicle
实体,它是Car
的父实体。我添加了一个新的Truck
实体,它也有Vehicle
作为其父实体。对于EngineData
,反向关系通常命名为object
,因此目标实体只是从Car
更改为Vehicle
。
我不完全确定这是否适用于轻量级迁移,但我在几周前第一次更改了它,直到现在它看起来还不错。我有使用Car
的engineData
属性从EngineData
获取和更新现有数据的代码,我还没有看到任何问题。但是,我的应用程序中有一个 Core Data 提取,每次都会导致崩溃。我要做的就是简单地获取我所有的 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
对象,它不再崩溃(即使在创建一些新对象之后)。如果可能的话,我更愿意了解问题的原因,并找到一个不太激进的解决方案。
以下是我发现的其他一些东西:
我尝试从另一台计算机复制数据库,但无法使用该数据复制问题。这让我想到,数据可能一开始就损坏了…… 但如果我返回到旧版本的应用程序及其数据,同样的提取工作正常。如果我除了升级数据模型什么都不做,我也会遇到同样的异常。 我尝试为关系设置版本哈希修饰符,希望这可能会强制 Core Data 在迁移时清理内容,但没有运气。 如果我将批量大小设置为 10,它能够在异常之前循环遍历 900 个结果(共 1185 个)。 使用该过程,我可以一次删除一个好的记录,以缩小有问题的记录。 (每次删除后我都会保存,如果需要返回进行进一步测试,我会保留原始数据库的备份。)【问题讨论】:
【参考方案1】:经过大量试验后,我能够缩小范围:我有一些 EngineData
对象引用了一个不再存在的 Car
对象。看起来由于这种关系被破坏,记录没有正确迁移到新的数据模型版本。如果我在 Mac 应用程序 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 语句,无需将任何内容加载到内存中——因此它避免了这个问题。
【讨论】:
以上是关于核心数据迁移:将关系从一个实体更改为其父实体后的异常的主要内容,如果未能解决你的问题,请参考以下文章
在 CoreData 实体中将属性从 NSNumber 转换为 NSString - LightWeightMigration