axiom-core-data-diag
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCore Data Diagnostics & Migration
Core Data 诊断与迁移
Overview
概述
Core Data issues manifest as production crashes from schema mismatches, mysterious concurrency errors, performance degradation under load, and data corruption from unsafe migrations. Core principle 85% of Core Data problems stem from misunderstanding thread-confinement, schema migration requirements, and relationship query patterns—not Core Data defects.
Core Data问题表现为架构不匹配导致的生产环境崩溃、难以捉摸的并发错误、负载下的性能下降,以及不安全迁移导致的数据损坏。核心原则:85%的Core Data问题源于对线程限制、架构迁移要求和关联查询模式的误解,而非Core Data框架本身的缺陷。
Red Flags — Suspect Core Data Issue
危险信号——怀疑是Core Data问题
If you see ANY of these, suspect a Core Data misunderstanding, not framework breakage:
- Crash on production launch: "Unresolvable fault" after schema change
- Thread-confinement error: "Accessing NSManagedObject on a different thread"
- App suddenly slow after adding a User→Posts relationship
- SwiftData app needs complex features; considering mixing Core Data alongside
- Schema migration works in simulator but crashes on production
- ❌ FORBIDDEN "Core Data is broken, we need a different database"
- Core Data handles trillions of records in production apps
- Schema mismatches and thread errors are always developer code, not framework
- Do not rationalize away the issue—diagnose it
Critical distinction Simulator deletes the database on each rebuild, hiding schema mismatch issues. Real devices keep persistent databases and crash immediately on schema mismatch. MANDATORY: Test migrations on real device with real data before shipping.
如果出现以下任何一种情况,应怀疑是对Core Data的理解有误,而非框架故障:
- 生产环境启动时崩溃:架构变更后出现"无法解析的故障"
- 线程限制错误:"在不同线程上访问NSManagedObject"
- 添加User→Posts关联后应用突然变慢
- SwiftData应用需要复杂功能;考虑同时混用Core Data
- 架构迁移在模拟器中正常运行,但在生产环境崩溃
- ❌ 禁止 认为"Core Data坏了,我们需要换个数据库"
- Core Data在生产应用中处理过数万亿条记录
- 架构不匹配和线程错误始终是开发者代码问题,而非框架问题
- 不要回避问题——要诊断问题
关键区别:模拟器每次重建都会删除数据库,隐藏架构不匹配问题。真实设备会保留持久化数据库,一旦架构不匹配会立即崩溃。强制要求:发布前必须在真实设备上使用真实数据测试迁移。
Mandatory First Steps
强制第一步
ALWAYS run these FIRST (before changing code):
swift
// 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"在修改代码前,务必先执行以下步骤:
swift
// 1. 确定崩溃/问题类型
// 截图崩溃信息并记录:
// - "Unresolvable fault" = 架构不匹配
// - "different thread" = 线程限制问题
// - 性能缓慢 = N+1查询或获取大小问题
// - 数据损坏 = 不安全迁移
// 记录:"崩溃类型:[准确错误信息]"
// 2. 检查是否为架构不匹配
// 对比以下内容:
let coordinator = persistentStoreCoordinator
let model = coordinator.managedObjectModel
let store = coordinator.persistentStores.first
// 获取实际存储的架构版本:
do {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: NSSQLiteStoreType,
at: storeURL,
options: nil
)
print("存储版本标识符:\(metadata[NSStoreModelVersionIdentifiersKey] ?? "unknown")")
// 获取应用当前的模型版本:
print("应用模型版本:\(model.versionIdentifiers)")
// 如果不同 = 架构不匹配
} catch {
print("架构检查错误:\(error)")
}
// 记录:"存储版本与应用模型:匹配或不匹配?"
// 3. 检查并发错误的线程限制
// 对于任何NSManagedObject访问:
print("主线程?\(Thread.isMainThread)")
print("上下文并发类型:\(context.concurrencyType.rawValue)")
print("访问来源线程:\(Thread.current)")
// 记录:"线程不匹配?是/否"
// 4. 分析关联访问以排查N+1问题
// 在Xcode中,使用以下参数运行:
// -com.apple.CoreData.SQLDebug 1
// 检查控制台中的SQL查询:
// SELECT * FROM USERS; (1次查询)
// SELECT * FROM POSTS WHERE user_id = 1; (每个用户1次查询 = N+1!)
// 记录:"是否发现N+1?是/否,额外查询次数"
// 5. 检查SwiftData与Core Data的混淆问题
if #available(iOS 17.0, *) {
// 如果同时使用SwiftData @Model + Core Data:
// 错误:"Store is locked" 或 "EXC_BAD_ACCESS"
// = 尝试从两个层访问同一个数据库
print("是否在同一存储上同时使用SwiftData和Core Data?")
}
// 记录:"是否混用SwiftData + Core Data?是/否"What this tells you
这些步骤能告诉你什么
- Schema mismatch → Proceed to Pattern 1 (lightweight migration decision)
- Thread-confinement error → Proceed to Pattern 2 (async/await concurrency)
- N+1 queries → Proceed to Pattern 3 (relationship prefetching)
- SwiftData + Core Data conflict → Proceed to Pattern 4 (bridging)
- Slow after migration → Proceed to Pattern 5 (testing safety)
- 架构不匹配 → 执行模式1(轻量迁移决策)
- 线程限制错误 → 执行模式2(async/await并发)
- N+1查询 → 执行模式3(关联预获取)
- SwiftData + Core Data冲突 → 执行模式4(桥接)
- 迁移后性能缓慢 → 执行模式5(安全性测试)
MANDATORY INTERPRETATION
强制解读
Before changing ANY code, identify ONE of these:
- If crash is "Unresolvable fault" AND store/model versions differ → Schema mismatch (not user error)
- If crash mentions "different thread" AND you're using DispatchQueue → Thread-confinement (not thread-safe design)
- If performance degrades with relationship access → N+1 queries (check SQL log)
- If SwiftData and Core Data code exist together → Conflicting data layers (architectural issue)
- If migration test passes but production fails → Edge case in real data (testing gap)
在修改任何代码之前,先确定以下其中一种情况:
- 如果崩溃是"Unresolvable fault"且存储/模型版本不同 → 架构不匹配(非用户错误)
- 如果崩溃提及"different thread"且你正在使用DispatchQueue → 线程限制问题(非线程安全设计)
- 如果访问关联时性能下降 → N+1查询(检查SQL日志)
- 如果同时存在SwiftData和Core Data代码 → 数据层冲突(架构问题)
- 如果迁移测试通过但生产环境失败 → 真实数据中的边缘情况(测试缺口)
If diagnostics are contradictory or unclear
如果诊断结果矛盾或不明确
- STOP. Do NOT proceed to patterns yet
- Add print statements to every NSManagedObject access (thread check)
- Add and count SQL queries
-com.apple.CoreData.SQLDebug 1 - Establish baseline: what's actually happening vs. what you assumed
- 停止操作。先不要执行任何模式
- 在每个NSManagedObject访问处添加打印语句(线程检查)
- 添加并统计SQL查询次数
-com.apple.CoreData.SQLDebug 1 - 建立基准:实际发生的情况 vs 你的假设
Decision Tree
决策树
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)怀疑是Core Data问题?
├─ 崩溃:"Unresolvable fault"?
│ └─ 是 → 架构不匹配(存储 ≠ 应用模型)
│ ├─ 添加了新的必填字段? → 模式1a(轻量迁移)
│ ├─ 删除字段、重命名或更改类型? → 模式1b(重度迁移)
│ └─ 不知道如何修复? → 模式1c(安全性测试)
│
├─ 崩溃:"different thread"?
│ └─ 是 → 违反线程限制
│ ├─ 使用DispatchQueue处理后台工作? → 模式2a(异步上下文)
│ ├─ 混用Core Data与async/await? → 模式2b(结构化并发)
│ └─ SwiftUI @FetchRequest导致问题? → 模式2c(@FetchRequest安全性)
│
├─ 性能问题:应用变慢?
│ └─ 是 → 可能是N+1查询
│ ├─ 在循环中访问user.posts? → 模式3a(预获取)
│ ├─ 结果集过大? → 模式3b(批量大小)
│ └─ 刚添加了关联? → 模式3c(关联调优)
│
├─ 同时使用SwiftData和Core Data?
│ └─ 是 → 数据层冲突
│ ├─ 需要SwiftData不具备的Core Data功能? → 模式4a(切换到Core Data)
│ ├─ 已决定使用SwiftData? → 模式4b(继续使用SwiftData)
│ └─ 不确定使用哪个? → 模式4c(决策框架)
│
└─ 迁移在本地正常但在生产环境崩溃?
└─ 是 → 测试缺口
├─ 未使用真实数据测试? → 模式5a(生产环境测试)
├─ 架构变更影响大型数据集? → 模式5b(迁移安全性)
└─ 发布前需要验证? → 模式5c(发布前检查清单)Common Patterns
常见模式
Pattern Selection Rules (MANDATORY)
模式选择规则(强制)
Apply ONE pattern at a time, starting with diagnostics
一次仅应用一种模式,从诊断开始
- Always start with Mandatory First Steps — Identify the actual problem
- Run decision tree — Narrow to specific pattern
- Apply ONE pattern — Don't combine patterns
- Test on real device — Simulator hides issues
- Verify with migration test — Before deploying
- 始终从强制第一步开始 — 确定实际问题
- 运行决策树 — 缩小到特定模式
- 应用一种模式 — 不要组合模式
- 在真实设备上测试 — 模拟器会隐藏问题
- 通过迁移测试验证 — 部署前执行
FORBIDDEN
禁止操作
- ❌ Changing code without diagnostics
- ❌ Skipping real device testing
- ❌ Using simulator success as proof of migration safety
- ❌ Mixing multiple migration patterns
- ❌ Deploying migrations without pre-deployment verification
- ❌ 未诊断就修改代码
- ❌ 跳过真实设备测试
- ❌ 以模拟器测试成功作为迁移安全的证明
- ❌ 混用多种迁移模式
- ❌ 未进行发布前验证就部署迁移
Pattern 1a: Lightweight Migration (Simple Schema Changes)
模式1a:轻量迁移(简单架构变更)
PRINCIPLE Core Data can automatically migrate simple schemas (additive changes) without data loss if done correctly.
原则:如果操作正确,Core Data可以自动迁移简单架构(增量变更)且不会丢失数据。
✅ SAFE Lightweight Migrations
✅ 安全的轻量迁移
- Adding new optional field:
@NSManaged var nickname: String? - Adding new required field WITH default: Create attribute with default value
- Renaming entity or attribute: Use mapping model with automatic mapping
- Removing unused field: Just delete from model (data stays on disk, ignored)
- 添加新的可选字段:
@NSManaged var nickname: String? - 添加带默认值的新必填字段:创建带默认值的属性
- 重命名实体或属性:使用映射模型进行自动映射
- 删除未使用的字段:只需从模型中删除(数据仍在磁盘上,会被忽略)
❌ WRONG (Crashes production)
❌ 错误操作(导致生产环境崩溃)
swift
// 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 convertswift
// 错误:添加必填字段但未执行迁移
@NSManaged var userID: String // 必填,无默认值
// 错误:假设模拟器=生产环境
// 在模拟器中正常运行(会删除数据库),但在真实设备上崩溃
// 错误:修改字段类型
@NSManaged var createdAt: Date // 之前是String,现在改为Date
// Core Data无法自动转换✅ CORRECT (Safe lightweight migration)
✅ 正确操作(安全的轻量迁移)
swift
// 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 confidentswift
// 1. 在Xcode中:Editor → Add Model Version
// 创建新的.xcdatamodel版本文件
// 2. 在新版本中,添加带默认值的必填字段:
@NSManaged var userID: String = UUID().uuidString
// 3. 将其标记为当前模型版本:
// File Inspector → Versioned Core Data Model
// 勾选"Current Model Version"
// 4. 测试:
// 模拟旧版本:删除应用,复制旧数据库,运行新代码
// 真实应用成功加载 → 迁移成功
// 5. 确认无误后部署When this works
适用场景
- Adding optional fields (always safe)
- Adding required fields WITH default values
- Removing fields
- Renaming entities/attributes with mapping model
- 添加可选字段(始终安全)
- 添加带默认值的必填字段
- 删除字段
- 使用映射模型重命名实体/属性
When this FAILS (don't try lightweight)
不适用场景(不要尝试轻量迁移)
- Changing field type (String → Int)
- Making optional field required (data has nulls, can't convert)
- Complex relationship changes
- Custom data transformations needed
Time cost 5-10 minutes for lightweight migration setup
- 更改字段类型(String → Int)
- 将可选字段设为必填(数据中存在null,无法转换)
- 复杂的关联变更
- 需要自定义数据转换
时间成本:轻量迁移设置需5-10分钟
Pattern 1b: Heavy Migration (Complex Schema Changes)
模式1b:重度迁移(复杂架构变更)
PRINCIPLE When lightweight migration won't work, use NSEntityMigrationPolicy for custom transformation logic.
原则:当轻量迁移无法解决问题时,使用NSEntityMigrationPolicy实现自定义转换逻辑。
Use when
适用场景
- Changing field types (String → Date)
- Making optional required (need to populate existing nulls)
- Complex relationship restructuring
- Custom data transformations (e.g., split "firstName lastName" into separate fields)
- 更改字段类型(String → Date)
- 将可选字段设为必填(需要填充现有null值)
- 复杂的关联重构
- 自定义数据转换(例如,将"firstName lastName"拆分为单独字段)
Example: Convert String dates to Date objects
示例:将String日期转换为Date对象
swift
// 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 shippingswift
// 1. 创建映射模型
// File → New → Mapping Model
// 源:旧版本,目标:新版本
// 2. 创建自定义迁移策略
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)
}
// 自定义转换: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. 在映射模型检查器中:
// 设置Custom Policy Class: DateMigrationPolicy
// 4. 发布前使用真实数据进行全面测试Critical safety rules
关键安全规则
- ALWAYS backup database before testing migration
- Test migration on COPY of production data
- Verify data integrity after migration (spot checks)
- Create rollback plan if migration fails
Time cost 30-60 minutes per migration + testing
- 测试迁移前务必备份数据库
- 使用生产数据的副本测试迁移
- 迁移后验证数据完整性(抽样检查)
- 制定迁移失败时的回滚计划
时间成本:每次迁移需30-60分钟 + 测试时间
Pattern 2a: Async Context for Background Fetching
模式2a:用于后台获取的异步上下文
PRINCIPLE Core Data objects are thread-confined. Fetch on background thread, convert to lightweight representations for main thread.
原则:Core Data对象是线程限制的。在后台线程获取数据,转换为轻量表示后再传递到主线程。
❌ WRONG (Thread-confinement crash)
❌ 错误操作(线程限制崩溃)
swift
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
}
}swift
DispatchQueue.global().async {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let results = try! context.fetch(request)
DispatchQueue.main.async {
self.objects = results // ❌ 崩溃:对象在后台线程故障
}
}✅ CORRECT (Use private queue context for background work)
✅ 正确操作(使用私有队列上下文处理后台工作)
swift
// 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)")
}
}swift
// 创建后台上下文
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = viewContext
// 在后台线程获取数据
backgroundContext.perform {
do {
let results = try backgroundContext.fetch(userRequest)
// 传递到主线程前转换为轻量表示
let userIDs = results.map { $0.id } // 仅传递ID,而非完整对象
DispatchQueue.main.async {
// 在主线程中,从主上下文获取完整对象
let mainResults = try self.viewContext.fetch(request)
self.objects = mainResults
}
} catch {
print("获取数据错误:\(error)")
}
}Why this works
为什么这样可行
- Background context fetches on background thread (safe)
- Converts heavy objects to lightweight values (safe to pass to main)
- Main context fetches on main thread (safe)
- No thread-confined objects crossing thread boundaries
Time cost 10 minutes to restructure
- 后台上下文在后台线程获取数据(安全)
- 将重型对象转换为轻量值(可安全传递到主线程)
- 主上下文在主线程获取数据(安全)
- 没有线程限制的对象跨线程传递
时间成本:重构需10分钟
Pattern 2b: Structured Concurrency (async/await with Core Data)
模式2b:结构化并发(Core Data与async/await)
PRINCIPLE Use NSPersistentContainer or NSManagedObjectContext async methods for Swift Concurrency compatibility.
原则:使用NSPersistentContainer或NSManagedObjectContext的异步方法实现Swift并发兼容性。
✅ CORRECT (iOS 13+ async APIs)
✅ 正确操作(iOS 13+ 异步API)
swift
// 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-safeswift
// iOS 13+:使用async perform
let users = try await viewContext.perform {
try viewContext.fetch(userRequest)
}
// 在正确的线程执行获取操作,返回给调用者
// iOS 17+:直接使用Swift Concurrency async/await
let users = try await container.mainContext.fetch(userRequest)
// 后台工作:
let backgroundUsers = try await backgroundContext.perform {
try backgroundContext.fetch(userRequest)
}
// 在后台队列执行获取操作,线程安全❌ WRONG (Mixing Swift Concurrency with DispatchQueue)
❌ 错误操作(混用Swift Concurrency与DispatchQueue)
swift
async {
DispatchQueue.global().async {
try context.fetch(request) // ❌ Wrong thread!
}
}Time cost 5 minutes to convert from DispatchQueue to async/await
swift
async {
DispatchQueue.global().async {
try context.fetch(request) // ❌ 线程错误!
}
}时间成本:从DispatchQueue转换为async/await需5分钟
Pattern 3a: Relationship Prefetching (Prevent N+1)
模式3a:关联预获取(防止N+1查询)
PRINCIPLE Tell Core Data to fetch relationships eagerly instead of lazy-loading on access.
原则:告诉Core Data提前获取关联数据,而非在访问时延迟加载。
❌ WRONG (N+1 query pattern)
❌ 错误操作(N+1查询模式)
swift
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
}swift
let users = try context.fetch(userRequest)
for user in users {
let posts = user.posts // ❌ 每个用户都会触发一次获取!
// 1次用户查询 + N次关联查询 = 共N+1次查询
}✅ CORRECT (Prefetch relationships)
✅ 正确操作(预获取关联)
swift
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
}swift
var request = NSFetchRequest<User>(entityName: "User")
// 告诉Core Data提前获取关联数据
request.relationshipKeyPathsForPrefetching = ["posts", "comments"]
// 现在每个关联都会通过一次查询获取
let users = try context.fetch(request)
for user in users {
let posts = user.posts // ✅ 即时获取:已提前加载
// 总计:1次用户查询 + 1次所有帖子查询 = 2次查询
}Other optimization patterns
其他优化模式
swift
// 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 = trueTime cost 2-5 minutes to add prefetching
swift
// 批量大小:对大型结果集分块获取
request.fetchBatchSize = 100
// 故障行为:将故障转换为轻量快照
request.returnsObjectsAsFaults = false // 将对象保存在内存中
// 谨慎使用——大型结果集可能导致内存压力
// 去重:从关联查询中移除重复项
request.returnsDistinctResults = true时间成本:添加预获取需2-5分钟
Pattern 3b: Fetch Batch Sizing
模式3b:获取批量大小
PRINCIPLE For large result sets, fetch in batches to manage memory.
原则:对于大型结果集,分批量获取以管理内存。
Example: Scrolling through 100,000 users
示例:滚动浏览100,000个用户
swift
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
}Time cost 3 minutes to tune batch size
swift
var request = NSFetchRequest<User>(entityName: "User")
request.fetchBatchSize = 100 // 每次获取100个
// 设置排序描述符以实现稳定分页
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let results = try context.fetch(request)
// 内存占用:一次约100个用户,而非全部100,000个
for user in results {
// 访问用户0-99:在内存中
// 访问用户100:重新获取批量数据(用户100-199)
// 自动分页,内存占用最小
}时间成本:调整批量大小需3分钟
Pattern 4a: Core Data Features SwiftData Doesn't Have
模式4a:SwiftData不具备的Core Data功能
Scenario You chose SwiftData, but need features it lacks.
场景:你选择了SwiftData,但需要它不具备的功能。
SwiftData lacks
SwiftData缺少的功能
- Complex migrations (auto-migration only)
- Custom validation (before save)
- Relationship delete rules (cascade, deny, nullify)
- Direct SQL queries
- Advanced prefetching
- Faulting control
- 复杂迁移(仅支持自动迁移)
- 自定义验证(保存前)
- 关联删除规则(级联、拒绝、置空)
- 直接SQL查询
- 高级预获取
- 故障控制
When to drop to Core Data from SwiftData
何时从SwiftData切换到Core Data
- Need custom migrations
- Need validation logic
- Need complex relationship rules
- Need raw SQL for performance
- Need fault tolerance patterns
- 需要自定义迁移
- 需要验证逻辑
- 需要复杂的关联规则
- 需要使用原始SQL提升性能
- 需要容错模式
✅ CORRECT (Hybrid approach when necessary)
✅ 正确操作(必要时采用混合方式)
swift
// 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)
}CRITICAL Do NOT access the same entity from both SwiftData and Core Data simultaneously. One or the other, not both.
Time cost 30-60 minutes to create bridging layer
swift
// 对简单实体使用SwiftData
@Model final class Note {
var id: String
var title: String
}
// 对复杂操作使用Core Data
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = container.viewContext
// 使用Core Data获取数据,转换为SwiftData模型
let results = try backgroundContext.perform {
try backgroundContext.fetch(coreDataRequest)
}关键注意事项:不要同时从SwiftData和Core Data访问同一实体。二选一,不要同时使用。
时间成本:创建桥接层需30-60分钟
Pattern 4b: Stay in SwiftData (Recommended for New Projects)
模式4b:继续使用SwiftData(新项目推荐)
Scenario You're in SwiftData and wondering if you need Core Data.
场景:你正在使用SwiftData,不确定是否需要Core Data。
SwiftData provides 80% of Core Data functionality for modern apps
SwiftData为现代应用提供了80%的Core Data功能
- Type-safe models (@Model)
- Reactive queries (@Query)
- CloudKit sync (built-in)
- Automatic migrations (for simple changes)
- Proper async/await integration
- 类型安全模型(@Model)
- 响应式查询(@Query)
- CloudKit同步(内置)
- 自动迁移(针对简单变更)
- 完善的async/await集成
When SwiftData is sufficient
SwiftData足够用的场景
- Simple schemas (users, notes, todos)
- Minimal relationship complexity
- CloudKit sync needed
- iOS 17+ requirement acceptable
- No legacy Core Data code to maintain
- 简单架构(用户、笔记、待办事项)
- 关联复杂度低
- 需要CloudKit同步
- 可接受iOS 17+要求
- 无需维护旧的Core Data代码
Decision: Stay in SwiftData if you can answer YES to 3+ of these
决策:如果以下3项以上为是,继续使用SwiftData
- ✅ iOS 17+ only (no iOS 16 support needed)
- ✅ Simple relationships (1-to-many, not many-to-many)
- ✅ Standard migrations (add fields, remove fields)
- ✅ CloudKit sync beneficial
- ✅ Type safety important
- ✅ 仅支持iOS 17+(无需支持iOS 16)
- ✅ 简单关联(一对多,非多对多)
- ✅ 标准迁移(添加字段、删除字段)
- ✅ 需要CloudKit同步
- ✅ 类型安全很重要
Decision: Drop to Core Data if
决策:切换到Core Data的情况
- ❌ Need iOS 16 support (SwiftData iOS 17+ only)
- ❌ Complex relationship rules (cascade rules, constraints)
- ❌ Custom migrations required
- ❌ Raw SQL needed for performance
- ❌ Already have Core Data codebase
Time cost 0 minutes (decision only)
- ❌ 需要支持iOS 16(SwiftData仅支持iOS 17+)
- ❌ 需要复杂的关联规则(级联规则、约束)
- ❌ 需要自定义迁移
- ❌ 需要使用原始SQL提升性能
- ❌ 已有Core Data代码库
时间成本:0分钟(仅需决策)
Pattern 5a: Safe Production Testing Before Migration
模式5a:迁移前的安全生产环境测试
PRINCIPLE Never deploy a migration without testing against real data.
原则:绝不要在未使用真实数据测试的情况下部署迁移。
MANDATORY Pre-Deployment Checklist
强制发布前检查清单
swift
// 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 shippingswift
// 步骤1:导出生产环境数据库
// 从模拟器或真实设备上运行的应用中:
// ~/Library/Developer/CoreData/[AppName]/
// 复制整个[AppName].sqlite数据库
// 步骤2:创建迁移测试
@Test func testProductionDataMigration() throws {
// 将生产环境数据库复制到测试位置
let testDB = tempDirectory.appendingPathComponent("test.sqlite")
try FileManager.default.copyItem(from: prodDatabase, to: testDB)
// 尝试迁移
var config = ModelConfiguration(url: testDB, isStoredInMemory: false)
let container = try ModelContainer(for: User.self, configurations: [config])
// 验证数据完整性
let context = container.mainContext
let allUsers = try context.fetch(FetchDescriptor<User>())
// 抽样检查:验证特定记录是否正确迁移
guard let user1 = allUsers.first(where: { $0.id == "test-id-1" }) else {
throw MigrationError.missingUser
}
// 检查派生数据是否正确
XCTAssertEqual(user1.name, "预期名称")
XCTAssertNotNil(user1.createdAt)
// 检查关联
XCTAssertEqual(user1.posts.count, expectedPostCount)
}
// 步骤3:使用真实生产环境数据运行测试
// 通过✓后再发布Safety rules
安全规则
- ❌ NEVER test migrations with simulator (simulator deletes DB)
- ✅ ALWAYS test with copy of real production data
- ✅ ALWAYS verify spot checks (specific records)
- ✅ ALWAYS check relationships loaded correctly
- ✅ ALWAYS have rollback plan documented
Time cost 15-30 minutes to create migration test
- ❌ 绝不要在模拟器中测试迁移(模拟器会删除数据库)
- ✅ 始终使用真实生产环境数据的副本测试
- ✅ 始终进行抽样检查(特定记录)
- ✅ 始终检查关联是否正确加载
- ✅ 始终记录回滚计划
时间成本:创建迁移测试需15-30分钟
Pattern 5c: Pre-Deployment Verification Checklist
模式5c:发布前验证检查清单
MANDATORY before shipping ANY Core Data change
发布任何Core Data变更前的强制检查
- Did you create a new .xcdatamodel version? (Not just editing the existing one)
- Does the new version have a mapping model if needed?
- Did you test migration with real production data? (Not simulator)
- Did you verify 5+ specific records migrated correctly?
- Did you check relationships loaded?
- Did you test on real device (oldest supported)?
- Does app launch without crashing? (Fresh install)
- Does app launch with old data? (Migration path)
- Is rollback plan documented? (In case production fails)
- 你是否创建了新的.xcdatamodel版本?(而非仅编辑现有版本)
- 必要时新版本是否有映射模型?
- 你是否使用真实生产环境数据测试了迁移?(而非模拟器)
- 你是否验证了5个以上特定记录的迁移正确性?
- 你是否检查了关联是否正确加载?
- 你是否在真实设备(最旧支持版本)上测试过?
- 应用能否正常启动?(全新安装)
- 应用能否加载旧数据?(迁移路径)
- 是否记录了回滚计划?(以防生产环境失败)
If you answer NO to any item
如果有任何一项为否
- ❌ DO NOT SHIP
- Go back, fix the issue, re-test
- One "NO" = data loss risk
Time cost 5 minutes checklist
- ❌ 不要发布
- 返回,修复问题,重新测试
- 一个"否"就意味着存在数据丢失风险
时间成本:检查清单需5分钟
Quick Reference Table
快速参考表
| Issue | Check | Fix |
|---|---|---|
| "Unresolvable fault" crash | Do store/model versions match? | Create .xcdatamodel version + mapping model |
| "Different thread" crash | Is fetch happening on main thread? | Use private queue context for background work |
| App became slow | Are relationships being prefetched? | Add relationshipKeyPathsForPrefetching |
| N+1 query performance | Check | Add prefetching or convert to lightweight representation |
| SwiftData needs Core Data features | Do you need custom migrations? | Use Core Data NSEntityMigrationPolicy |
| Not sure about SwiftData vs. Core Data | Do you need iOS 16 support? | Use Core Data for iOS 16, SwiftData for iOS 17+ |
| Migration test works, production fails | Did you test with real data? | Create migration test with production database copy |
| 问题 | 检查内容 | 修复方案 |
|---|---|---|
| "Unresolvable fault"崩溃 | 存储/模型版本是否匹配? | 创建.xcdatamodel版本 + 映射模型 |
| "Different thread"崩溃 | 获取操作是否在主线程执行? | 使用私有队列上下文处理后台工作 |
| 应用变慢 | 是否预获取了关联数据? | 添加relationshipKeyPathsForPrefetching |
| N+1查询性能问题 | 检查 | 添加预获取或转换为轻量表示 |
| SwiftData需要Core Data功能 | 是否需要自定义迁移? | 使用Core Data NSEntityMigrationPolicy |
| 不确定使用SwiftData还是Core Data | 是否需要支持iOS 16? | iOS 16使用Core Data,iOS 17+使用SwiftData |
| 迁移测试正常但生产环境崩溃 | 是否使用真实数据测试? | 使用生产环境数据库副本创建迁移测试 |
When You're Stuck After 30 Minutes
当你卡壳30分钟后
If you've spent >30 minutes and the Core Data issue persists:
如果你已经花费了>30分钟但Core Data问题仍未解决:
STOP. You either
停止操作。你可能
- Skipped mandatory diagnostics (most common)
- Misidentified the actual problem
- Applied wrong pattern for your symptom
- Haven't tested on real device/real data
- Have edge case requiring custom NSEntityMigrationPolicy
- 跳过了强制诊断(最常见)
- 错误识别了实际问题
- 针对症状应用了错误的模式
- 未在真实设备/真实数据上测试
- 遇到了需要自定义NSEntityMigrationPolicy的边缘情况
MANDATORY checklist before claiming "skill didn't work"
声称"方法无效"前的强制检查清单
- I ran all Mandatory First Steps diagnostics
- I identified the problem type (schema, concurrency, performance, bridging, testing)
- I checked Core Data SQL debug logs ()
-com.apple.CoreData.SQLDebug 1 - I tested on real device with real data (not simulator)
- I applied the FIRST matching pattern from Decision Tree
- I created a migration test if schema changed
- I verified at least 3 specific records migrated correctly
- I have a rollback plan documented
- 我已执行所有强制第一步诊断
- 我已确定问题类型(架构、并发、性能、桥接、测试)
- 我已检查Core Data SQL调试日志()
-com.apple.CoreData.SQLDebug 1 - 我已在真实设备上使用真实数据测试(而非模拟器)
- 我已应用决策树中第一个匹配的模式
- 如果架构变更,我已创建迁移测试
- 我已验证至少3个特定记录的迁移正确性
- 我已记录回滚计划
If ALL boxes are checked and still broken
如果所有项都已勾选但问题仍存在
- You need custom NSEntityMigrationPolicy (not covered by basic patterns)
- Time cost: 60-90 minutes for complex migration
- Ask: "What data transformation is actually needed?" and implement custom policy
- 你需要自定义NSEntityMigrationPolicy(基本模式未涵盖)
- 时间成本:60-90分钟用于复杂迁移
- 思考:"实际需要什么数据转换?"并实现自定义策略
Time cost transparency
时间成本透明
- Pattern 1 (lightweight migration): 5-10 minutes
- Pattern 1 (heavy migration with custom policy): 60-90 minutes
- Pattern 2 (concurrency): 5-10 minutes
- Pattern 3 (prefetching): 2-5 minutes
- Pattern 4 (bridging): 30-60 minutes
- Pattern 5 (testing): 15-30 minutes
- 模式1(轻量迁移):5-10分钟
- 模式1(带自定义策略的重度迁移):60-90分钟
- 模式2(并发):5-10分钟
- 模式3(预获取):2-5分钟
- 模式4(桥接):30-60分钟
- 模式5(测试):15-30分钟
Common Mistakes
常见错误
❌ Testing migration in simulator only
- Simulator deletes database on rebuild, hiding schema mismatches
- Fix: ALWAYS test on real device or with production database copy
❌ Assuming default values protect against data loss
- Default values only work for new records, not existing data
- Fix: Use NSEntityMigrationPolicy for existing data
❌ Accessing Core Data objects across threads without conversion
- Objects are thread-confined, can't cross thread boundaries
- Fix: Convert to lightweight representations before passing to other threads
❌ Not realizing relationship access = database query
- triggers a fetch for EACH user (N+1)
user.posts - Fix: Use relationshipKeyPathsForPrefetching or extract IDs first
❌ Mixing SwiftData and Core Data on same store
- Both layers can't access the same database simultaneously
- Fix: Choose one layer, or use hybrid approach with separate entities
❌ Deploying migrations without pre-deployment testing
- Edge cases in production data cause crashes
- Fix: MANDATORY migration test with real production data
❌ Rationalizing: "I'll just delete the data"
- ❌ FORBIDDEN: Users won't appreciate losing their data
- Users uninstall and leave bad reviews
- Fix: Invest in safe migration testing
❌ 仅在模拟器中测试迁移
- 模拟器每次重建都会删除数据库,隐藏架构不匹配问题
- 修复:始终在真实设备上或使用生产环境数据库副本测试
❌ 假设默认值能防止数据丢失
- 默认值仅对新记录有效,对现有数据无效
- 修复:对现有数据使用NSEntityMigrationPolicy
❌ 未转换就跨线程访问Core Data对象
- 对象是线程限制的,不能跨线程传递
- 修复:传递到其他线程前转换为轻量表示
❌ 未意识到关联访问=数据库查询
- 会为每个用户触发一次获取(N+1)
user.posts - 修复:使用relationshipKeyPathsForPrefetching或先提取ID
❌ 在同一存储上混用SwiftData和Core Data
- 两个层不能同时访问同一个数据库
- 修复:选择其中一个层,或对不同实体使用混合方式
❌ 未进行发布前测试就部署迁移
- 生产环境数据中的边缘情况会导致崩溃
- 修复:强制使用真实生产环境数据进行迁移测试
❌ 合理化:"我只需删除数据"
- ❌ 禁止:用户不会接受丢失数据
- 用户会卸载应用并留下差评
- 修复:投入时间进行安全迁移测试
Production Crisis Pressure: Defending Safe Migration Patterns
生产环境危机压力:捍卫安全迁移模式
The Problem
问题
Under production crisis pressure, you'll face requests to:
- "Users are crashing - just delete the database and start fresh"
- "Migration is taking too long - skip the testing and ship it"
- "We can't wait 2 days for proper migration - hack it together"
- "Schema mismatch? Just force-create a new store"
These sound like pragmatic crisis responses. But they cause data loss and permanent user trust damage. Your job: defend using data safety principles and customer impact, not fear of pressure.
在生产环境危机压力下,你会面临以下请求:
- "用户正在崩溃——只需删除数据库并重新开始"
- "迁移花费时间太长——跳过测试直接发布"
- "我们不能等2天进行正确迁移——随便解决一下"
- "架构不匹配?直接强制创建新存储"
这些听起来像是务实的危机应对方案。但它们会导致数据丢失和永久的用户信任损害。你的职责:用数据安全原则和客户影响来捍卫正确做法,而非屈服于压力。
Red Flags — PM/Manager Requests That Cause Data Loss
危险信号——导致数据丢失的产品经理/经理请求
If you hear ANY of these during a production crisis, STOP and reference this skill:
- ❌ "Delete the persistent store and start fresh" – Users lose ALL their data permanently
- ❌ "Force lightweight migration without testing" – High risk of data corruption in production
- ❌ "Skip migration and create new store" – Abandons existing user data
- ❌ "We'll fix data issues after launch" – Impossible to recover lost/corrupted data
- ❌ "Just ship it, we can handle support tickets" – Data loss creates permanent user churn
- ❌ "Test on simulator is enough" – Simulator deletes database on rebuild, hides schema mismatches
如果你在生产环境危机中听到以下任何一种说法,停止操作并参考本方法:
- ❌ "删除持久化存储并重新开始" – 用户永久丢失所有数据
- ❌ "未测试就强制轻量迁移" – 生产环境数据损坏的高风险
- ❌ "跳过迁移并创建新存储" – 放弃现有用户数据
- ❌ "我们会在发布后修复数据问题" – 丢失/损坏的数据无法恢复
- ❌ "直接发布,我们可以处理支持工单" – 数据丢失会导致永久用户流失
- ❌ "模拟器测试足够了" – 模拟器每次重建都会删除数据库,隐藏架构不匹配问题
How to Push Back Professionally
如何专业地反驳
Step 1: Quantify the Customer Impact
步骤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)""我希望尽快解决崩溃问题,但让我说明删除存储的后果:
当前情况:
- 10,000名活跃用户有数据
- 每位用户平均50个项目(总计500,000条记录)
- 用户的数据积累了1周到2年不等
如果我们删除存储:
- 10,000名用户在下次应用启动时会丢失所有数据
- 卸载率:60-80%(数据丢失后的行业标准)
- App Store评价:预计会出现大量提及数据丢失的1星评价
- 恢复:不可能——数据永久丢失
安全替代方案:
- 使用生产环境数据副本在真实设备上测试迁移(2-4小时)
- 部署能保留用户数据的迁移
- 卸载率:<5%(标准更新流失率)"Step 2: Demonstrate the Risk
步骤2:展示风险
Show the PM/manager what happens:
- Copy production database from device backup
- Run proposed "quick fix" (delete store)
- Show: All user data gone permanently
- Show alternative: Safe migration preserving data
- Time comparison: 30 min hack vs. 2-4 hour safe migration
向产品经理/经理展示后果:
- 从设备备份中复制生产环境数据库
- 运行提议的"快速修复"(删除存储)
- 展示:所有用户数据永久丢失
- 展示替代方案:安全迁移保留数据
- 时间对比:30分钟的权宜之计 vs 2-4小时的安全迁移
Reference
参考依据
- "Users don't forgive data loss" (App Store review patterns)
- Migration testing on real device prevents 95% of production crashes
- Schema mismatch crashes affect 100% of existing users
- "用户不会原谅数据丢失"(App Store评价模式)
- 在真实设备上测试迁移可防止95%的生产环境崩溃
- 架构不匹配崩溃会影响100%的现有用户
Step 3: Offer Compromise
步骤3:提供折中方案
"I can get us through this crisis while protecting user data:"我可以在保护用户数据的同时解决这场危机:Fast track (4 hours total)
快速通道(总计4小时)
- Copy production database from TestFlight user (30 min)
- Write and test migration on real device copy (2 hours)
- Submit build with tested migration (30 min)
- Monitor first 100 updates for crashes (1 hour)
- 从TestFlight用户处复制生产环境数据库(30分钟)
- 在真实设备副本上编写并测试迁移(2小时)
- 提交经过测试的迁移版本(30分钟)
- 监控前100次更新是否崩溃(1小时)
Fallback if migration fails
迁移失败后的 fallback 方案
- 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)"
undefined- 准备好"删除存储"版本作为备选方案B
- 仅在迁移100%失败时部署
- 主动向用户告知数据丢失情况
这种方法:
- 首先尝试安全路径(保护用户数据)
- 有紧急备选方案(不阻碍进度)
- 提供真实的时间线
undefinedStep 4: Document the Decision
步骤4:记录决策
If overruled (PM insists on deleting store):
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"如果被否决(产品经理坚持删除存储):
给产品经理+团队的Slack消息:
"生产环境危机:架构不匹配导致现有用户崩溃。
产品经理决策:删除持久化存储以立即解决问题。
影响评估:
- 10,000名用户在下次应用启动时永久丢失所有数据
- 预计卸载率:60-80%(基于数据丢失模式)
- App Store评价损害:高风险出现1星评价
- 客户支持:预计会收到大量数据丢失投诉
- 恢复:不可能——删除的数据无法恢复
因时间紧迫,我提出的(4小时安全迁移)替代方案被拒绝。
我主动标记此决策,以便我们:
1. 让支持团队准备好处理数据丢失投诉
2. 起草对预期负面评价的App Store回复
3. 考虑在发布前向用户告知数据丢失情况"Why this works
为什么这样有效
- You're not questioning their judgment under pressure
- You're quantifying user impact (business consequences)
- You're offering a solution with honest timeline
- You're providing fallback option (not blocking progress)
- You're documenting the decision (protects you post-launch)
- 你没有质疑他们在压力下的判断
- 你量化了用户影响(业务后果)
- 你提供了带有真实时间线的解决方案
- 你提供了备选方案(不阻碍进度)
- 你记录了决策(发布后保护你)
Real-World Example: Production Crash (500K Active Users)
真实案例:生产环境崩溃(500K活跃用户)
Scenario
场景
- Production app crashing for 100% of users after update
- Error: "The model used to open the store is incompatible with the one used to create the store"
- CTO says: "Delete the database and ship hotfix in 2 hours"
- 500,000 active users with average 6 months of data each
- 生产环境应用更新后100%的用户崩溃
- 错误:"用于打开存储的模型与创建存储的模型不兼容"
- CTO说:"删除数据库并在2小时内发布热修复"
- 500,000名活跃用户,每位用户平均有6个月的数据
What to do
正确做法
swift
// ❌ 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)")
}swift
// ❌ 错误 - 删除所有用户数据(CTO的请求)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
let storeURL = /* 持久化存储URL */
try? FileManager.default.removeItem(at: storeURL) // 500K用户丢失数据
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: nil)
// ✅ 正确 - 安全轻量迁移(4小时时间线)
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: options)
// 迁移成功 - 用户数据保留
} catch {
// 迁移失败 — 此时再考虑在告知用户的情况下删除数据
print("迁移错误:\(error)")
}In the meeting, show
在会议中展示
- Schema version mismatch causing crash
- Lightweight migration can fix automatically
- Testing on production database copy (2 hours)
- Time comparison: 2 hours (safe) vs. immediate (data loss)
Time estimate 4 hours total (2 hours migration testing, 2 hours build/deploy)
- 架构版本不匹配导致崩溃
- 轻量迁移可自动修复
- 使用生产环境数据库副本测试(2小时)
- 时间对比:2小时(安全) vs 立即(数据丢失)
时间估算 总计4小时(2小时迁移测试,2小时构建/部署)
Result
结果
- Honest timeline manages expectations
- Safe migration preserves 500K users' data
- Uninstall rate: 3% (standard update churn)
- App Store reviews: No data loss complaints
- 真实时间线管理了预期
- 安全迁移保留了500K用户的数据
- 卸载率:3%(标准更新流失率)
- App Store评价:没有数据丢失投诉
Alternative if migration truly impossible
如果迁移确实不可能的替代方案
- Document why migration failed
- Communicate data loss to users proactively
- Provide export feature in next version
- 记录迁移失败的原因
- 主动向用户告知数据丢失情况
- 在下次版本中提供导出功能
When to Accept Data Loss (Even If You Disagree)
何时接受数据丢失(即使你不同意)
Sometimes data loss is the only option. Accept if:
- Migration is genuinely impossible (tried on production data copy)
- PM/CTO understand 60-80% expected uninstall rate
- Team commits to user communication about data loss
- You've documented technical reasons migration failed
有时数据丢失是唯一选择。如果满足以下条件,可以接受:
- 迁移确实不可能(已在生产环境数据副本上尝试)
- 产品经理/CTO理解预计60-80%的卸载率
- 团队承诺向用户告知数据丢失情况
- 你已记录迁移失败的技术原因
Document in Slack
在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"This protects you and shows you exhausted safe options first.
"生产环境危机:在4小时测试后,迁移在生产环境数据副本上失败。
技术细节:
- 尝试轻量迁移:失败,错误[具体错误]
- 尝试带映射模型的重度迁移:失败,错误[具体错误]
- 根本原因:[特定架构不兼容]
数据丢失决策:
- 不存在安全迁移路径
- 产品经理批准删除持久化存储方案
- 预计影响:60-80%卸载率(500K → 100-200K用户)
缓解计划:
- 在下一次架构变更前添加数据导出功能
- 通过应用内消息向用户告知数据丢失情况
- 让支持团队准备好处理投诉
- 发布后监控卸载率"这能保护你,并表明你已先穷尽了所有安全选项。
Real-World Impact
真实影响
Before Core Data debugging 3-8 hours per issue
- Crashes in production (schema mismatch)
- Performance gradually degrades (N+1 queries)
- Thread errors in background operations
- Data corruption from unsafe migrations
- Customer trust damaged
After 30 minutes to 2 hours with systematic diagnosis
- Identify problem type with diagnostics (5 min)
- Apply correct pattern (5-10 min)
- Test on real device (varies)
- Deploy with confidence
Key insight Core Data has well-established patterns for every common issue. The problem is developers don't know which pattern applies to their symptom.
Last Updated: 2025-11-30
Status: TDD-tested with pressure scenarios
Framework: Core Data (Foundation framework)
Complements: SwiftData skill (understanding relationship to Core Data)
之前 每个Core Data调试问题需3-8小时
- 生产环境崩溃(架构不匹配)
- 性能逐渐下降(N+1查询)
- 后台操作中的线程错误
- 不安全迁移导致的数据损坏
- 客户信任受损
之后 系统化诊断需30分钟到2小时
- 用诊断确定问题类型(5分钟)
- 应用正确模式(5-10分钟)
- 在真实设备上测试(时间不等)
- 自信地部署
关键见解:Core Data对每个常见问题都有成熟的模式。问题在于开发者不知道哪种模式适用于他们的症状。
最后更新:2025-11-30
状态:经过TDD测试和压力场景验证
框架:Core Data(Foundation框架)
补充:SwiftData方法(理解与Core Data的关系)