核心数据迁移:将关系从一个实体更改为其父实体后的异常
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
。
我不完全确定这是否适用于轻量级迁移,但几周前我第一次更改它,直到现在它看起来还不错。我有使用 Car
的 engineData
属性 从 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
对象,它不再崩溃(即使在创建一些新对象)。如果可能的话,我非常希望了解问题的原因,并找到一个不那么激烈的解决方案。
以下是我发现的其他一些事情:
- 我尝试从另一台计算机复制数据库,但无法使用该数据重现问题。这让我想到也许数据一开始就损坏了……
- 但是,如果我回到我的应用程序及其数据的旧版本,同样的提取工作正常。如果我除了升级数据模型之外什么都不做,我会得到同样的异常。
- 我已经尝试为该关系设置一个版本散列修饰符,希望这可能会强制 Core Data 在迁移时进行清理,但没有成功。
- 如果我将批处理大小设置为 10,它可以在异常发生前遍历 900 个结果(共 1185 个)。
- 使用该过程,我可以一次删除一个好的记录,以缩小有问题的记录的范围。 (每次删除后我都会保存,如果我需要返回进行进一步测试,我会保留原始数据库的备份。)
经过大量试验后,我能够缩小范围:我有一些 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 语句工作,无需将任何内容加载到内存中——因此它避免了这个问题。
我有一个核心数据模型,其中包含一个 Car
实体,假设是一个 EngineData
实体。这是一对一的关系。
我想在我的应用程序的新版本中添加卡车。所以我创建了一个新版本的 Core Data 模型。我现在有一个 Vehicle
实体,它是 Car
的父实体。我添加了一个新的 Truck
实体,它的父实体也是 Vehicle
。对于 EngineData
,反向关系通常被命名为 object
,因此目标实体只是从 Car
更改为 Vehicle
。
我不完全确定这是否适用于轻量级迁移,但几周前我第一次更改它,直到现在它看起来还不错。我有使用 Car
的 engineData
属性 从 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
对象,它不再崩溃(即使在创建一些新对象)。如果可能的话,我非常希望了解问题的原因,并找到一个不那么激烈的解决方案。
以下是我发现的其他一些事情:
- 我尝试从另一台计算机复制数据库,但无法使用该数据重现问题。这让我想到也许数据一开始就损坏了……
- 但是,如果我回到我的应用程序及其数据的旧版本,同样的提取工作正常。如果我除了升级数据模型之外什么都不做,我会得到同样的异常。
- 我已经尝试为该关系设置一个版本散列修饰符,希望这可能会强制 Core Data 在迁移时进行清理,但没有成功。
- 如果我将批处理大小设置为 10,它可以在异常发生前遍历 900 个结果(共 1185 个)。
- 使用该过程,我可以一次删除一个好的记录,以缩小有问题的记录的范围。 (每次删除后我都会保存,如果我需要返回进行进一步测试,我会保留原始数据库的备份。)
经过大量试验后,我能够缩小范围:我有一些 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 语句工作,无需将任何内容加载到内存中——因此它避免了这个问题。