追記: バグが修正されたようで、Xcode7 以降では再現しませんでした。
まず、アプリがリリース前の場合はConfiguration分割をしなければとりあえずこの現象は避けられる。
Configuration分割する必要性が高くないのであれば分割しないのがお薦め。
それでもConfiguration分割をする場合、またはすでにリリース済みのアプリで、Configration分割している場合は、バージョンアップの過程で突然発火する可能性があるのでご一読推奨です。
前提
- Xcode6(5以下は試してない)
- ストアはNSSQLiteStoreType(他では試してない)
- 複数Configurationを使用
とりあえず再現する最小構成のプロジェクトファイルを github に上げた。
https://github.com/satoshi-takano/reproduce_coredata_lightweight_migration_error
CoreData軽量マイグレーションのバグと思われ。
ざっくりどんな問題か
特定の条件を満たしてConfiguration分割したモデルのバージョンアップをすると、あるバージョンから
addPersistentStoreWithType:configuration:URL:options:error:
の error が NSUnderlyingError "The operation couldn’t be completed. (Cocoa error 134110.) になる。
エラーの詳細を見るとマイグレーションの過程で、テーブルに存在しないカラムから値をSELECTしようとしていることがわかる。
addPersistentStoreWithType に失敗しているので、ユーザーからはデータが消えたように見える。
実際にはストアファイルは失われないので、修復用のバージョンアップで復旧は可能。
原因は?
CoreDataの軽量マイグレーションはメタデータ用のテーブルの内容から
※こんなん
{
NSPersistenceFrameworkVersion = 519;
NSStoreModelVersionHashes = {
EntityA = <5294236b fd22c89e 671401c2 ce3ae677 340b7118 d0930f79 ae760343 d2b2ced1>;
EntityB = <764dceb7 4326d5e6 b822647b 51730769 a59d075e 1330b9f6 c7319af7 2d3db7b9>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
CoreDataAutoMigrationErrorExample
);
NSStoreType = SQLite;
NSStoreUUID = "0C0ECD11-FFD9-4798-A4EF-845B614219B5";
"_NSAutoVacuumLevel" = 2;
}
マイグレーションが必用かどうかを判断しているが、以下の条件を満たすと実際のスキーマと、メタデータとの間に不整合が生じることにより、以降存在しないカラムから値を取得しようとしてSQLiteのエラーを引き起こす。
条件
結構複雑なので再現プロジェクトのモデルを例に説明する。
再現用プロジェクトに3つのバージョンのxcdatamodelが入っていて、
順番にバージョンアップしていくと、2つめのバージョンでSQLiteのスキーマとメタデータに不整合が生じ、3つめのバージョンで addPersistentStoreWithType:configuration:URL:options:error
がエラーになる。
- モデルバージョン1
モデルエディタで EntityA, EntityB が定義されていて、
それぞれ EntityA_Store, EntityB_Store という Configuration に設定されている。
Attribute は下図の通り
- モデルバージョン2
EntityA に Attribute を追加
このバージョンへのマイグレーションでは、
EntityA はカラム追加により過去バージョンとの互換性が失われているため、 EntityA_Store に対し自動マイグレーションが実行される。
EntityB には変更がないため、 EntityB_Store に対しては自動マイグレーションが実行されない。
そして何故か EntityB_Store の EntityA テーブルは変更されないのに EntityA についてのメタデータは更新され、
この時点で EntityB_Store の EntityA はスキーマとメタデータとの間に不整合が起きる。
※ そもそも EntityB_Store に EntityA のテーブルが作られるのも腑に落ちないが...
マイグレーション前の EntityB_Store メタデータ
{
NSPersistenceFrameworkVersion = 519;
NSStoreModelVersionHashes = {
EntityA = <5294236b fd22c89e 671401c2 ce3ae677 340b7118 d0930f79 ae760343 d2b2ced1>;
EntityB = <764dceb7 4326d5e6 b822647b 51730769 a59d075e 1330b9f6 c7319af7 2d3db7b9>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
CoreDataAutoMigrationErrorExample
);
NSStoreType = SQLite;
NSStoreUUID = "0C0ECD11-FFD9-4798-A4EF-845B614219B5";
"_NSAutoVacuumLevel" = 2;
}
マイグレーション後の EntityB_Store メタデータ
{
NSPersistenceFrameworkVersion = 519;
NSStoreModelVersionHashes = {
EntityA = <0c716cd8 68c244cc d31719e1 c5636168 0ba32568 3f15d7a6 56718cd4 b018bed5>;
EntityB = <764dceb7 4326d5e6 b822647b 51730769 a59d075e 1330b9f6 c7319af7 2d3db7b9>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
"CoreDataAutoMigrationErrorExample_2"
);
NSStoreType = SQLite;
NSStoreUUID = "0C0ECD11-FFD9-4798-A4EF-845B614219B5";
"_NSAutoVacuumLevel" = 2;
}
- モデルバージョン3
EntityA, EntityB にそれぞれ Attribute を追加
このバージョンへのマイグレーションでは、EntityA_Store EntityB_Store どちらも軽量マイグレーションが実行される。
ただし EntityB_Store に対してのマイグレーション処理の過程で、移行元テーブルから移行先テーブルにデータを流し込む際に存在しない attr_a_2 カラムを含めて SELECT していて SQLiteのエラーを引き起こす。
その結果 addPersistentStoreWithType:configuration:URL:options:error
もエラーになる。
どう解決するのか
自分が遭遇した時は、Default Configuration に1本化した新しいモデルバージョンを用意し、そちらに改めてマイグレーションした。
Configuration 分割することによるパフォーマンス上のメリットは失われるが、ユーザーの端末からデータが失われる(すくなくともユーザーにとってはそう見える)よりはマシ。
一応バグと断定して話を進めてきたけど本家の言質をとってないので、正直まだバグ確定とまではいえない。
公式のバグレポーターからプロジェクトファイル付けてレポートしたのに
Please attach a sample app and/or Xcode project.
という返信が来てなんか心が折れた