GRDB.swiftというSQLiteライブラリがイイ感じだった

みなさんアプリのデータベースには何を使っていますか？

周りをみてみると、「SQLite？今はやっぱRealmやろ！」って感じで最近はRealmが積極的に採用されていますね。
僕もRealmを利用していて、SQLite+FMDBを使っていた頃に比べると生SQL書かんで済むわサーバー建てればデバイス間の同期が楽にできるわで爆速で開発が進むようになり、控えめに言って尊いです。

このRealmの躍進によりSQLiteの影が薄くなりつつありますが、最近
「**Swiftyに書けて**」「**比較的早くて**」「**イイ感じ**」にSQLiteを操作できるライブラリ「**GRDB.swift**」を使ってみて、まだまだSQLiteでもいけそうだなーと感じたところがあったのでまとめてみました。
~~でもやっぱ結局中身はSQLiteなので根本的なところはどうにかなってないんですけどね~~

## GRDB.swiftを採用する3つの理由
### シンプルで機能が豊富
生SQLでの実行はもちろん、モデルマッピングや、データベースの変化をオブザーバで検知することができます(Rx向けに[RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB)も用意されています)
他機能としてマイグレーション、全文検索、暗号化も備えています。
特に他のライブラリではマイグレーション機能を標準でサポートされていないことが多く、別途ライブラリを追加する必要があったのでこれは嬉しいと思いますね。

### 比較的早い
参考元: [Performance](https://github.com/groue/GRDB.swift/wiki/Performance)
流石にRealmには負けてしまいますが、他のSQLiteライブラリに比べるとInsert/Fetch共に早いですね。

### ドキュメントがしっかり書かれている & Swift4対応されている
機能や仕様について細かく書かれています。
Swift4にもきちんと対応しており、Codableを用いたモデル定義も可能です。
コミュニティ(Issuesのみですが)も比較的活発です。

## セットアップ

[groue/GRDB.swift](https://github.com/groue/GRDB.swift)

CocoaPods, Swift Package Managerに対応しています。
Carthageは未サポートのようです。残念

今回はCocoaPodsでセットアップを行います

```bash
use_frameworks!
pod 'GRDB.swift'
```


## DBへの接続方法
データベースへ接続するには、DatabaseQueueあるいはDatabasePoolを用意します。
**DatabaseQueue**は、データベースへ直列アクセスします。引数を指定しない場合、In Memoryになります。
**DatabasePool**は、データベースへの同時並行アクセスをサポートします。**WALモードを有効にしたい場合、パフォーマンス重視の場合**はこちらを使いましょう。

**どちらを使えばいいのかわからない場合は、DatabaseQueueを使ってください。**

```swift
let inMemoryQueue = DatabaseQueue()
let queue = DatabaseQueue(path: "path")
let pool = DatabasePool(path: "path")
```
## モデルの定義とテーブルの作成
Userテーブルを作成する例を考えてみます。
GRDBにはいくつかプロトコルが用意されており、使いたい機能に応じてプロトコルを継承する形です。
よくわからない場合はすべての機能が使えるRecordクラスを使っておけばいいと思います。

・Persistable
・TableMapping
・RowConvertible
・Record
・RecordBox

### Persistable
Insert, Update, Deleteなどを行う際に必要となるプロトコルです。
プロパティの変更を行いたい場合(idなど)、**MutablePersistable**を使ってください。

### TableMapping
テーブル名の定義、クエリインターフェイスを利用する場合に必要となるプロトコルです。
Persistable, Recordの継承元でもあり、これ単体で必要になるパターンはあまりないと思います。
必要に応じてRowConvertibleを継承します。

### RowConvertible
データベースからデータを読み取るときに必要なプロトコルです（fetchAll, fetchOneなど）。クエリインターフェイスを必要とする場合、TableMappingも一緒に継承する必要があります。

### Record
RowConvertible, TableMapping, Persistableを継承した**クラス**です。
全ての機能が使えるようになるので、だいたいはこのクラスを継承してモデルを作成します。
他のプロトコルとの違いは、プロパティの変更検知が効くことです。これにより、プロパティが変更されたかどうか、された場合は必要なカラムのみUpdateという処理が行えます。(*後述します)


今回は、このRecordクラスを継承したモデルを作成します。

```swift
class User: Record {
    var id: Int64?
    var name: String
    var age: Int64
    var score: Int64
    let created_at: Date?
    
    // テーブル名
    override static var databaseTableName: String {
        return "users"
    }
    
    enum Columns {
        static let id = Column("id")
        static let name = Column("name")
        static let age = Column("age")
        static let score = Column("score")
        static let createdAt = Column("created_at")
    }
    
    init(name: String) {
        self.name = name
        self.age = 0
        self.score = 0
        self.created_at = Date()
        super.init()
    }
    
    required init(row: Row) {
        self.id = row["id"]
        self.name = row["name"]
        self.age = row["age"]
        self.score = row["score"]
        self.created_at = row["created_at"]
        super.init(row: row)
    }
    
    override func encode(to container: inout PersistenceContainer) {
        container["id"] = self.id
        container["name"] = self.name
        container["age"] = self.age
        container["score"] = self.score
        container["created_at"] = self.created_at
    }
    
    override func didInsert(with rowID: Int64, for column: String?) {
		// Insert時にidに反映
        self.id = rowID
    }
}
```

プロパティの変更検知いらない、もっとすっきり書きたい！という場合はこういう書き方でもいけます。Codableサイコーですね。

```swift
struct User: Codable, MutablePersistable, RowConvertible {
    // テーブル名
    static var databaseTableName: String {
        return "users"
    }
    
    var id: Int64?
    var name: String
    var age: Int64
    var score: Int64
    let created_at: Date?
    
    mutating func didInsert(with rowID: Int64, for column: String?) {
        // Insert時にidに反映
        self.id = rowID
    }
}
```

定義ができたところで、実際にテーブルを作成します。
inDatabase(あるいはinTransaction)内でcreateメソッドを呼び出し、カラムの定義を行います。

```swift
let queue = DatabaseQueue()
try queue.inDatabase{ (db) in
	try db.create(table: User.databaseTableName) { (t) in
		t.column("id", .integer).primaryKey(onConflict: .ignore, autoincrement: true)
		t.column("name", .text).notNull()
		t.column("age", .integer).notNull()
		t.column("score", .integer).notNull()
		t.column("created_at", .date).notNull()
	}
}
```

実際にプロジェクトで使う場合はマイグレーションの定義内でやるほうが良いです。

## fetch
データベースから行を取得する場合は、fetchメソッドを呼び出します。
1行のみ欲しい場合は**fetchOne**、複数の行の場合は**fetchAll**を呼び出します。それぞれのメソッドから直接SQLを呼び出すことも可能です

```swift
//	最初の行を取得
try User.fetchOne(db)
//  プライマリキーが設定されていれば直接指定もできます
try User.fetchOne(db, key: 1)
//	全てのUserを取得
try User.fetchAll(db)
//	SQLを直接実行
try User.fetchAll(db, "SELECT * FROM users")
//	プレースホルダの指定も可能です
try User.fetchOne(db, "SELECT * FROM users WHERE id = ?", arguments: [1], adapter: nil)
```

## select
SQLの"SELECT"にあたる部分です。そのままですね。
取得したいカラムを指定することができます。
カラムに合わせた型に変更する場合は、**asRequest**を呼び出します。

```swift
//	単一のカラム(name)で取得
try User.select(User.Columns.name)
		.asRequest(of: String.self)
		.fetchOne(db)
//	SQL文もOK
try User.select(sql: "name")
		.asRequest(of: String.self)
		.fetchOne(db)
```

## filter
SQLの"WHERE"にあたる部分です。
カラムを利用し条件を指定するか、直接SQLを記述することもできます。

```swift
//	定義したカラムを指定
try User.filter(User.Columns.id == 1).fetchOne(db)
//	SQLで指定
try User.filter(sql: "id = ?", arguments: [1]).fetchOne(db)
```

AND/ORの場合はfilterを繋げるか、以下のような書き方でいけます


```swift
//	filterを繋げるとANDになります
//	SELECT * FROM "users" WHERE (("name" = 'hoge') AND ("id" = 1))
//	と同等
try User.filter(User.Columns.name == "hoge").filter(User.Columns.id == 1).fetchAll(db)

//	上と同じ
try User.filter(User.Columns.name == "hoge" && User.Columns.id == 1).fetchAll(db)

//	ORの場合
//	SELECT * FROM "users" WHERE (("name" = 'hoge') OR ("id" = 2))
//	と同等
try User.filter(User.Columns.name == "hoge" || User.Columns.id == 2).fetchAll(db)
```

IN句の場合はcontainsを使用します。

```swift
//	SELECT * FROM "users" WHERE ("id" IN (1, 2))
//	と同等になります。
try User.filter([1, 2].contains(User.Columns.id)).fetchAll(db)
```

## order/reversed
SQLの"ORDER BY"にあたる部分です。
降順(DESC)を指定したい場合はorderのあとに**reversed**メソッドを呼び出します。

```swift
//	idを昇順で並び替え
try User.order(User.Columns.id).fetchAll(db)
//	idを降順で並び替え
try User.order(User.Columns.id).reversed().fetchAll(db)
//	idを降順で並び替えて、created_atを昇順で並び替える
//	SELECT * FROM "users" ORDER BY "id" DESC, "created_at"
try User.order(User.Columns.id.desc, User.Columns.createdAt).fetchAll(db)
```

## limit
SQLの"LIMIT"にあたる部分です。OFFSETの指定も可能です。

```swift
//	LIMIT句の場合
//	SELECT * FROM "users" LIMIT 10
try User.limit(10).fetchAll(db)

//	OFFSETの指定もできます
//	SELECT * FROM "users" LIMIT 10 OFFSET 5
try User.limit(10, offset: 5).fetchAll(db)
```

## group/having
SQLの"GROUP BY"と"HAVING"にあたる部分です。

```swift
//	ageカラムでGROUPBY
//	SELECT MAX("score") FROM "users" GROUP BY "age"
try User.select(max(User.Columns.score))
		.group(User.Columns.age)
		.asRequest(of: Int.self)
		.fetchOne(db)
		
//	HAVING句の場合
//	SELECT MAX("score") FROM "users" GROUP BY "age" HAVING ("score" > 100)
try User.select(max(User.Columns.score))
		.group(User.Columns.age)
		.having(User.Columns.score > 80)
		.asRequest(of: Int.self)
		.fetchOne(db)

//	SQL文で書きたい場合
//	SELECT MAX(score) FROM "users" GROUP BY age HAVING score > 80
try User.select(sql: "MAX(score)")
		.group(sql: "age")
		.having(sql: "score > 80")
		.asRequest(of: Int.self)
		.fetchOne(db)
```


## Insert/Update/Delete
データを挿入する場合はモデルからinsertが生えているのでこれを呼び出します。
今回の例ではUserのidにnilを指定していますが、didInsertを実装しているため、InsertされるとidにRowIDが割り当てられるようになっています。

なお**save**というメソッド使うと**DBにまだ挿入されていない場合はInsert、挿入されていればUpdate**してくれます。

```swift
try pool.write { (db) in
	let user = User(name: "hoge")
	try user.insert(db)
	print(user.id)
	// Optional(1)
}
// あるいは
// try user.save(db)
```

UpdateとDeleteの場合も同様です。

```swift
//var user = User(id: nil, name: "hoge")
//try user.insert(db)
user.name = "fuga"
// 更新
try user.update(db)
// あるいは
// try user.save(db)

// 削除
try user.delete(db)
```

注意点として、**update**メソッドは、**変更を加えていないカラムに対してもSETを行います。**
以下のSQLは実際にGRDBが発行するSQLです。

```sql

UPDATE "users" SET "name"='fuga', "created_at"='2018-02-16 09:25:30.543' WHERE "id"=1
```

変更を加えていないカラムに対しても更新を行ってしまうのはよろしくないので、変更を加えたカラムのみ更新を行うようにします。

Recordクラスを継承していれば、**updateChanges**を呼び出す方法が一番簡単です。

```swift
try user.updateChanges(db)
```

update時にカラムを指定する方法もあります。

```swift
try user.update(db, columns: [User.Columns.name])
```


## トランザクションと同時並行アクセス
読み込むだけ、書き込むだけなら**read**, **write**メソッドを利用するとすっきり書けます。

```swift
let user = try queue.read { (db) in
    try User.fetchOne(db)
}
```

複数、あるいは大量にInsertやUpdateを行う場合はwriteではなく**writeInTransaction**を呼び出しトランザクション内で処理を行うようにしてください。

```swift
try pool.writeInTransaction { (db) in
    //  ここで書き込み
    return .commit
}
```
また、データベースに書き込んだ直後にテーブルからデータを取得したい場合があると思います。
以下の例は良くない例で、writeとreadの間に別スレッドから書き込みが行われた場合、fetchCountの値が正しい値になりません。

```swift
//	以下の処理の流れは良くない例です。
try pool.write { (db) in
    var user = User(name: "hoge")
    user.age = 20
    try user.save(db)
}

// countが1になるとは限らない(この間に他のスレッドから書き込まれた場合など)
let count = try pool.read { (db) in
    try User.fetchCount(db)
}
```

この場合、**readFromCurrentState**を使用します

```swift
//	良い例
try pool.write { (db) in
    var user = User(name: "hoge")
    user.age = 20
    try user.save(db)
    
    //	readFromCurrentStateを呼び出しその中で読み取りを行う
    try pool.readFromCurrentState { db in
        let count = try? User.fetchCount(db)
    }
}
```


GRDBのreadmeには、同時並行性を保証するためにこういう実装をしてね。という記載がありますので、詳しくはこちらを読んでください。
[GRDB.swift#Concurrency](https://github.com/groue/GRDB.swift#concurrency)

## オブザーバ
GRDBでは、オブザーバを通して全てのデータベース上のInsert/Update/Deleteおよびcommit/rollbackのイベントを受け取ることができます。

利用するには、**TrsansactionObserverプロトコル**を継承したクラスを作成し、DatabasePool/DatabaseQueueにaddします。
inTransaction(inDatabase)内でもaddできます。

observesメソッドでtrueを返すと**databaseDidChange**が呼ばれるようになります。
引数にeventKindがあり、これを参照して呼ぶか呼ばないか判断することができます。

```swift
class TestObserver: TransactionObserver {
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
        //	trueにすると、databaseDidChangeが呼ばれます
        //	eventKind.tableNameをみてイベント通知するかしないかといった処理が行えます。
        return true
    }
    
    //	insert/update/deleteの際に呼ばれます
    func databaseDidChange(with event: DatabaseEvent) {
        print("did change")
        print("tableName: \(event.tableName)")
        print("rowId: \(event.rowID)")
    }
    
    //	commit直前に呼ばれます
    func databaseWillCommit() throws {
        print("will commit")
    }
    
    //	commit直後に呼ばれます
    func databaseDidCommit(_ db: Database) {
        print("did commit")
    }
    
    //	rollback時に呼ばれます
    func databaseDidRollback(_ db: Database) {
        print("did rollback")
    }
}

//  Queue/Poolに対してadd
let queue = DatabaseQueue()
let observer = TestObserver()
queue.add(transactionObserver: observer)
// queue.remove(transactionObserver: observer)

//
//	transaction内でも可能です
try queue.inTransaction { (db) in
    db.add(transactionObserver: observer)
    ...
}
```

## マイグレーション
GRDBは単体でマイグレーション機能がついています。
DatabaseMigratorの**registerMigration**でテーブルの定義や処理を記述し、**migrate**を呼び出すだけでイイ感じにやってくれます。

```swift
var migrator = DatabaseMigrator()

//	v1で行う処理(この場合はテーブル作成)
migrator.registerMigration("v1") { (db) in
    try db.create(table: User.databaseTableName) { (t) in
        t.column("id", .integer).primaryKey(onConflict: .ignore, autoincrement: true)
        t.column("name", .text).notNull()
        t.column("age", .integer).notNull()
        t.column("score", .integer).notNull()
        t.column("created_at", .date).notNull()
    }
}

//	v2で行う処理(Userテーブルにupdated_atを追加)
migrator.registerMigration("v2") { (db) in
    try db.alter(table: User.databaseTableName) { (t) in
        t.add(column: "updated_at", .date)
    }
}

//	マイグレーション
try migrator.migrate(queue)

//	直接バージョンを指定することもできます
//	※既にマイグレーション済みのバージョンを指定 or ロールバックすることはできないので注意
//	try migrator.migrate(queue, upTo: "v2")
```

## RxGRDB
GRDBはRx向けにRxGRDBを別途用意されています。
RxSwiftでアプリを作ることが多いので感謝感激雨あられですね。

[RxSwiftCommunity/RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)

```swift
User.all().rx
	.fetchAll(in: queue)
    .subscribe(onNext: { (users) in
        //	ここで処理
    })

//	ageが20のユーザーのみに絞る
User.filter(User.Columns.age == 20)
	.rx.fetchAll(in: queue)
    .subscribe(onNext: { (users) in
        //	ここで処理
    })
```

changesを使うとオブザーバーと同じようにデータベースの変更にも対応できます。

```swift
User.all().rx
	.changes(in: queue)
    .subscribe(onNext: { (users) in
        //	変更があった
    })
```

## 感想
機能が充実していてとてもイイ感じなので、RealmにしたいけどSQLiteに依存しすぎてリプレースがキツいという方、FMDBよりもっと手軽にやりたい方にはぜひおすすめしたいライブラリです！

## 参考
[groue/GRDB.swift](https://github.com/groue/GRDB.swift)
[RxSwiftCommunity/RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)