axiom-swiftdata-migration について
SwiftData のスキーマ移行を扱う開発者向けスキルです。軽量移行とカスタム移行の選択、リレーションの維持、アップグレードの検証を、移行段階の制約と実践的なテスト方法を交えて説明します。
主な機能
- 01軽量移行とカスタム移行の判断
- 02プロパティ型変更の二段階移行
- 03リレーション移行パターン
- 04VersionedSchema の設定
- 05移行テストのチェックリスト
活用シーン
- 01永続化プロパティの型変更
- 02モデル間リレーションの再構成
- 03重複データがある項目への一意制約追加
- 04実機でのアップグレードテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftdata-migrationインストール後、Claude Code で/axiom-swiftdata-migrationを入力して読み込まれたか確認します。
詳細
SwiftData のカスタムスキーマ移行
概要
SwiftData のスキーマ移行では、モデルの変更時にデータを安全に移行します。基本原則 SwiftData の willMigrate からアクセスできるのは OLD モデルのみで、didMigrate からアクセスできるのは NEW モデルのみです。両方に同時にアクセスすることはできません。この制約が、すべての移行戦略の形を決めます。
必要要件 iOS 17 以降、Swift 5.9 以降
対象 iOS 26 以降(propertiesToFetch などの機能)
カスタム移行が必要な場合
軽量移行(自動)
SwiftData は、次の変更を自動的に移行できます。
- ✅ 新しいオプショナルプロパティの追加
- ✅ デフォルト値を持つ新しい必須プロパティの追加
- ✅ プロパティの削除
- ✅ プロパティの名前変更(
@Attribute(originalName:)を使用) - ✅ リレーションシップの削除ルールの変更
- ✅ 新しいモデルの追加
カスタム移行(このスキル)
次の場合はカスタム移行が必要です。
- ❌ プロパティ型の変更(
String→AttributedString、Int→String) - ❌ オプショナルプロパティを必須にする(既存の null を補完する必要があります)
- ❌ 複雑なリレーションシップの再構成
- ❌ データ変換(フィールドの分割/統合)
- ❌ 一意性制約の追加時の重複排除
プロンプトの例
以下は、このスキルでの回答を想定した、開発者から実際に寄せられる質問です。
1. 「プロパティを String から AttributedString に変更する必要があります。リレーションシップを維持したまま既存データを移行するにはどうすればよいですか?」
→ このスキルでは、willMigrate/didMigrate の制約を回避できる 2 段階の移行パターンを紹介します。
2. 「カスケード削除を伴う 1 対多のリレーションシップをモデルで使っています。型変更の移行時にこれを維持するにはどうすればよいですか?」
→ このスキルでは、リレーションシップの事前取得と、スキーマバージョン間で逆方向リレーションシップを維持する方法を説明します。
3. 「Tags と Notes の間に多対多のリレーションシップがあります。移行が 'Expected only Arrays for Relationships' というエラーで失敗します。何が問題ですか?」
→ このスキルでは、逆方向リレーションシップを明示的に指定する必要性と、iOS 17.0 のアルファベット順命名に関するバグについて説明します。
4. 「すべてのリレーションシップを維持したまま、モデルの名前を変更する必要があります。」
→ このスキルでは、軽量移行における @Attribute(originalName:) のパターンを紹介します。
5. 「シミュレーターでは移行が機能しますが、既存データがある実機ではクラッシュします。」
→ このスキルでは、実機テストの重要性を強調し、シミュレーターでの成功が本番環境での安全性を保証しない理由を説明します。
6. 「変更していないモデルも含め、すべてのモデルを各 VersionedSchema にコピーしなければならないのはなぜですか?」
→ このスキルでは、SwiftData の設計について説明します。各 VersionedSchema は差分ではなく、完全なスナップショットです。
7. 「'The model used to open the store is incompatible with the one used to create the store' というエラーが表示されます。」
→ このスキルでは、スキーマバージョンの不一致をデバッグする手順を紹介します。
8. 「本番環境にリリースする前に、SwiftData の移行をテストするにはどうすればよいですか?」
→ このスキルでは、移行テストのワークフロー、実機テストの要件、検証戦略について説明します。
willMigrate/didMigrate の制約
重要 これは、すべての SwiftData 移行パターンの形を決めるアーキテクチャ上の制約です。
アクセスできるもの
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// ✅ CAN access: SchemaV1 models (old)
let v1Notes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
// ❌ CANNOT access: SchemaV2 models
// SchemaV2.Note doesn't exist yet
},
didMigrate: { context in
// ✅ CAN access: SchemaV2 models (new)
let v2Notes = try context.fetch(FetchDescriptor<SchemaV2.Note>())
// ❌ CANNOT access: SchemaV1 models
// SchemaV1.Note is gone
}
)
これが重要な理由
1 つの移行ステージで、古い型から新しい型へデータを直接変換することはできません。例:
// ❌ IMPOSSIBLE - you can't do this in one stage
willMigrate: { context in
let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
for oldNote in oldNotes {
let newNote = SchemaV2.Note() // ❌ Doesn't exist yet!
newNote.content = oldNote.contentAsAttributedString()
}
}
解決策 2 段階の移行パターンを使用します(以下で説明します)。
基本パターン
パターン 1:基本的な VersionedSchema のセットアップ
スキーマの各バージョンを、それぞれ VersionedSchema として定義する必要があります。
import SwiftData
enum NotesSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self, Folder.self, Tag.self] // ALL models, even if unchanged
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String
var content: String // Original type
var createdAt: Date
@Relationship(deleteRule: .nullify, inverse: \Folder.notes)
var folder: Folder?
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = []
init(id: String, title: String, content: String, createdAt: Date) {
self.id = id
self.title = title
self.content = content
self.createdAt = createdAt
}
}
@Model
final class Folder {
@Attribute(.unique) var id: String
var name: String
@Relationship(deleteRule: .cascade)
var notes: [Note] = []
init(id: String, name: String) {
self.id = id
self.name = name
}
}
@Model
final class Tag {
@Attribute(.unique) var id: String
var name: String
@Relationship(deleteRule: .nullify)
var notes: [Note] = []
init(id: String, name: String) {
self.id = id
self.name = name
}
}
}
主なパターン
- 完全なスナップショット 変更のないモデルも含め、すべてのモデルを含めます。
- セマンティックバージョニング Schema.Version(major, minor, patch) を使用します。
- 明示的な init SwiftData はイニシャライザーを自動生成しません。
- 逆方向リレーションシップ 双方向にするには、両側で指定します。
パターン 2:型変更のための 2 段階移行
使用する場合 プロパティ型を変更するとき(String → AttributedString、Int → String など)
問題
Note.content を String から AttributedString に変更したいのですが、古い型と新しい型の両方に同時にアクセスすることはできません。
ソリューション
両方のプロパティを持つ中間スキーマバージョン(V1.1)を使用します。
// Stage 1: V1 → V1.1 (Add new property alongside old)
enum NotesSchemaV1_1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 1, 0)
static var models: [any PersistentModel.Type] {
[Note.self, Folder.self, Tag.self]
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String
// OLD property (to be deprecated)
@Attribute(originalName: "content")
var contentOld: String = ""
// NEW property (target type)
var contentNew: AttributedString?
var createdAt: Date
@Relationship(deleteRule: .nullify, inverse: \Folder.notes)
var folder: Folder?
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = []
init(id: String, title: String, contentOld: String, createdAt: Date) {
self.id = id
self.title = title
self.contentOld = contentOld
self.createdAt = createdAt
}
}
// Folder and Tag unchanged (copy from V1)
@Model final class Folder { /* same as V1 */ }
@Model final class Tag { /* same as V1 */ }
}
// Stage 2: V1.1 → V2 (Transform data, remove old property)
enum NotesSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self, Folder.self, Tag.self]
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String
// Renamed from contentNew
@Attribute(originalName: "contentNew")
var content: AttributedString?
var createdAt: Date
@Relationship(deleteRule: .nullify, inverse: \Folder.notes)
var folder: Folder?
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = []
init(id: String, title: String, content: AttributedString?, createdAt: Date) {
self.id = id
self.title = title
self.content = content
self.createdAt = createdAt
}
}
@Model final class Folder { /* same as V1 */ }
@Model final class Tag { /* same as V1 */ }
}
移行計画
enum NotesMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[NotesSchemaV1.self, NotesSchemaV1_1.self, NotesSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV1_1, migrateV1_1toV2]
}
// Stage 1: Lightweight migration (adds contentNew)
static let migrateV1toV1_1 = MigrationStage.lightweight(
fromVersion: NotesSchemaV1.self,
toVersion: NotesSchemaV1_1.self
)
// Stage 2: Custom migration (transform String → AttributedString)
static let migrateV1_1toV2 = MigrationStage.custom(
fromVersion: NotesSchemaV1_1.self,
toVersion: NotesSchemaV2.self,
willMigrate: { context in
// Transform data while we still have access to V1.1 models
var fetchDesc = FetchDescriptor<NotesSchemaV1_1.Note>()
// Prefetch relationships to preserve them
fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]
let notes = try context.fetch(fetchDesc)
for note in notes {
// Convert String → AttributedString
note.contentNew = try? AttributedString(markdown: note.contentOld)
}
try context.save()
},
didMigrate: nil
)
}
移行計画を適用する
@main
struct NotesApp: App {
let container: ModelContainer = {
do {
let schema = Schema(versionedSchema: NotesSchemaV2.self)
return try ModelContainer(
for: schema,
migrationPlan: NotesMigrationPlan.self
)
} catch {
fatalError("Failed to create container: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
パターン 3:多対多リレーションシップの移行
使用する場面: 多対多リレーションシップがある場合(タグ ↔ ノート)
重要な要件
- 逆リレーションシップを明示する SwiftData は多対多を推論しません
- 両側に配列を使用する 省略可能ではなく、配列である必要があります
- iOS 17.0 のバグへの回避策 アルファベット順の命名に関する問題
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self, Tag.self]
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String
// Many-to-many: MUST specify inverse
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = [] // ✅ Array with default value
init(id: String, title: String) {
self.id = id
self.title = title
}
}
@Model
final class Tag {
@Attribute(.unique) var id: String
var name: String
// Many-to-many: MUST specify inverse
@Relationship(deleteRule: .nullify, inverse: \Note.tags)
var notes: [Note] = [] // ✅ Array with default value
init(id: String, name: String) {
self.id = id
self.name = name
}
}
}
iOS 17.0 のアルファベット順に関するバグへの回避策
iOS 17.0 では、モデル名がアルファベット順の場合(例:Actor ↔ Movie は動作しますが、Movie ↔ Person は失敗します)、多対多リレーションシップが失敗することがあります。
回避策: リレーションシップ配列にデフォルト値を設定します。
@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var actors: [Actor] = [] // ✅ Default value prevents bug
修正済み: iOS 17.1 以降
中間テーブルのメタデータを追加する
リレーションシップに追加のフィールド(例:「このタグはいつ追加されましたか?」)が必要な場合は、明示的な中間モデルを使用します。
@Model
final class NoteTag {
@Attribute(.unique) var id: String
var addedAt: Date // Metadata on relationship
@Relationship(deleteRule: .cascade)
var note: Note?
@Relationship(deleteRule: .cascade)
var tag: Tag?
init(id: String, note: Note, tag: Tag, addedAt: Date) {
self.id = id
self.note = note
self.tag = tag
self.addedAt = addedAt
}
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade)
var noteTags: [NoteTag] = [] // One-to-many to junction
var tags: [Tag] {
noteTags.compactMap { $0.tag }
}
}
@Model
final class Tag {
@Attribute(.unique) var id: String
var name: String
@Relationship(deleteRule: .cascade)
var noteTags: [NoteTag] = [] // One-to-many to junction
var notes: [Note] {
noteTags.compactMap { $0.note }
}
}
パターン 4:移行中のリレーションシップの事前読み込み
使用する場面: N+1 クエリを避けるため、リレーションシップのあるモデルを移行する場合
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
var fetchDesc = FetchDescriptor<SchemaV1.Note>()
// Prefetch relationships (iOS 26+)
fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]
// Only fetch properties you need (iOS 26+)
fetchDesc.propertiesToFetch = [\.title, \.content]
let notes = try context.fetch(fetchDesc)
// Relationships are already loaded - no N+1
for note in notes {
let folderName = note.folder?.name // ✅ Already in memory
let tagCount = note.tags.count // ✅ Already in memory
}
try context.save()
},
didMigrate: nil
)
パフォーマンスへの影響
Without prefetching:
- 1 query to fetch notes
- N queries to fetch each note's folder
- N queries to fetch each note's tags
= 1 + N + N queries
With prefetching:
- 1 query to fetch notes
- 1 query to fetch all folders
- 1 query to fetch all tags
= 3 queries total
パターン 5:プロパティの名前変更
使用する場面: データを失わずにプロパティの名前を変更する場合
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
final class Note {
@Attribute(.unique) var id: String
var title: String // Original name
}
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
final class Note {
@Attribute(.unique) var id: String
// Renamed from "title" to "heading"
@Attribute(originalName: "title")
var heading: String
}
}
// Migration plan (lightweight migration)
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
)
}
この方法が機能する理由: SwiftData は originalName を認識し、軽量移行中にデータを保持します。
パターン 6:一意制約に対する重複排除
使用する場面: 重複があるフィールドに @Attribute(.unique) を追加する場合
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self]
}
@Model
final class Trip {
@Attribute(.unique) var id: String
var name: String // ❌ Not unique, has duplicates
init(id: String, name: String) {
self.id = id
self.name = name
}
}
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self]
}
@Model
final class Trip {
@Attribute(.unique) var id: String
@Attribute(.unique) var name: String // ✅ Now unique
init(id: String, name: String) {
self.id = id
self.name = name
}
}
}
enum TripMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// Deduplicate before adding unique constraint
let trips = try context.fetch(FetchDescriptor<SchemaV1.Trip>())
var seenNames = Set<String>()
for trip in trips {
if seenNames.contains(trip.name) {
// Duplicate - delete or rename
context.delete(trip)
} else {
seenNames.insert(trip.name)
}
}
try context.save()
},
didMigrate: nil
)
}
移行のテスト
必須テストチェックリスト
- 新規インストールをテストする(すべての移行が V1 → 最新版まで実行されること)
- 各旧バージョンからのアップグレードをテストする
- 実機でテストする(シミュレーターだけでは不十分です)
- 移行後のリレーションシップの整合性を確認する
- データ損失がないことを確認する(前後のレコード数を比較する)
- 本番環境と同規模のデータセットでテストする
シミュレーターでのテストだけでは不十分な理由
シミュレーターの動作: 再ビルド時にデータベースが削除され、常に新しいスキーマが読み込まれます
実機の動作: アップデート後も永続データベースが保持されるため、スキーマが一致している必要があります
// ❌ WRONG - only testing in simulator
// You rebuild → simulator deletes database → fresh install
// Migration code never runs!
// ✅ CORRECT - test on real device
// 1. Install v1 build on device
// 2. Create sample data
// 3. Install v2 build (with migration)
// 4. Verify data preserved
テストのワークフロー
本番環境に移行をデプロイする前に:
1. テストデータセットを作成する
移行前の状態を表すテストデータを準備します:
- 最小データセット - すべてのリレーションシップタイプを含む 10~20 件のレコード
- 現実的なデータセット - 本番環境の規模に相当する 1,000 件以上のレコード
- エッジケース - 空のリレーションシップ、リレーションシップ数の上限、省略可能なフィールド
2. シミュレーターでテストする
テストデータを使って移行を実行します:
// Create test data in V1 schema
let v1Container = try ModelContainer(for: Schema(versionedSchema: SchemaV1.self))
// ... populate test data ...
// Run migration
let v2Container = try ModelContainer(
for: Schema(versionedSchema: SchemaV2.self),
migrationPlan: MigrationPlan.self
)
以下を確認します:
- すべてのリレーションシップが保持されている
- データ損失がない(前後のレコード数を比較する)
- 新しいフィールドに正しく値が設定されている
- 現実的なデータセットのサイズでパフォーマンスに問題がない
3. 実機でテストする
重要: シミュレーターでの成功は、本番環境での安全性を保証しません。
# Workflow:
1. Install v1 build on real device
2. Create 100+ records with relationships
3. Verify data exists
4. Install v2 build (over existing app, don't delete)
5. Launch app
6. Verify:
- App launches without crash
- All 100+ records still exist
- Relationships intact
- New fields populated
4. 本番データで検証する(可能な場合)
本番データにアクセスできる場合:
- 本番データベースを開発環境にコピーする
- コピーに対して移行を実行する
- データ破損がないことを確認する
- 本番環境と同規模のデータセットでパフォーマンスを確認する
移行に失敗した場合のデバッグツールについては、axiom-swiftdata-migration-diag を参照してください。
移行テストのパターン
import Testing
import SwiftData
@Test func testMigrationFromV1ToV2() throws {
// 1. Create V1 data
let v1Schema = Schema(versionedSchema: SchemaV1.self)
let v1Config = ModelConfiguration(isStoredInMemoryOnly: true)
let v1Container = try ModelContainer(for: v1Schema, configurations: v1Config)
let context = v1Container.mainContext
let note = SchemaV1.Note(id: "1", title: "Test", content: "Original")
context.insert(note)
try context.save()
// 2. Run migration to V2
let v2Schema = Schema(versionedSchema: SchemaV2.self)
let v2Container = try ModelContainer(
for: v2Schema,
migrationPlan: MigrationPlan.self,
configurations: v1Config
)
// 3. Verify data migrated
let v2Context = v2Container.mainContext
let notes = try v2Context.fetch(FetchDescriptor<SchemaV2.Note>())
#expect(notes.count == 1)
#expect(notes.first?.content != nil) // String → AttributedString
}
判断フロー:軽量移行とカスタム移行
What change are you making?
├─ Adding optional property → Lightweight ✓
├─ Adding required property with default → Lightweight ✓
├─ Renaming property (with originalName) → Lightweight ✓
├─ Removing property → Lightweight ✓
├─ Changing relationship delete rule → Lightweight ✓
├─ Adding new model → Lightweight ✓
├─ Changing property type → Custom (two-stage) ✗
├─ Making optional → required → Custom (populate nulls first) ✗
├─ Adding unique constraint (duplicates exist) → Custom (deduplicate first) ✗
└─ Complex relationship restructure → Custom ✗
よくある間違い
❌ VersionedSchema にすべてのモデルを含め忘れる
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Note.self] // ❌ WRONG: Missing Folder and Tag
}
}
// ✅ CORRECT: Include ALL models
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Note.self, Folder.self, Tag.self] // ✅ Even if unchanged
}
}
理由: 各 VersionedSchema はデータモデルの差分ではなく、完全なスナップショットです。
❌ 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>())
}
)
// ✅ CORRECT: Use willMigrate for old models
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>())
},
didMigrate: nil
)
❌ 実データを使って実機でテストしない
// ❌ WRONG: Simulator success ≠ production safety
// Rebuild simulator → database deleted → fresh install
// Migration never actually runs!
// ✅ CORRECT: Test migration path
// 1. Install v1 on real device
// 2. Create data (100+ records)
// 3. Install v2 with migration
// 4. Verify data preserved
❌ 逆リレーションシップを明示せずに多対多を設定する
// ❌ WRONG: SwiftData can't infer many-to-many
@Model
final class Note {
var tags: [Tag] = [] // ❌ Missing inverse
}
// ✅ CORRECT: Explicit inverse
@Model
final class Note {
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = [] // ✅ Inverse specified
}
❌ シミュレーターでの成功を本番環境での成功と見なす
シミュレーターでは再ビルド時にデータベースが削除されます。実機ではアップデート後も永続データベースが保持されます。
影響: シミュレーターでは移行のバグが隠れたままになり、本番ユーザー全員でクラッシュします。
対処法: リリース前に必ず実機でテストしてください。
失敗した移行のデバッグ
Core Data SQL デバッグを有効にする
# In Xcode scheme, add argument:
-com.apple.coredata.swiftdata.debug 1
出力 移行中に実際の SQL クエリを表示します
CoreData: sql: SELECT Z_PK, Z_ENT, Z_OPT, ZID, ZTITLE FROM ZNOTE
CoreData: sql: ALTER TABLE ZNOTE ADD COLUMN ZCONTENT TEXT
よくあるエラーメッセージ
| エラー | 考えられる原因 | 修正方法 |
|---|---|---|
| "Expected only Arrays for Relationships" | 多対多の逆方向の関連付けがありません | @Relationship(inverse:) を追加します |
| "The model used to open the store is incompatible" | スキーマのバージョンが一致しません | 移行プランの schemas 配列を確認します |
| "Failed to fulfill faulting for..." | 関連付けの整合性が崩れています | 移行中に関連付けを事前取得します |
| スキーマ変更後、アプリの起動時にクラッシュします | VersionedSchema にモデルがありません | すべてのモデルを含めます |
クイックリファレンス
基本的な移行の設定
// 1. Define versioned schemas
enum SchemaV1: VersionedSchema { /* models */ }
enum SchemaV2: VersionedSchema { /* models */ }
// 2. Create migration plan
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
)
}
// 3. Apply to container
let schema = Schema(versionedSchema: SchemaV2.self)
let container = try ModelContainer(
for: schema,
migrationPlan: MigrationPlan.self
)
リソース
WWDC:2025-291、2023-10195
ドキュメント:/swiftdata
スキル:axiom-swiftdata、axiom-swiftdata-migration-diag、axiom-database-migration
作成日 2025-12-09 対象 iOS 17 以降(iOS 26 以降の機能を中心に扱います) フレームワーク SwiftData(Apple) Swift 5.9 以降