axiom-sqlitedata-migration について
アプリデータをSwiftDataからSQLiteDataへ移行する開発者向けガイドです。APIパターンの対応、CloudKitのレコード共有や性能上の考慮点に加え、データを保持する移行や段階的な移行方法を説明します。
主な機能
- 01SwiftDataとSQLiteDataのパターン対応
- 02CloudKitのレコード共有設定
- 03性能特性の比較
- 04既存ユーザーデータのエクスポートとインポート
- 05モデルの段階的な移行
- 06リレーションとカスケード削除の処理
活用シーン
- 01性能上の課題があるモデルをSQLiteDataへ移行
- 02アプリにレコード共有を追加
- 03移行中にユーザーデータを保持
- 04SwiftDataと併用しながらSQLiteDataを段階導入
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-sqlitedata-migrationインストール後、Claude Code で/axiom-sqlitedata-migrationを入力して読み込まれたか確認します。
詳細
SwiftData から SQLiteData への移行
切り替えのタイミング
┌─────────────────────────────────────────────────────────┐
│ Should I switch from SwiftData to SQLiteData? │
├─────────────────────────────────────────────────────────┤
│ │
│ Performance problems with 10k+ records? │
│ YES → SQLiteData (10-50x faster for large datasets) │
│ │
│ Need CloudKit record SHARING (not just sync)? │
│ YES → SQLiteData (SwiftData cannot share records) │
│ │
│ Complex queries across multiple tables? │
│ YES → SQLiteData + raw GRDB when needed │
│ │
│ Need Sendable models for Swift 6 concurrency? │
│ YES → SQLiteData (value types, not classes) │
│ │
│ Testing @Model classes is painful? │
│ YES → SQLiteData (pure structs, easy to mock) │
│ │
│ Happy with SwiftData for simple CRUD? │
│ YES → Stay with SwiftData (simpler for basic apps) │
│ │
└─────────────────────────────────────────────────────────┘
パターンの対応関係
| SwiftData | SQLiteData |
|---|---|
@Model class Item | @Table nonisolated struct Item |
@Attribute(.unique) | @Column(primaryKey: true) または SQL UNIQUE |
@Relationship var tags: [Tag] | var tagIDs: [Tag.ID] + 結合クエリ |
@Query var items: [Item] | @FetchAll var items: [Item] |
@Query(sort: \.title) | @FetchAll(Item.order(by: \.title)) |
@Query(filter: #Predicate { $0.isActive }) | @FetchAll(Item.where(\.isActive)) |
@Environment(\.modelContext) | @Dependency(\.defaultDatabase) |
context.insert(item) | Item.insert { Item.Draft(...) }.execute(db) |
context.delete(item) | Item.find(id).delete().execute(db) |
try context.save() | database.write { } ブロック内で自動的に保存 |
ModelContainer(for:) | prepareDependencies { $0.defaultDatabase = } |
コード例
SwiftData(変更前)
import SwiftData
@Model
class Task {
var id: UUID
var title: String
var isCompleted: Bool
var project: Project?
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
}
}
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \.title) private var tasks: [Task]
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
func addTask(_ title: String) {
let task = Task(title: title)
context.insert(task)
}
func deleteTask(_ task: Task) {
context.delete(task)
}
}
SQLiteData(変更後)
import SQLiteData
@Table
nonisolated struct Task: Identifiable {
let id: UUID
var title = ""
var isCompleted = false
var projectID: Project.ID?
}
struct TaskListView: View {
@Dependency(\.defaultDatabase) var database
@FetchAll(Task.order(by: \.title)) var tasks
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
func addTask(_ title: String) {
try database.write { db in
try Task.insert {
Task.Draft(title: title)
}
.execute(db)
}
}
func deleteTask(_ task: Task) {
try database.write { db in
try Task.find(task.id).delete().execute(db)
}
}
}
主な違い:
class→nonisolatedを付けたstruct@Model→@Table@Query→@FetchAll@Environment(\.modelContext)→@Dependency(\.defaultDatabase)- 暗黙の保存 → 明示的な
database.write { }ブロック - 直接の初期化 → 挿入には
.Draft型を使用 @Relationship→ 明示的な外部キー + 結合
CloudKit の共有(SwiftData ではできません)
SwiftData は CloudKit の同期には対応していますが、共有には対応していません。SQLiteData はレコード共有に対応する唯一の Apple ネイティブの選択肢です。
// 1. Setup SyncEngine with sharing
prepareDependencies {
$0.defaultDatabase = try! appDatabase()
$0.defaultSyncEngine = try SyncEngine(
for: $0.defaultDatabase,
tables: Task.self, Project.self
)
}
// 2. Share a record
@Dependency(\.defaultSyncEngine) var syncEngine
@State var sharedRecord: SharedRecord?
func shareProject(_ project: Project) async throws {
sharedRecord = try await syncEngine.share(record: project) { share in
share[CKShare.SystemFieldKey.title] = "Join my project!"
}
}
// 3. Present native sharing UI
.sheet(item: $sharedRecord) { record in
CloudSharingView(sharedRecord: record)
}
共有で実現できること: 共同編集リスト、共有ワークスペース、家族での共有、チーム機能。
パフォーマンスの比較
| 操作 | SwiftData | SQLiteData | 改善 |
|---|---|---|---|
| 50k 件のレコードを挿入 | 約 4 分 | 約 45 秒 | 5 倍 |
| 述語を使って 10k 件をクエリ | 約 2 秒 | 約 50ms | 40 倍 |
| メモリ(10k オブジェクト) | 約 80MB | 約 20MB | 4 分の 1 |
| コールド起動(大規模 DB) | 約 3 秒 | 約 200ms | 15 倍 |
ベンチマークは概算であり、デバイスやデータの形状によって異なります。
既存のユーザーデータの移行
重要:スキーマの移行だけでは、ユーザーデータがすべて失われます。SwiftData からデータをエクスポートし、SQLiteData にインポートする必要があります。
// 1. Read all records from SwiftData's backing store
func migrateExistingData(from modelContext: ModelContext, to database: any DatabaseWriter) throws {
// Fetch all SwiftData records
let descriptor = FetchDescriptor<SwiftDataTask>()
let existingTasks = try modelContext.fetch(descriptor)
// 2. Bulk insert into SQLiteData
try database.write { db in
for task in existingTasks {
try SQLiteTask.insert {
SQLiteTask.Draft(
id: task.id,
title: task.title,
isCompleted: task.isCompleted,
projectID: task.project?.id
)
}
.execute(db)
}
}
// 3. Verify migration
let count = try database.read { db in
try SQLiteTask.fetchCount(db)
}
assert(count == existingTasks.count, "Migration count mismatch!")
}
移行チェックリスト:
- SwiftData コンテナを削除する前に、すべてのモデルをエクスポートする
- リレーションシップを移行する(外部キー用に親の ID を取得する)
- 移行後にレコード数が一致することを確認する
- 正常に動作することを確認するまで、SwiftData コンテナをバックアップとして保持する
- UserDefaults のバージョンフラグを使って、初回起動時に移行を実行する
段階的な移行戦略
すべてを一度に移行する必要はありません。
- 新機能に SQLiteData を追加する — 既存のシンプルな CRUD には SwiftData を使い続けます
- モデルを 1 つずつ移行する — パフォーマンスのボトルネックから始めます
- 最初は別々のデータベースを使う — 大量のデータや共有には SQLiteData、設定には SwiftData を使います
- 必要に応じて統合する — または、うまく動作するなら併用を続けます
よくある落とし穴
リレーションシップ → 外部キー
// SwiftData: implicit relationship
@Relationship var tasks: [Task]
// SQLiteData: explicit column + query
// In child: var projectID: Project.ID
// To fetch: Task.where { $0.projectID.eq(#bind(project.id)) }
カスケード削除
// SwiftData: @Relationship(deleteRule: .cascade)
// SQLiteData: Define in SQL schema
// "REFERENCES parent(id) ON DELETE CASCADE"
逆方向のリレーションシップは自動設定されない
// SwiftData: @Relationship(inverse: \Task.project)
// SQLiteData: Query both directions manually
let tasks = Task.where { $0.projectID.eq(#bind(project.id)) }
let project = Project.find(task.projectID)
関連スキル:
axiom-sqlitedata— SQLiteData API の完全なリファレンスaxiom-swiftdata— Apple のフレームワークを使い続ける場合の SwiftData パターンaxiom-grdb— 複雑なクエリ向けの生の GRDB
履歴: 変更については git log を参照してください
コメント
Markdown 対応