axiom-core-data-diag について
Core Data を扱う開発者向けの診断・移行ガイドです。スレッド制約、性能問題、モデル変更を症状別に調べ、軽量移行とカスタム移行、非同期データアクセスの方針を示します。
主な機能
- 01モデルバージョンの不一致を特定
- 02軽量移行とカスタム移行を選択
- 03コンテキストのスレッド制約に対応
- 04非同期でデータにアクセス
- 05リレーションの先読みとバッチ設定
- 06SwiftData と Core Data の使い分けを検討
活用シーン
- 01モデル変更後にリリース版でクラッシュする
- 02バックグラウンド処理でスレッドエラーが発生する
- 03リレーションのクエリで N+1 の性能問題が起きる
- 04旧データを新しいモデル向けに変換する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-core-data-diagインストール後、Claude Code で/axiom-core-data-diagを入力して読み込まれたか確認します。
詳細
Core Data の診断と移行
概要
Core Data の問題は、スキーマの不一致による本番環境でのクラッシュ、原因不明の並行処理エラー、高負荷時のパフォーマンス低下、安全でない移行によるデータ破損として現れます。基本原則: Core Data の問題の 85%は、スレッド制約、スキーマ移行の要件、リレーションシップのクエリパターンを誤解していることが原因であり、Core Data の欠陥が原因ではありません。
要注意の兆候 — Core Data の問題を疑う
以下のいずれかが見られた場合、フレームワークの不具合ではなく、Core Data に対する誤解を疑ってください。
- 本番環境での起動時にクラッシュする:「Unresolvable fault」(解決不能なフォルト)がスキーマ変更後に発生
- スレッド制約エラー:「Accessing NSManagedObject on a different thread」(別のスレッドから NSManagedObject にアクセスしています)
- User→Posts リレーションシップの追加後、アプリが突然遅くなる
- SwiftData アプリで複雑な機能が必要になり、Core Data の併用を検討している
- シミュレーターではスキーマ移行が成功するが、本番環境ではクラッシュする
- ❌ 禁止「Core Data は壊れているので、別のデータベースが必要です」
- Core Data は、本番環境のアプリで何兆件ものレコードを扱えます
- スキーマの不一致やスレッドエラーは、常にフレームワークではなく開発者のコードが原因です
- 問題を正当化して片付けず、原因を診断してください
重要な違い: シミュレーターはビルドのたびにデータベースを削除するため、スキーマの不一致が隠れてしまいます。実機では永続データベースが保持されるため、スキーマの不一致があると直ちにクラッシュします。必須:リリース前に、実データを使って実機で移行をテストしてください。
必須の最初の手順
コードを変更する前に、必ず最初に以下を実行してください。
// 1. Identify the crash/issue type
// Screenshot the crash message and note:
// - "Unresolvable fault" = schema mismatch
// - "different thread" = thread-confinement
// - Slow performance = N+1 queries or fetch size issues
// - Data corruption = unsafe migration
// Record: "Crash type: [exact message]"
// 2. Check if it's schema mismatch
// Compare these:
let coordinator = persistentStoreCoordinator
let model = coordinator.managedObjectModel
let store = coordinator.persistentStores.first
// Get actual store schema version:
do {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: NSSQLiteStoreType,
at: storeURL,
options: nil
)
print("Store version identifier: \(metadata[NSStoreModelVersionIdentifiersKey] ?? "unknown")")
// Get app's current model version:
print("App model version: \(model.versionIdentifiers)")
// If different = schema mismatch
} catch {
print("Schema check error: \(error)")
}
// Record: "Store version vs. app model: match or mismatch?"
// 3. Check thread-confinement for concurrency errors
// For any NSManagedObject access:
print("Main thread? \(Thread.isMainThread)")
print("Context concurrency type: \(context.concurrencyType.rawValue)")
print("Accessing from: \(Thread.current)")
// Record: "Thread mismatch? Yes/no"
// 4. Profile relationship access for N+1 problems
// In Xcode, run with arguments:
// -com.apple.CoreData.SQLDebug 1
// Check Console for SQL queries:
// SELECT * FROM USERS; (1 query)
// SELECT * FROM POSTS WHERE user_id = 1; (1 query per user = N+1!)
// Record: "N+1 found? Yes/no, how many extra queries"
// 5. Check SwiftData vs. Core Data confusion
if #available(iOS 17.0, *) {
// If using SwiftData @Model + Core Data simultaneously:
// Error: "Store is locked" or "EXC_BAD_ACCESS"
// = trying to access same database from both layers
print("Using both SwiftData and Core Data on same store?")
}
// Record: "Mixing SwiftData + Core Data? Yes/no"
ここで分かること
- スキーマの不一致 → パターン 1(軽量移行の判断)へ進みます
- スレッド制約エラー → パターン 2(async/await による並行処理)へ進みます
- N+1 クエリ → パターン 3(リレーションシップのプリフェッチ)へ進みます
- SwiftData と Core Data の競合 → パターン 4(ブリッジ)へ進みます
- 移行後の速度低下 → パターン 5(安全性のテスト)へ進みます
必須の解釈
コードを変更する前に、以下のいずれか 1 つを特定してください。
- クラッシュが「Unresolvable fault」で、かつストアとモデルのバージョンが異なる場合 → スキーマの不一致(ユーザーのエラーではありません)
- クラッシュに「different thread」と記載され、DispatchQueue を使用している場合 → スレッド制約(スレッドセーフな設計ではありません)
- リレーションシップへのアクセスでパフォーマンスが低下する場合 → N+1 クエリ(SQL ログを確認してください)
- SwiftData と Core Data のコードが共存している場合 → データレイヤーの競合(アーキテクチャ上の問題です)
- 移行テストは成功するが本番環境で失敗する場合 → 実データのエッジケース(テストの不足です)
診断結果に矛盾がある、または不明瞭な場合
- 作業を止めてください。まだパターンには進まないでください
- すべての NSManagedObject アクセスに print 文を追加します(スレッドを確認)
-com.apple.CoreData.SQLDebug 1を追加し、SQL クエリの数を数えます- 実際に起きていることと、想定していたことの基準を明確にします
判断フローチャート
Core Data problem suspected?
├─ Crash: "Unresolvable fault"?
│ └─ YES → Schema mismatch (store ≠ app model)
│ ├─ Add new required field? → Pattern 1a (lightweight migration)
│ ├─ Remove field, rename, or change type? → Pattern 1b (heavy migration)
│ └─ Don't know how to fix? → Pattern 1c (testing safety)
│
├─ Crash: "different thread"?
│ └─ YES → Thread-confinement violated
│ ├─ Using DispatchQueue for background work? → Pattern 2a (async context)
│ ├─ Mixing Core Data with async/await? → Pattern 2b (structured concurrency)
│ └─ SwiftUI @FetchRequest causing issues? → Pattern 2c (@FetchRequest safety)
│
├─ Performance: App became slow?
│ └─ YES → Likely N+1 queries
│ ├─ Accessing user.posts in loop? → Pattern 3a (prefetching)
│ ├─ Large result set? → Pattern 3b (batch sizing)
│ └─ Just added relationships? → Pattern 3c (relationship tuning)
│
├─ Using both SwiftData and Core Data?
│ └─ YES → Data layer conflict
│ ├─ Need Core Data features SwiftData lacks? → Pattern 4a (drop to Core Data)
│ ├─ Already committed to SwiftData? → Pattern 4b (stay in SwiftData)
│ └─ Unsure which to use? → Pattern 4c (decision framework)
│
└─ Migration works locally but crashes in production?
└─ YES → Testing gap
├─ Didn't test with real data? → Pattern 5a (production testing)
├─ Schema change affects large dataset? → Pattern 5b (migration safety)
└─ Need verification before shipping? → Pattern 5c (pre-deployment checklist)
よくあるパターン
パターン選択ルール(必須)
診断から始め、1 度に適用するパターンは 1 つだけにします
- 必ず「必須の最初の手順」から始めます — 実際の問題を特定します
- 判断フローチャートを実行します — 該当するパターンを絞り込みます
- パターンを 1 つ適用します — 複数のパターンを組み合わせないでください
- 実機でテストします — シミュレーターでは問題が隠れてしまいます
- 移行テストで検証します — デプロイ前に実施します
禁止事項
- ❌ 診断せずにコードを変更する
- ❌ 実機テストを省略する
- ❌ シミュレーターで成功したことを、移行が安全である証明として扱う
- ❌ 複数の移行パターンを組み合わせる
- ❌ デプロイ前の検証をせずに移行をデプロイする
パターン 1a:軽量移行(単純なスキーマ変更)
原則: Core Data は、正しく行えば、データを失うことなく単純なスキーマ(追加のみの変更)を自動で移行できます。
✅ 安全な軽量移行
- 新しいオプション項目の追加:
@NSManaged var nickname: String? - 新しい必須項目の追加(デフォルト値あり):デフォルト値を設定して属性を作成します
- エンティティまたは属性の名前変更:自動マッピングを使うマッピングモデルを使用します
- 未使用の項目の削除:モデルから削除するだけです(データはディスク上に残り、無視されます)
❌ 誤った方法(本番環境でクラッシュします)
// BAD: Adding required field without migration
@NSManaged var userID: String // Required, no default
// BAD: Assuming simulator = production
// Works in simulator (deletes DB), crashes on real device
// BAD: Modifying field type
@NSManaged var createdAt: Date // Was String, now Date
// Core Data can't automatically convert
✅ 正しい(安全な軽量マイグレーション)
// 1. In Xcode: Editor → Add Model Version
// Creates new .xcdatamodel version file
// 2. In new version, add required field WITH default:
@NSManaged var userID: String = UUID().uuidString
// 3. Mark as current model version:
// File Inspector → Versioned Core Data Model
// Check "Current Model Version"
// 4. Test:
// Simulate old version: delete app, copy old database, run with new code
// Real app loads → migration succeeded
// 5. Deploy when confident
この方法が機能する場合
- オプションのフィールドを追加する(常に安全です)
- デフォルト値を設定して必須フィールドを追加する
- フィールドを削除する
- マッピングモデルを使ってエンティティ/属性の名前を変更する
この方法が失敗する場合(軽量マイグレーションを試さないでください)
- フィールドの型を変更する(String → Int)
- オプションのフィールドを必須にする(データに null があり、変換できません)
- 複雑なリレーションシップを変更する
- カスタムデータ変換が必要
所要時間 軽量マイグレーションの設定に 5~10 分
パターン 1b:重量マイグレーション(複雑なスキーマ変更)
原則 軽量マイグレーションが機能しない場合は、NSEntityMigrationPolicy を使ってカスタム変換ロジックを実装します。
使用する場合
- フィールドの型を変更する(String → Date)
- オプションのフィールドを必須にする(既存の null を埋める必要がある)
- 複雑なリレーションシップを再構成する
- カスタムデータ変換を行う(例:「firstName lastName」を個別のフィールドに分割する)
例:String 型の日付を Date オブジェクトに変換する
// 1. Create mapping model
// File → New → Mapping Model
// Source: old version, Destination: new version
// 2. Create custom migration policy
class DateMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName ?? "",
into: manager.destinationContext
)
for key in sInstance.entity.attributesByName.keys {
destination.setValue(sInstance.value(forKey: key), forKey: key)
}
// Custom transformation: String → Date
if let dateString = sInstance.value(forKey: "createdAt") as? String,
let date = ISO8601DateFormatter().date(from: dateString) {
destination.setValue(date, forKey: "createdAt")
} else {
destination.setValue(Date(), forKey: "createdAt")
}
manager.associate(source: sInstance, withDestinationInstance: destination, for: mapping)
}
}
// 3. In mapping model Inspector:
// Set Custom Policy Class: DateMigrationPolicy
// 4. Test extensively with real data before shipping
重要な安全ルール
- マイグレーションをテストする前に、必ずデータベースをバックアップしてください
- 本番データのコピーでマイグレーションをテストしてください
- マイグレーション後にデータの整合性を確認してください(抜き取り検査)
- マイグレーションが失敗した場合に備えて、ロールバック計画を作成してください
所要時間 マイグレーションごとに 30~60 分+テスト
パターン 2a:バックグラウンド取得のための非同期コンテキスト
原則 Core Data オブジェクトはスレッドに固定されます。バックグラウンドスレッドで取得し、メインスレッドに渡すために軽量な表現に変換します。
❌ 誤り(スレッド固定によるクラッシュ)
DispatchQueue.global().async {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let results = try! context.fetch(request)
DispatchQueue.main.async {
self.objects = results // ❌ CRASH: objects faulted on background thread
}
}
✅ 正しい(バックグラウンド処理にはプライベートキューコンテキストを使用する)
// Create background context
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = viewContext
// Fetch on background thread
backgroundContext.perform {
do {
let results = try backgroundContext.fetch(userRequest)
// Convert to lightweight representation BEFORE main thread
let userIDs = results.map { $0.id } // Just the IDs, not full objects
DispatchQueue.main.async {
// On main thread, fetch full objects from main context
let mainResults = try self.viewContext.fetch(request)
self.objects = mainResults
}
} catch {
print("Fetch error: \(error)")
}
}
この方法が機能する理由
- バックグラウンドコンテキストはバックグラウンドスレッドで取得します(安全です)
- 重いオブジェクトを軽量な値に変換します(メインスレッドに安全に渡せます)
- メインコンテキストはメインスレッドで取得します(安全です)
- スレッドに固定されたオブジェクトがスレッド境界を越えることはありません
所要時間 構成の変更に 10 分
パターン 2b:構造化並行処理(Core Data での async/await)
原則 Swift Concurrency との互換性のために、NSPersistentContainer または NSManagedObjectContext の非同期メソッドを使用します。
✅ 正しい(iOS 13 以降の非同期 API)
// iOS 13+: Use async perform
let users = try await viewContext.perform {
try viewContext.fetch(userRequest)
}
// Executes fetch on correct thread, returns to caller
// iOS 17+: Use Swift Concurrency async/await directly
let users = try await container.mainContext.fetch(userRequest)
// For background work:
let backgroundUsers = try await backgroundContext.perform {
try backgroundContext.fetch(userRequest)
}
// Fetch happens on background queue, thread-safe
❌ 誤り(Swift Concurrency と DispatchQueue の混在)
async {
DispatchQueue.global().async {
try context.fetch(request) // ❌ Wrong thread!
}
}
所要時間 DispatchQueue から async/await への変換に 5 分
パターン 3a:リレーションシップの先行取得(N+1 の防止)
原則 Core Data に、アクセス時に遅延読み込みするのではなく、リレーションシップを先行取得するよう指示します。
❌ 誤り(N+1 クエリパターン)
let users = try context.fetch(userRequest)
for user in users {
let posts = user.posts // ❌ Triggers fetch for EACH user!
// 1 fetch for users + N fetches for relationships = N+1 total
}
✅ 正しい(リレーションシップを先行取得する)
var request = NSFetchRequest<User>(entityName: "User")
// Tell Core Data to fetch relationships eagerly
request.relationshipKeyPathsForPrefetching = ["posts", "comments"]
// Now relationships are fetched in a single query per relationship
let users = try context.fetch(request)
for user in users {
let posts = user.posts // ✅ INSTANT: Already fetched
// Total: 1 fetch for users + 1 fetch for all posts = 2 queries
}
その他の最適化パターン
// Batch size: fetch in chunks for large result sets
request.fetchBatchSize = 100
// Faulting behavior: convert faults to lightweight snapshots
request.returnsObjectsAsFaults = false // Keep objects in memory
// Use carefully—can cause memory pressure with large results
// Distinct: remove duplicates from relationship fetches
request.returnsDistinctResults = true
所要時間 先行取得の追加に 2~5 分
パターン 3b:取得バッチサイズ
原則 大規模な結果セットでは、メモリを管理するためにバッチ単位で取得します。
例:100,000 人のユーザーをスクロールする
var request = NSFetchRequest<User>(entityName: "User")
request.fetchBatchSize = 100 // Fetch 100 at a time
// Set sort descriptor for stable pagination
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let results = try context.fetch(request)
// Memory footprint: ~100 users at a time, not all 100,000
for user in results {
// Accessing user 0-99: in memory
// Accessing user 100: batch refetch (user 100-199)
// Auto-pagination, minimal memory usage
}
所要時間 バッチサイズの調整に 3 分
パターン 4a:SwiftData にない Core Data の機能
シナリオ SwiftData を選びましたが、SwiftData にない機能が必要です。
SwiftData にない機能
- 複雑なマイグレーション(自動マイグレーションのみ)
- カスタム検証(保存前)
- リレーションシップの削除ルール(cascade、deny、nullify)
- 直接 SQL クエリ
- 高度な先行取得
- Faulting の制御
SwiftData から Core Data に切り替える場合
- カスタムマイグレーションが必要
- 検証ロジックが必要
- 複雑なリレーションシップルールが必要
- パフォーマンス向上のために生の SQL が必要
- フォールトトレランスのパターンが必要
✅ 正しい(必要に応じたハイブリッドアプローチ)
// Keep SwiftData for simple entities
@Model final class Note {
var id: String
var title: String
}
// Drop to Core Data for complex operations
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = container.viewContext
// Fetch with Core Data, convert to SwiftData models
let results = try backgroundContext.perform {
try backgroundContext.fetch(coreDataRequest)
}
重要 同じエンティティに SwiftData と Core Data の両方から同時にアクセスしてはいけません。どちらか一方だけを使い、両方は使わないでください。
所要時間 ブリッジ層の作成に 30~60 分
パターン 4b:SwiftData を使い続ける(新規プロジェクトに推奨)
シナリオ SwiftData を使っていて、Core Data が必要かどうか迷っています。
SwiftData は、最新のアプリに必要な Core Data の機能の 80%を提供します
- 型安全なモデル(@Model)
- リアクティブなクエリ(@Query)
- CloudKit 同期(組み込み)
- 自動マイグレーション(単純な変更の場合)
- 適切な async/await 統合
SwiftData で十分な場合
- シンプルなスキーマ(ユーザー、メモ、ToDo)
- リレーションシップの複雑さが最小限
- CloudKit による同期が必要
- iOS 17 以降という要件を許容できる
- メンテナンスが必要な既存の Core Data コードがない
判断:以下のうち 3 つ以上に YES と答えられる場合は、SwiftData を使い続けてください
- ✅ iOS 17 以降のみ(iOS 16 のサポートは不要)
- ✅ シンプルなリレーションシップ(多対一であり、多対多ではない)
- ✅ 標準的なマイグレーション(フィールドの追加、削除)
- ✅ CloudKit による同期が役立つ
- ✅ 型安全性が重要
判断:以下の場合は Core Data に切り替えてください
- ❌ iOS 16 のサポートが必要(SwiftData は iOS 17 以降のみ)
- ❌ 複雑なリレーションシップルール(カスケードルール、制約)
- ❌ カスタムマイグレーションが必要
- ❌ パフォーマンスのために生の SQL が必要
- ❌ すでに Core Data のコードベースがある
所要時間 0 分(判断のみ)
パターン 5a:マイグレーション前の安全な本番テスト
原則 実データでテストせずにマイグレーションをデプロイしてはいけません。
デプロイ前の必須チェックリスト
// Step 1: Export production database
// From running app in simulator or real device:
// ~/Library/Developer/CoreData/[AppName]/
// Copy entire [AppName].sqlite database
// Step 2: Create migration test
@Test func testProductionDataMigration() throws {
// Copy production database to test location
let testDB = tempDirectory.appendingPathComponent("test.sqlite")
try FileManager.default.copyItem(from: prodDatabase, to: testDB)
// Attempt migration
var config = ModelConfiguration(url: testDB, isStoredInMemory: false)
let container = try ModelContainer(for: User.self, configurations: [config])
// Verify data integrity
let context = container.mainContext
let allUsers = try context.fetch(FetchDescriptor<User>())
// Spot checks: verify specific records migrated correctly
guard let user1 = allUsers.first(where: { $0.id == "test-id-1" }) else {
throw MigrationError.missingUser
}
// Check derived data is correct
XCTAssertEqual(user1.name, "Expected Name")
XCTAssertNotNil(user1.createdAt)
// Check relationships
XCTAssertEqual(user1.posts.count, expectedPostCount)
}
// Step 3: Run test against real production data
// Pass ✓ before shipping
安全ルール
- ❌ シミュレーターでマイグレーションをテストしないでください(シミュレーターは DB を削除します)
- ✅ 必ず実際の本番データのコピーでテストしてください
- ✅ 必ず抜き取り検査(特定のレコード)を行ってください
- ✅ 必ずリレーションシップが正しく読み込まれることを確認してください
- ✅ 必ずロールバック計画を文書化してください
所要時間 マイグレーションテストの作成に 15~30 分
パターン 5c:デプロイ前の確認チェックリスト
Core Data の変更をリリースする前に必ず確認
- 新しい .xcdatamodel バージョンを作成しましたか?(既存のものを編集しただけではありませんか?)
- 必要に応じて、新しいバージョンにマッピングモデルがありますか?
- 実際の本番データでマイグレーションをテストしましたか?(シミュレーターではありません)
- 5 件以上の特定のレコードが正しくマイグレーションされたことを確認しましたか?
- リレーションシップが読み込まれたことを確認しましたか?
- 実機(サポート対象のうち最も古い端末)でテストしましたか?
- アプリはクラッシュせずに起動しますか?(新規インストール)
- 古いデータがある状態でアプリは起動しますか?(マイグレーションパス)
- ロールバック計画を文書化しましたか?(本番環境で失敗した場合に備えて)
いずれかの項目に NO と答えた場合
- ❌ リリースしないでください
- 戻って問題を修正し、再テストしてください
- 「NO」が 1 つでもあれば、データ損失のリスクがあります
所要時間 チェックリストに 5 分
クイックリファレンステーブル
| 問題 | 確認事項 | 修正方法 |
|---|---|---|
| 「Unresolvable fault」クラッシュ | ストアとモデルのバージョンは一致していますか? | .xcdatamodel バージョンとマッピングモデルを作成する |
| 「Different thread」クラッシュ | フェッチはメインスレッドで実行されていますか? | バックグラウンド処理にはプライベートキューコンテキストを使う |
| アプリの動作が遅くなった | リレーションシップは事前にフェッチされていますか? | relationshipKeyPathsForPrefetching を追加する |
| N+1 クエリのパフォーマンス問題 | -com.apple.CoreData.SQLDebug 1 のログを確認する | 事前フェッチを追加するか、軽量な表現に変換する |
| SwiftData で Core Data の機能が必要 | カスタムマイグレーションが必要ですか? | Core Data の NSEntityMigrationPolicy を使う |
| SwiftData と Core Data のどちらにするか迷っている | iOS 16 のサポートが必要ですか? | iOS 16 には Core Data、iOS 17 以降には SwiftData を使う |
| マイグレーションテストは成功するが、本番環境では失敗する | 実データでテストしましたか? | 本番データベースのコピーを使ってマイグレーションテストを作成する |
30 分経っても行き詰まっている場合
30 分以上費やしても Core Data の問題が解決しない場合:
いったん停止してください。次のいずれかに該当します
- 必須の診断手順を省略している(最もよくある原因)
- 実際の問題を誤って特定している
- 症状に合わないパターンを適用している
- 実機/実データでテストしていない
- カスタム NSEntityMigrationPolicy が必要なエッジケースがある
「スキルが機能しなかった」と判断する前に必ず確認するチェックリスト
- 必須の初期診断をすべて実行した
- 問題の種類(スキーマ、並行処理、パフォーマンス、ブリッジング、テスト)を特定した
- Core Data の SQL デバッグログ(
-com.apple.CoreData.SQLDebug 1)を確認した - 実データを使って実機でテストした(シミュレーターではない)
- 決定木で最初に一致するパターンを適用した
- スキーマが変更された場合、マイグレーションテストを作成した
- 特定のレコードを少なくとも 3 件確認し、正しくマイグレーションされたことを確かめた
- ロールバック計画を文書化した
すべての項目にチェックが入っているのに、まだ動作しない場合
- カスタムの NSEntityMigrationPolicy が必要です(基本パターンでは扱いません)
- 所要時間:複雑なマイグレーションでは60~90分
- 「実際にどのようなデータ変換が必要ですか?」と確認し、カスタムポリシーを実装してください
所要時間の目安
- パターン1(軽量マイグレーション):5~10分
- パターン1(カスタムポリシーを使う大規模なマイグレーション):60~90分
- パターン2(並行処理):5~10分
- パターン3(プリフェッチ):2~5分
- パターン4(ブリッジング):30~60分
- パターン5(テスト):15~30分
よくある間違い
❌ シミュレーターだけでマイグレーションをテストする
- シミュレーターでは再ビルド時にデータベースが削除されるため、スキーマの不一致が見つかりません
- 対処法:必ず実機、または本番データベースのコピーでテストしてください
❌ デフォルト値がデータ損失を防ぐと思い込む
- デフォルト値が適用されるのは新しいレコードのみで、既存のデータには適用されません
- 対処法:既存のデータには NSEntityMigrationPolicy を使用してください
❌ 変換せずにスレッドをまたいで Core Data オブジェクトにアクセスする
- オブジェクトはスレッド内に限定されており、スレッドの境界を越えて渡せません
- 対処法:ほかのスレッドに渡す前に、軽量な表現に変換してください
❌ リレーションシップへのアクセスがデータベースクエリになることに気づかない
user.postsはユーザーごとにフェッチを実行します(N+1)- 対処法:relationshipKeyPathsForPrefetching を使用するか、先に ID を取り出してください
❌ 同じストアで SwiftData と Core Data を併用する
- 両方のレイヤーから同時に同じデータベースへアクセスすることはできません
- 対処法:どちらか一方のレイヤーを選ぶか、エンティティを分けてハイブリッド方式を使用してください
❌ デプロイ前にテストせずマイグレーションをデプロイする
- 本番データに含まれるエッジケースがクラッシュの原因になります
- 対処法:本番データを使ったマイグレーションテストは必須です
❌ 「データを削除すればいい」と正当化する
- ❌ 禁止:ユーザーはデータを失うことを快く思いません
- ユーザーはアプリをアンインストールし、低評価を残します
- 対処法:安全なマイグレーションテストに時間をかけてください
本番環境の危機的な状況で、安全なマイグレーションパターンを守る
問題
本番環境の危機的な状況では、次のような要求を受けることがあります。
- 「ユーザーがクラッシュしています。データベースを削除して、まっさらな状態から始めてください」
- 「マイグレーションに時間がかかりすぎています。テストを省略してリリースしてください」
- 「適切なマイグレーションに2日も待てません。急ごしらえで対応してください」
- 「スキーマが一致しませんか?新しいストアを強制的に作成してください」
一見、危機的な状況に対する現実的な対応に思えるかもしれません。**しかし、これらはデータの損失と、ユーザーからの信頼を恒久的に損なう結果を招きます。**あなたの役割は、圧力を恐れるのではなく、データ保護の原則と顧客への影響を根拠に、安全な方法を守ることです。
危険信号 — データ損失を招く PM/マネージャーからの要求
本番環境の危機的な状況で次のいずれかを耳にしたら、作業を止め、このスキルを参照してください。
- ❌ 「永続ストアを削除して、まっさらな状態から始めてください」 – ユーザーはすべてのデータを恒久的に失います
- ❌ 「テストせずに軽量マイグレーションを強制してください」 – 本番環境でデータが破損するリスクが高くなります
- ❌ 「マイグレーションを省略して、新しいストアを作成してください」 – 既存のユーザーデータを放棄することになります
- ❌ 「リリース後にデータの問題を修正します」 – 失われた、または破損したデータを復元することはできません
- ❌ 「そのままリリースしてください。サポートチケットには対処できます」 – データ損失はユーザーの恒久的な離脱を招きます
- ❌ 「シミュレーターでのテストで十分です」 – シミュレーターでは再ビルド時にデータベースが削除されるため、スキーマの不一致が見つかりません
プロとして異議を唱える方法
ステップ1:顧客への影響を数値化する
"I want to resolve this crash ASAP, but let me show you what deleting the store means:
Current situation:
- 10,000 active users with data
- Average 50 items per user (500,000 total records)
- Users have 1 week to 2 years of accumulated data
If we delete the store:
- 10,000 users lose ALL their data on next app launch
- Uninstall rate: 60-80% (industry standard after data loss)
- App Store reviews: Expect 1-star reviews citing data loss
- Recovery: Impossible - data is gone permanently
Safe alternative:
- Test migration on real device with production data copy (2-4 hours)
- Deploy migration that preserves user data
- Uninstall rate: <5% (standard update churn)"
ステップ2:リスクを実証する
PM/マネージャーに、何が起きるかを示します。
- デバイスのバックアップから本番データベースをコピーする
- 提案された「手っ取り早い修正」(ストアの削除)を実行する
- すべてのユーザーデータが恒久的に消失することを示す
- データを保持する安全なマイグレーションという代替案を示す
- 所要時間を比較する:急ごしらえの対応は30分、安全なマイグレーションは2~4時間
参考情報
- 「ユーザーはデータ損失を許しません」(App Store のレビュー傾向)
- 実機でマイグレーションをテストすれば、本番環境でのクラッシュの95%を防げます
- スキーマの不一致によるクラッシュは、既存ユーザー全員に影響します
ステップ3:妥協案を提示する
"I can get us through this crisis while protecting user data:
#### Fast track (4 hours total)
1. Copy production database from TestFlight user (30 min)
2. Write and test migration on real device copy (2 hours)
3. Submit build with tested migration (30 min)
4. Monitor first 100 updates for crashes (1 hour)
#### Fallback if migration fails
- Have "delete store" build ready as Plan B
- Only deploy if migration shows 100% failure rate
- Communicate data loss to users proactively
This approach:
- Tries safe path first (protects user data)
- Has emergency fallback (if migration impossible)
- Honest timeline (4 hours vs. "just delete it" 30 min)"
ステップ4:決定を記録する
却下された場合(PM がストアの削除を強く求める場合):
Slack message to PM + team:
"Production crisis: Schema mismatch causing crashes for existing users.
PM decision: Delete persistent store to resolve immediately.
Impact assessment:
- 10,000 users lose ALL data permanently on next app launch
- Expected uninstall rate: 60-80% based on data loss patterns
- App Store review damage: High risk of 1-star reviews
- Customer support: Expect high volume of data loss complaints
- Recovery: Impossible - deleted data cannot be recovered
Alternative proposed (4-hour safe migration) was declined due to urgency.
I'm flagging this decision proactively so we can:
1. Prepare support team for data loss complaints
2. Draft App Store response to expected negative reviews
3. Consider user communication about data loss before launch"
これが有効な理由
- プレッシャーのかかる状況で、相手の判断を疑っていません
- ユーザーへの影響(ビジネス上の結果)を数値で示しています
- 誠実な所要時間とともに、解決策を提案しています
- 代替案を提示しています(進行を妨げません)
- 判断を記録しています(リリース後にあなたを守ります)
実例:本番環境でのクラッシュ(アクティブユーザー 50 万人)
シナリオ
- アップデート後、本番アプリが全ユーザーでクラッシュしている
- エラー:「ストアを開くために使用したモデルが、ストアの作成に使用したモデルと互換性がありません」
- CTO の発言:「データベースを削除して、2 時間でホットフィックスをリリースしよう」
- アクティブユーザー 50 万人が、それぞれ平均 6 か月分のデータを保有している
対応方法
// ❌ WRONG - Deletes all user data (CTO's request)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
let storeURL = /* persistent store URL */
try? FileManager.default.removeItem(at: storeURL) // 500K users lose data
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: nil)
// ✅ CORRECT - Safe lightweight migration (4-hour timeline)
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: options)
// Migration succeeded - user data preserved
} catch {
// Migration failed — NOW consider deleting with user communication
print("Migration error: \(error)")
}
ミーティングで提示する内容
- クラッシュの原因となっているスキーマバージョンの不一致
- 軽量なマイグレーションで自動的に修正できること
- 本番データベースのコピーでのテスト(2 時間)
- 所要時間の比較:2 時間(安全)対 即時対応(データ損失)
所要時間の見積もり 合計 4 時間(マイグレーションのテストに 2 時間、ビルドとデプロイに 2 時間)
結果
- 誠実な所要時間の提示で期待値を調整できた
- 安全なマイグレーションにより、50 万人のユーザーデータを保護できた
- アンインストール率:3%(アップデート時の標準的な離脱率)
- App Store のレビュー:データ損失に関する苦情なし
マイグレーションが本当に不可能な場合の代替案
- マイグレーションに失敗した理由を記録する
- データ損失について、事前にユーザーへ伝える
- 次のバージョンでエクスポート機能を提供する
同意できなくても、データ損失を受け入れる場合
データ損失が唯一の選択肢である場合もあります。次の場合は受け入れてください。
- マイグレーションが本当に不可能である(本番データのコピーで試行済み)
- PM/CTO が、予想されるアンインストール率 60~80%を理解している
- チームが、データ損失についてユーザーに伝えることを約束している
- マイグレーションに失敗した技術的な理由を記録している
Slack に記録する
"Production crisis: Migration failed on production data copy after 4-hour testing.
Technical details:
- Attempted lightweight migration: Failed with [error]
- Attempted heavy migration with mapping model: Failed with [error]
- Root cause: [specific schema incompatibility]
Data loss decision:
- No safe migration path exists
- PM approved delete persistent store approach
- Expected impact: 60-80% uninstall rate (500K → 100-200K users)
Mitigation plan:
- Add data export feature before next schema change
- Communicate data loss to users via in-app message
- Prepare support team for complaints
- Monitor uninstall rates post-launch"
これにより、あなた自身を守るとともに、安全な選択肢を先に尽くしたことを示せます。
実際の影響
適用前 Core Data の問題のデバッグに、1 件あたり 3~8 時間
- 本番環境でのクラッシュ(スキーマの不一致)
- パフォーマンスが徐々に低下する(N+1 クエリ)
- バックグラウンド処理でのスレッドエラー
- 安全でないマイグレーションによるデータ破損
- 顧客の信頼が損なわれる
適用後 体系的な診断により、30 分~2 時間
- 診断で問題の種類を特定(5 分)
- 適切なパターンを適用(5~10 分)
- 実機でテスト(状況による)
- 自信を持ってデプロイ
重要なポイント Core Data には、よくある問題ごとに確立されたパターンがあります。問題は、症状にどのパターンを適用すべきかを開発者が把握していないことです。
最終更新日:2025-11-30 ステータス:プレッシャーのかかるシナリオで TDD によるテスト済み フレームワーク:Core Data(Foundation フレームワーク) 関連項目:SwiftData スキル(Core Data との関係の理解)