mcpField
A
axiom-sqlitedata-migration

axiom-sqlitedata-migration

github.com/charleswiltgenデータ・データベース1.2k184 回インストール今日更新
リポジトリ

axiom-sqlitedata-migration について

アプリデータをSwiftDataからSQLiteDataへ移行する開発者向けガイドです。APIパターンの対応、CloudKitのレコード共有や性能上の考慮点に加え、データを保持する移行や段階的な移行方法を説明します。

主な機能

  1. 01SwiftDataとSQLiteDataのパターン対応
  2. 02CloudKitのレコード共有設定
  3. 03性能特性の比較
  4. 04既存ユーザーデータのエクスポートとインポート
  5. 05モデルの段階的な移行
  6. 06リレーションとカスケード削除の処理

活用シーン

  1. 01性能上の課題があるモデルをSQLiteDataへ移行
  2. 02アプリにレコード共有を追加
  3. 03移行中にユーザーデータを保持
  4. 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)   │
│                                                         │
└─────────────────────────────────────────────────────────┘

パターンの対応関係

SwiftDataSQLiteData
@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)
}

共有で実現できること: 共同編集リスト、共有ワークスペース、家族での共有、チーム機能。


パフォーマンスの比較

操作SwiftDataSQLiteData改善
50k 件のレコードを挿入約 4 分約 45 秒5 倍
述語を使って 10k 件をクエリ約 2 秒約 50ms40 倍
メモリ(10k オブジェクト)約 80MB約 20MB4 分の 1
コールド起動(大規模 DB)約 3 秒約 200ms15 倍

ベンチマークは概算であり、デバイスやデータの形状によって異なります。


既存のユーザーデータの移行

重要:スキーマの移行だけでは、ユーザーデータがすべて失われます。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 のバージョンフラグを使って、初回起動時に移行を実行する

段階的な移行戦略

すべてを一度に移行する必要はありません。

  1. 新機能に SQLiteData を追加する — 既存のシンプルな CRUD には SwiftData を使い続けます
  2. モデルを 1 つずつ移行する — パフォーマンスのボトルネックから始めます
  3. 最初は別々のデータベースを使う — 大量のデータや共有には SQLiteData、設定には SwiftData を使います
  4. 必要に応じて統合する — または、うまく動作するなら併用を続けます

よくある落とし穴

リレーションシップ → 外部キー
// 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 を参照してください

コメント