GRDB.swift

GRDB.swiftは「SwiftアプリケーションのためのSQLiteツールキット」として開発された、Swiftエコシステムで最も高機能なSQLiteライブラリです。「型安全でリアクティブなデータベース操作」をコンセプトに、複雑なデータベース操作をSwiftらしい型安全で直感的なAPIで提供。ValueObservation、DatabasePool、マイグレーション管理など、現代的なiOS/macOSアプリ開発に必要な全機能を包括的にサポートし、エンタープライズレベルのデータベースソリューションとして確固たる地位を築いています。

ORMSwiftSQLiteiOSデータベースリアクティブ

GitHub概要

groue/GRDB.swift

A toolkit for SQLite databases, with a focus on application development

スター7,641
ウォッチ88
フォーク768
作成日:2015年6月30日
言語:Swift
ライセンス:MIT License

トピックス

databasedatabase-observationgrdbspmsqlsql-buildersqlitesqlite-databases

スター履歴

groue/GRDB.swift Star History
データ取得日時: 2025/7/19 09:29

ライブラリ

GRDB.swift

概要

GRDB.swiftは「SwiftアプリケーションのためのSQLiteツールキット」として開発された、Swiftエコシステムで最も高機能なSQLiteライブラリです。「型安全でリアクティブなデータベース操作」をコンセプトに、複雑なデータベース操作をSwiftらしい型安全で直感的なAPIで提供。ValueObservation、DatabasePool、マイグレーション管理など、現代的なiOS/macOSアプリ開発に必要な全機能を包括的にサポートし、エンタープライズレベルのデータベースソリューションとして確固たる地位を築いています。

詳細

GRDB.swift 2025年版はSwift SQLite操作の決定版として7年以上の開発実績により成熟したAPIと最高レベルの機能性を誇ります。プロトコル指向設計によりFetchableRecord、PersistableRecord、TableRecordの組み合わせで柔軟なモデル定義を実現。SQLクエリビルダー、生SQLアクセス、リアクティブなデータベース監視など複数のアプローチを統合し、シンプルなCRUD操作から複雑なアナリティクスまで幅広いニーズに対応。Codable統合、Swift Concurrency対応、WALモードによる並列読み取りなど最新技術を積極採用しています。

主な特徴

  • 型安全SQLクエリビルダー: QueryInterfaceRequestによる型安全なクエリ構築
  • データベース監視: ValueObservationによるリアルタイムデータ変更通知
  • 堅牢な同期制御: DatabaseQueue/DatabasePoolによるスレッドセーフな操作
  • マイグレーション管理: DatabaseMigratorによる自動スキーマ進化
  • 直接SQLiteアクセス: 生SQLとHigh-levelAPIの両立
  • プロトコル指向設計: Swift Codableとの完全統合

メリット・デメリット

メリット

  • Swift言語機能との深い統合と優れた型安全性
  • 豊富な機能セットでエンタープライズアプリケーションにも対応
  • データベース監視によるリアクティブUIの簡単実装
  • SQLiteの全機能にアクセス可能で制限なし
  • 優れたドキュメントとアクティブなコミュニティサポート
  • Swift Concurrency対応による最新の非同期処理

デメリット

  • 学習コストが高く、初心者には複雑
  • 機能豊富な分、シンプルなアプリには過剰な場合がある
  • CocoaPodsデプロイ問題による最新版インストールの煩雑さ
  • Carthageサポート非対応による依存関係管理の制約
  • Sendableでないレガシークラスとの互換性問題
  • 他のモバイル開発者にとって学習リソースが限定的

参考ページ

書き方の例

セットアップ

// Package.swift
dependencies: [
    .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.24.1")
]

// CocoaPods (特定バージョン指定が必要)
// Podfile
pod 'GRDB.swift', '~> 6.24.1'

// または最新版のワークアラウンド
pod 'GRDB.swift', :git => 'https://github.com/groue/GRDB.swift.git', :tag => 'v6.29.3'
import GRDB

// データベース接続の初期化
var dbQueue: DatabaseQueue!

func setupDatabase() throws {
    let databaseURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("Database.sqlite")
    
    dbQueue = try DatabaseQueue(path: databaseURL.path)
}

基本的な使い方

// レコード定義(Codable + GRDB protocols)
struct Player: Codable, FetchableRecord, PersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    var team: String?
    var createdAt: Date
    
    // テーブル名の指定(省略可能)
    static let databaseTableName = "player"
    
    // カラム定義
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let name = Column(CodingKeys.name)
        static let score = Column(CodingKeys.score)
        static let team = Column(CodingKeys.team)
        static let createdAt = Column(CodingKeys.createdAt)
    }
}

// データベーススキーマの作成
func createSchema() throws {
    try dbQueue.write { db in
        try db.create(table: "player") { t in
            t.autoIncrementedPrimaryKey("id")
            t.column("name", .text).notNull()
            t.column("score", .integer).notNull().defaults(to: 0)
            t.column("team", .text)
            t.column("createdAt", .datetime).notNull()
        }
    }
}

クエリ実行

// データの挿入
func insertPlayer(name: String, score: Int, team: String?) throws {
    try dbQueue.write { db in
        var player = Player(
            id: nil,
            name: name,
            score: score,
            team: team,
            createdAt: Date()
        )
        try player.insert(db)
        print("Inserted player with ID: \(player.id!)")
    }
}

// 単一レコード取得
func getPlayer(id: Int64) throws -> Player? {
    try dbQueue.read { db in
        return try Player.fetchOne(db, id: id)
    }
}

// 全レコード取得
func getAllPlayers() throws -> [Player] {
    try dbQueue.read { db in
        return try Player.fetchAll(db)
    }
}

// 条件付き検索(Query Interface)
func getPlayersWithHighScore(threshold: Int) throws -> [Player] {
    try dbQueue.read { db in
        return try Player
            .filter(Player.Columns.score >= threshold)
            .order(Player.Columns.score.desc)
            .fetchAll(db)
    }
}

// 生SQLクエリ
func getTopPlayersByTeam() throws -> [Row] {
    try dbQueue.read { db in
        return try Row.fetchAll(db, sql: """
            SELECT team, MAX(score) as max_score, COUNT(*) as player_count
            FROM player
            WHERE team IS NOT NULL
            GROUP BY team
            ORDER BY max_score DESC
            """)
    }
}

データ操作

// レコード更新
func updatePlayerScore(_ player: Player, newScore: Int) throws {
    try dbQueue.write { db in
        var updatedPlayer = player
        updatedPlayer.score = newScore
        try updatedPlayer.update(db)
    }
}

// 部分更新
func updatePlayerTeam(id: Int64, newTeam: String) throws {
    try dbQueue.write { db in
        try Player
            .filter(key: id)
            .updateAll(db, Player.Columns.team.set(to: newTeam))
    }
}

// レコード削除
func deletePlayer(_ player: Player) throws {
    try dbQueue.write { db in
        try player.delete(db)
    }
}

// 条件付き削除
func deletePlayersWithLowScore(threshold: Int) throws -> Int {
    try dbQueue.write { db in
        return try Player
            .filter(Player.Columns.score < threshold)
            .deleteAll(db)
    }
}

// 複雑なクエリ例
func getPlayerStatistics() throws -> PlayerStatistics {
    try dbQueue.read { db in
        let totalPlayers = try Player.fetchCount(db)
        let averageScore = try Double.fetchOne(db, sql: "SELECT AVG(score) FROM player") ?? 0
        let topScore = try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") ?? 0
        let teams = try String.fetchAll(db, sql: "SELECT DISTINCT team FROM player WHERE team IS NOT NULL")
        
        return PlayerStatistics(
            totalPlayers: totalPlayers,
            averageScore: averageScore,
            topScore: topScore,
            teams: teams
        )
    }
}

struct PlayerStatistics {
    let totalPlayers: Int
    let averageScore: Double
    let topScore: Int
    let teams: [String]
}

設定とカスタマイズ

// データベース監視(ValueObservation)
func observePlayers() -> AnyPublisher<[Player], Error> {
    ValueObservation
        .tracking { db in
            try Player
                .order(Player.Columns.score.desc)
                .fetchAll(db)
        }
        .publisher(in: dbQueue)
        .eraseToAnyPublisher()
}

// 使用例
class PlayerViewModel: ObservableObject {
    @Published var players: [Player] = []
    private var cancellable: AnyCancellable?
    
    func startObserving() {
        cancellable = observePlayers()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { completion in
                    if case .failure(let error) = completion {
                        print("Observer error: \(error)")
                    }
                },
                receiveValue: { players in
                    self.players = players
                }
            )
    }
}

// DatabasePoolによる並列読み取り
func setupConcurrentDatabase() throws -> DatabasePool {
    let databaseURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("ConcurrentDatabase.sqlite")
    
    var config = Configuration()
    config.prepareDatabase { db in
        // WALモードを有効化(並列読み取りのため)
        try db.execute(sql: "PRAGMA journal_mode = WAL")
    }
    
    return try DatabasePool(path: databaseURL.path, configuration: config)
}

// カスタムSQL関数
func setupCustomFunctions() throws {
    try dbQueue.write { db in
        let removeAccents = DatabaseFunction("removeAccents", argumentCount: 1, pure: true) { (arguments: [DatabaseValue]) in
            guard let string = String.fromDatabaseValue(arguments[0]) else {
                return nil
            }
            return string.folding(options: .diacriticInsensitive, locale: nil)
        }
        db.add(function: removeAccents)
    }
}

エラーハンドリング

// エラーハンドリングとトランザクション
enum DatabaseError: Error {
    case playerNotFound
    case invalidData
    case transactionFailed
}

func performBulkPlayerUpdate(playerUpdates: [PlayerUpdate]) throws {
    try dbQueue.write { db in
        try db.inSavepoint {
            for update in playerUpdates {
                guard let existingPlayer = try Player.fetchOne(db, id: update.playerId) else {
                    throw DatabaseError.playerNotFound
                }
                
                var updatedPlayer = existingPlayer
                updatedPlayer.score = update.newScore
                updatedPlayer.team = update.newTeam
                
                try updatedPlayer.update(db)
            }
        }
    }
}

struct PlayerUpdate {
    let playerId: Int64
    let newScore: Int
    let newTeam: String?
}

// 非同期処理とエラーハンドリング
@MainActor
func loadPlayersAsync() async throws -> [Player] {
    return try await withCheckedThrowingContinuation { continuation in
        dbQueue.asyncRead { result in
            switch result {
            case .success(let db):
                do {
                    let players = try Player.fetchAll(db)
                    continuation.resume(returning: players)
                } catch {
                    continuation.resume(throwing: error)
                }
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

// データベースマイグレーション
func setupMigrations() throws {
    var migrator = DatabaseMigrator()
    
    // 初期バージョン
    migrator.registerMigration("v1.0") { db in
        try db.create(table: "player") { t in
            t.autoIncrementedPrimaryKey("id")
            t.column("name", .text).notNull()
            t.column("score", .integer).notNull().defaults(to: 0)
        }
    }
    
    // バージョン1.1: teamカラムの追加
    migrator.registerMigration("v1.1") { db in
        try db.alter(table: "player") { t in
            t.add(column: "team", .text)
        }
    }
    
    // バージョン1.2: createdAtカラムの追加
    migrator.registerMigration("v1.2") { db in
        try db.alter(table: "player") { t in
            t.add(column: "createdAt", .datetime).notNull().defaults(to: Date())
        }
    }
    
    // マイグレーション実行
    try migrator.migrate(dbQueue)
}