GRDB.swift
GRDB.swiftは「SwiftアプリケーションのためのSQLiteツールキット」として開発された、Swiftエコシステムで最も高機能なSQLiteライブラリです。「型安全でリアクティブなデータベース操作」をコンセプトに、複雑なデータベース操作をSwiftらしい型安全で直感的なAPIで提供。ValueObservation、DatabasePool、マイグレーション管理など、現代的なiOS/macOSアプリ開発に必要な全機能を包括的にサポートし、エンタープライズレベルのデータベースソリューションとして確固たる地位を築いています。
GitHub概要
groue/GRDB.swift
A toolkit for SQLite databases, with a focus on application development
トピックス
スター履歴
ライブラリ
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)
}