关于 axiom-swiftdata-migration-diag
面向开发者的 SwiftData 迁移故障诊断技能,将常见错误对应到架构、关系、数据转换或测试问题,并提供检查步骤与修复模式。
主要功能
- 01错误与原因诊断清单
- 02架构版本和模型检查
- 03关系反向配置指导
- 04迁移数据完整性验证
- 05真机迁移测试
适用场景
- 01诊断架构变更后的崩溃
- 02排查迁移后的记录丢失
- 03检查关系断裂和 faulting 错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftdata-migration-diag安装后在 Claude Code 中输入/axiom-swiftdata-migration-diag确认已加载。
详细介绍
SwiftData 迁移诊断
概述
SwiftData 迁移失败会导致生产环境崩溃、数据丢失、关系损坏,或仅在模拟器中成功。核心原则 90% 的迁移失败源于 VersionedSchema 中缺少模型、关系反向关系问题,或迁移路径未经测试,而非 SwiftData 本身的缺陷。
危险信号——怀疑是 SwiftData 迁移问题
如果遇到以下任一情况,请怀疑迁移配置有问题:
- 更改架构后,应用启动时崩溃
- 出现 "Expected only Arrays for Relationships" 错误
- 出现 "The model used to open the store is incompatible with the one used to create the store"
- 出现 "Failed to fulfill faulting for [relationship]"
- 迁移在模拟器中正常,但在真实设备上崩溃
- 迁移前数据存在,迁移后却消失
- 迁移后关系损坏(本不应为 nil 的值却为 nil)
- ❌ 禁止 “SwiftData 迁移有问题,我们应该改用 Core Data”
- SwiftData 已在生产应用中处理了数百万次迁移
- 架构不匹配和关系错误始终是配置问题,而非框架问题
- 不要为问题找借口——应进行诊断
关键区别 模拟器会在每次重新构建时删除数据库,从而掩盖架构不匹配问题。真实设备会保留持久化数据库,并在架构不匹配时立即崩溃。必须:发布前在装有真实数据的真实设备上测试迁移。
必须执行的第一步
始终先运行以下内容(再更改代码):
// 1. Identify the crash/issue type
// Screenshot the crash message and note:
// - "Expected only Arrays" = relationship inverse missing
// - "incompatible model" = schema version mismatch
// - "Failed to fulfill faulting" = relationship integrity broken
// - Simulator works, device crashes = untested migration path
// Record: "Error type: [exact message]"
// 2. Check schema version configuration
// In your migration plan:
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
// ✅ VERIFY: All versions in order?
// ✅ VERIFY: Latest version matches container?
[SchemaV1.self, SchemaV2.self, SchemaV3.self]
}
static var stages: [MigrationStage] {
// ✅ VERIFY: Migration stages match schema transitions?
[migrateV1toV2, migrateV2toV3]
}
}
// In your app:
let schema = Schema(versionedSchema: SchemaV3.self) // ✅ VERIFY: Matches latest in plan?
let container = try ModelContainer(
for: schema,
migrationPlan: MigrationPlan.self // ✅ VERIFY: Plan is registered?
)
// Record: "Schema version: latest is [version]"
// 3. Check all models included in VersionedSchema
enum SchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] {
// ✅ VERIFY: Are ALL models listed? (even unchanged ones)
[Note.self, Folder.self, Tag.self]
}
}
// Record: "Missing models? Yes/no"
// 4. Check relationship inverse declarations
@Model
final class Note {
@Relationship(deleteRule: .nullify, inverse: \Folder.notes) // ✅ VERIFY: inverse specified?
var folder: Folder?
@Relationship(deleteRule: .nullify, inverse: \Tag.notes) // ✅ VERIFY: inverse specified?
var tags: [Tag] = []
}
// Record: "Relationship inverses: all specified? Yes/no"
// 5. Enable SwiftData debug logging
// In Xcode scheme, add argument:
// -com.apple.coredata.swiftdata.debug 1
// Run and check Console for SQL queries
// Record: "Debug log shows: [what you see]"
这些信息说明什么
- "Expected only Arrays for Relationships" → 转到模式 1(修复关系反向关系)
- "incompatible model" → 转到模式 2(架构版本不匹配)
- VersionedSchema 中缺少模型 → 转到模式 3(完整架构快照)
- 模拟器正常,设备崩溃 → 转到模式 4(迁移测试)
- 迁移后数据丢失 → 转到模式 5(误用 willMigrate/didMigrate)
必须进行的判断
更改任何代码之前,确定以下情况中的一种:
- 如果错误为 "Expected only Arrays",且缺少关系反向关系 → 关系配置问题
- 如果错误提到 "incompatible",且架构版本不匹配 → 版本不匹配
- 如果 VersionedSchema 中缺少模型 → 架构快照不完整
- 如果模拟器迁移成功但设备失败 → 迁移路径未经测试
- 如果迁移前数据存在、迁移后却不存在 → 违反了 willMigrate/didMigrate 的限制
如果诊断结果相互矛盾或不明确
- 停止。暂时不要继续查看模式
- 添加
-com.apple.coredata.swiftdata.debug 1并检查 SQL 输出 - 检查文件系统:.sqlite 文件是否存在?大小是多少?
- 建立基线:实际发生了什么,与原先的假设有什么不同
验证迁移是否成功完成
当迁移似乎已无错误地完成,但你想验证数据完整性时,请使用本节。
快速验证清单
迁移运行后未发生崩溃:
// 1. Verify record count matches pre-migration
let context = container.mainContext
let postMigrationCount = try context.fetch(FetchDescriptor<Note>()).count
print("Post-migration count: \(postMigrationCount)")
// Compare to pre-migration count
// 2. Spot-check specific records
let sampleNote = try context.fetch(
FetchDescriptor<Note>(predicate: #Predicate { $0.id == "known-test-id" })
).first
print("Sample note title: \(sampleNote?.title ?? "MISSING")")
// 3. Verify relationships intact
if let note = sampleNote {
print("Folder relationship: \(note.folder != nil ? "✓" : "✗")")
print("Tags count: \(note.tags.count)")
// Verify inverse relationships
if let folder = note.folder {
let folderHasNote = folder.notes.contains { $0.id == note.id }
print("Inverse relationship: \(folderHasNote ? "✓" : "✗")")
}
}
// 4. Check for orphaned data
let orphanedNotes = try context.fetch(
FetchDescriptor<Note>(predicate: #Predicate { $0.folder == nil })
)
print("Orphaned notes (should be 0 if cascade delete worked): \(orphanedNotes.count)")
成功迁移的表现
控制台输出:
Post-migration count: 1523 // Matches pre-migration
Sample note title: Test Note // Not "MISSING"
Folder relationship: ✓
Tags count: 3
Inverse relationship: ✓
Orphaned notes: 0
如果看到:
- 记录数不同 → 数据丢失(检查 willMigrate 逻辑)
- 出现 "MISSING" 记录 → 架构不匹配或提取错误
- 关系为 nil → 反向关系配置或预取问题
- 孤立记录数 >0 → 级联删除规则未生效
具体修复方法见下方的模式。
决策树
SwiftData migration problem suspected?
├─ Error: "Expected only Arrays for Relationships"?
│ └─ YES → Relationship inverse missing
│ ├─ Many-to-many relationship? → Pattern 1a (explicit inverse)
│ ├─ One-to-many relationship? → Pattern 1b (verify both sides)
│ └─ iOS 17.0 alphabetical bug? → Pattern 1c (default value workaround)
│
├─ Error: "incompatible model" or crash on launch?
│ └─ YES → Schema version mismatch
│ ├─ Latest schema not in plan? → Pattern 2a (add to schemas array)
│ ├─ Migration stage missing? → Pattern 2b (add stage)
│ └─ Container using wrong schema? → Pattern 2c (verify version)
│
├─ Migration runs but data missing?
│ └─ YES → Data loss during migration
│ ├─ Used didMigrate to access old models? → Pattern 3a (use willMigrate)
│ ├─ Forgot to save in willMigrate? → Pattern 3b (add context.save())
│ └─ Custom migration logic wrong? → Pattern 3c (debug transformation)
│
├─ Works in simulator but crashes on device?
│ └─ YES → Untested migration path
│ ├─ Never tested on real device? → Pattern 4a (real device testing)
│ ├─ Never tested upgrade path? → Pattern 4b (test v1 → v2 upgrade)
│ └─ Production data differs from test? → Pattern 4c (test with prod data)
│
└─ Relationships nil after migration?
└─ YES → Relationship integrity broken
├─ Forgot to prefetch relationships? → Pattern 5a (add prefetching)
├─ Inverse relationship wrong? → Pattern 5b (fix inverse)
└─ Delete rule caused cascade? → Pattern 5c (check delete rules)
常见模式
模式 1a:修复 "Expected only Arrays for Relationships"
原则 多对多关系需要显式声明反向关系。
❌ 错误(会导致 "Expected only Arrays" 错误)
@Model
final class Note {
var tags: [Tag] = [] // ❌ Missing inverse
}
@Model
final class Tag {
var notes: [Note] = [] // ❌ Missing inverse
}
✅ 正确(显式声明反向关系)
@Model
final class Note {
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = [] // ✅ Inverse specified
}
@Model
final class Tag {
@Relationship(deleteRule: .nullify, inverse: \Note.tags)
var notes: [Note] = [] // ✅ Inverse specified
}
原因 SwiftData 要求为多对多关系显式声明反向关系,才能正确创建联结表。
耗时 添加反向关系声明需要 2 分钟
模式 1b:iOS 17.0 字母顺序 Bug 的变通方案
原则 在 iOS 17.0 中,如果模型名称按字母顺序排列,多对多关系可能会失败。
❌ 错误(在 iOS 17.0 中崩溃)
@Model
final class Actor {
@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var movies: [Movie] // ❌ No default value
}
@Model
final class Movie {
@Relationship(deleteRule: .nullify, inverse: \Actor.movies)
var actors: [Actor] // ❌ No default value
}
// Crashes if "Actor" < "Movie" alphabetically
✅ 正确(适用于 iOS 17.0 及更高版本)
@Model
final class Actor {
@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var movies: [Movie] = [] // ✅ Default value
}
@Model
final class Movie {
@Relationship(deleteRule: .nullify, inverse: \Actor.movies)
var actors: [Actor] = [] // ✅ Default value
}
修复版本 iOS 17.1 及更高版本
耗时 添加默认值需要 1 分钟
模式 2a:架构版本不匹配
原则 迁移计划的 schemas 数组必须按顺序包含所有版本。
❌ 错误(缺少版本会导致崩溃)
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV3.self] // ❌ Missing V2!
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3] // References V2 but not in schemas
}
}
✅ 正确(按顺序包含所有版本)
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self, SchemaV3.self] // ✅ All versions
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
}
耗时 添加缺失版本需要 2 分钟
模式 3a:误用 willMigrate/didMigrate 导致数据丢失
原则 旧模型只能在 willMigrate 中访问,新模型只能在 didMigrate 中访问。
❌ 错误(尝试在 didMigrate 中访问旧模型)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: nil,
didMigrate: { context in
// ❌ CRASH: SchemaV1.Note doesn't exist here
let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
// Data lost because transformation never ran
}
)
✅ 正确(在 willMigrate 中进行转换)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// ✅ SchemaV1.Note exists here
let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
// Transform data while old models still accessible
for note in oldNotes {
note.transformed = transformLogic(note.oldValue)
}
try context.save() // ✅ Save before migration completes
},
didMigrate: nil
)
耗时 将逻辑移至正确的闭包需要 5 分钟
模式 4a:在真实设备上测试
原则 模拟器在重新构建时会删除数据库。真实设备会保留持久化数据库。
测试工作流
# 1. Install v1 on real device
# Build with SchemaV1 as current version
# Run app, create sample data (100+ records)
# 2. Verify data exists
# Check app: should see 100+ records
# 3. Install v2 with migration
# Build with SchemaV2 as current version + migration plan
# Install over existing app (don't delete)
# 4. Verify migration succeeded
# App launches without crash
# Data still exists (100+ records)
# Relationships intact
迁移测试代码
import Testing
import SwiftData
@Test func testMigrationOnRealDevice() throws {
// This test MUST run on real device, not simulator
#if targetEnvironment(simulator)
throw XCTSkip("Migration test requires real device")
#endif
let container = try ModelContainer(
for: Schema(versionedSchema: SchemaV2.self),
migrationPlan: MigrationPlan.self
)
let context = container.mainContext
let notes = try context.fetch(FetchDescriptor<SchemaV2.Note>())
// Verify data preserved
#expect(notes.count > 0)
// Verify relationships
for note in notes {
if note.folder != nil {
#expect(note.folder?.notes.contains { $0.id == note.id } == true)
}
}
}
耗时 在真实设备上测试需要 15 分钟
模式 5a:预取关系以保持完整性
原则 在迁移期间主动获取关系,避免触发故障错误。
❌ 错误(关系可能触发故障并导致问题)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
let notes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
for note in notes {
// ❌ May trigger fault, relationship not loaded
let folderName = note.folder?.name
}
},
didMigrate: nil
)
✅ 正确(预取关系)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
var fetchDesc = FetchDescriptor<SchemaV1.Note>()
// ✅ Prefetch relationships
fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]
let notes = try context.fetch(fetchDesc)
for note in notes {
// ✅ Relationships already loaded
let folderName = note.folder?.name
let tagCount = note.tags.count
}
try context.save()
},
didMigrate: nil
)
耗时 添加预取操作需要 3 分钟
快速参考:错误 → 修复方法对照表
| 错误消息 | 根本原因 | 修复方法 | 耗时 |
|---|---|---|---|
| "Expected only Arrays for Relationships" | 缺少多对多关系的反向关系 | 在两侧添加 @Relationship(inverse:) | 2 分钟 |
| "The model used to open the store is incompatible" | 架构版本不匹配 | 在 schemas 数组中添加缺失版本 | 2 分钟 |
| "Failed to fulfill faulting for [relationship]" | 未预取关系 | 添加 relationshipKeyPathsForPrefetching | 3 分钟 |
| 架构更改后应用崩溃 | VersionedSchema 中缺少模型 | 在 models 数组中包含所有模型 | 2 分钟 |
| 迁移后数据丢失 | 在错误的闭包中进行转换 | 将逻辑从 didMigrate 移至 willMigrate | 5 分钟 |
| 模拟器运行正常,设备崩溃 | 未测试迁移路径 | 使用真实数据在真实设备上测试 | 15 分钟 |
| 迁移后关系为 nil | 反向关系错误 | 修复 @Relationship(inverse:) 键路径 | 3 分钟 |
调试检查清单
迁移失败时,请核实以下所有事项:
- 所有模型都已包含在
VersionedSchema.models数组中 - 所有架构版本都已包含在
SchemaMigrationPlan.schemas数组中 - 迁移阶段与架构转换相匹配(V1→V2、V2→V3)
- 多对多关系的两侧都显式声明了
inverse: - 容器使用正确的最新架构版本初始化
- 在
ModelContainer初始化时注册了迁移计划 - 已在真实设备上测试(而不只是模拟器)
- 已测试升级路径(v1 → v2),而不只是全新安装
- 已启用 SwiftData 调试日志记录(
-com.apple.coredata.swiftdata.debug 1) - 数据转换逻辑位于
willMigrate中(而不是didMigrate)
卡住超过 30 分钟时
如果你已花费超过 30 分钟但迁移问题仍未解决:
停下来。你可能是
- 跳过了强制诊断步骤(最常见)
- 错误识别了实际问题
- 针对你的症状套用了错误模式
- 尚未在真实设备/真实数据上测试
- 遇到了需要两阶段迁移的复杂边缘情况
声称“技能不起作用”之前的必查清单
- 我已运行所有“强制首要步骤”中的诊断
- 我已确定问题类型(关系、架构不匹配、数据丢失、测试缺失)
- 我已启用 SwiftData 调试日志记录并检查 SQL 输出
- 我已在使用真实数据的真实设备上测试(不是模拟器)
- 我已应用决策树中第一个匹配的模式
- 我已确认 VersionedSchema 中包含所有模型
- 我已检查关系的反向关系声明
如果所有复选框都已勾选,但问题依然存在
- 你需要进行两阶段迁移(详见
axiom-swiftdata-migration技能) - 时间成本:复杂类型变更迁移需要 30-60 分钟
- 请问:“实际需要进行什么数据转换?”并实现两阶段模式
时间成本说明
- 模式 1(关系反向):2-3 分钟
- 模式 2(架构版本):2-5 分钟
- 模式 3(willMigrate 修复):5-10 分钟
- 模式 4(真机测试):15-30 分钟
- 模式 5(关系预取):3-5 分钟
实际影响
之前 SwiftData 迁移调试每个问题需要 2-8 小时
- 应用在生产环境启动时崩溃
- 现有用户数据丢失
- 迁移后关系损坏
- 模拟器运行成功,真机运行失败
- 用户信任受损
之后 使用系统化诊断只需 15-45 分钟
- 使用诊断信息识别问题类型(5 分钟)
- 应用正确的模式(5-10 分钟)
- 在真机上测试(15-30 分钟)
- 满怀信心地部署
关键见解 SwiftData 针对每种常见迁移问题都有成熟的模式。问题在于,开发者不知道针对错误应使用哪种诊断方法。
资源
WWDC:2025-291、2023-10195
文档:/swiftdata
技能:axiom-swiftdata-migration、axiom-swiftdata、axiom-database-migration
创建时间 2025-12-09 状态 可用于生产环境的诊断模式 框架 SwiftData(Apple) Swift 5.9+