Swift 4 GRDB.swift(SQLite)を使ってみた

## はじめに
Swift4で開発しているアプリから、SQLiteのデータベースを扱う必要があった為、
[GRDB.swift](https://github.com/groue/GRDB.swift)というライブラリを使ってみた。

最新の環境で動くサンプルコードが、ググっても情報が少なかったので、
本記事で基本的な使用方法を記載します。

## 導入
CocoaPodsからインストール

```text:Podfile
 pod 'GRDB.swift'
```

## エンティティ
サンプルコード用にユーザテーブルを定義します。
構造体にはCodable、RowConvertible、MutablePersistableのプロトコルを追加します。

```swift:UserTable.swift
struct UserTable : Codable, RowConvertible, MutablePersistable {
    // 今回は全カラムNotNullで
    var id: Int
    var name: String
    var age: Int
    var lastUpdate: Int64

    static var databaseTableName: String {
        return "tbl_user"  // テーブル名
    }
    
    static func create(_ db: Database) throws {
        try db.create(table: databaseTableName, body: { (t: TableDefinition) in
            t.column("id", .integer).primaryKey(onConflict: .replace, autoincrement: false)
            t.column("name", .text).notNull()
            t.column("age", .integer).notNull()
            t.column("lastUpdate", .integer).notNull()
        })
    }
}
```
複合主キーを設定する場合は下記のようにします。

```swift
t.column("id", .integer).notNull()
t.column("name", .text).notNull()
t.primaryKey(["id", "name"], onConflict: .replace)
```

## ヘルパークラス
各テーブルの生成処理を一元管理する為にヘルパークラスを作成します。

```swift:DatabaseHelper.swift
class DatabaseHelper {
    
    private struct Const {
        /// データベースファイル名
        static let DB_FILE_NAME = "application.db"
        
        /// データベースファイルの格納先
        static let DB_FILE_PATH = "\(File.DocumentsPath)/\(DB_FILE_NAME)"
    }
    
    /// イニシャライザ
    init() {
        self.createDatabase()
    }
    
    /// データベースの操作
    func inDatabase(_ block: (Database) throws -> Void) -> Bool {
        do {
            // 初回実行時にデータベースファイルを生成する
            let dbQueue = try DatabaseQueue(path: Const.DB_FILE_PATH)
            try dbQueue.inDatabase(block)
        } catch let error {
            Log.e("Database Error: \(error.localizedDescription)")
            return false
        }
        return true
    }
    
    /// データベースの生成処理
    private func createDatabase() {
        if File.isExists(Const.DB_FILE_PATH) {
            // 既にファイルが作成済みなら何もしない
            return
        }
        let result = inDatabase { (db) in
            // テーブルの作成処理
            try UserTable.create(db)
            
            // ※複数テーブルがある場合はここに追加する
        }
        if !result {
            Log.e("create failed for Database.")
            File.remove(Const.DB_FILE_PATH)
        }
    }
}
```

## データの取得
##### 全件取得

```swift
var result :[UserTable] = []
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    result = try UserTable.fetchAll(db)
}
// 出力
result.forEach { (it) in
    Log.d("\(it.id) - \(it.name) - \(it.age) - \(it.lastUpdate)")
}
```

##### 条件指定
filterメソッドのSQL指定を使うと、引数にWHEAE句を設定できる。

```swift
let helper = DatabaseHelper()
var entity: UserTable?

helper.inDatabase { (db) in
    entity = try UserTable.filter(sql: "id = 3").fetchOne(db)
}
// 出力
if entity != nil {
    Log.d("\(entity!.id) - \(entity!.name) - \(entity!.age) - \(entity!.lastUpdate)")
}
```

ソースにSQLを書きたくない場合は、カラム名指定で条件を設定できる。

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    try UserTable
        .filter(Column("id") == 2)
        .filter(Column("age") == 22)
        .fetchOne(db)
}
```


## データの挿入/更新

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    // エンティティを設定
    var entity = UserTable(id: 1,
                           name: "hoge",
                           age: 11,
                           lastUpdate: Date.currentTimeMillis())
    
    // 既に存在しているレコードなら上書きを行う
    try entity.insert(db)
}
```

## データの削除
##### 全件削除

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    try UserTable.deleteAll(db)
}
```

##### 条件指定

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    try UserTable.filter(sql: "id = 1").fetchOne(db)
    try UserTable
        .filter(Column("id") == 2)
        .filter(Column("age") == 22)
        .fetchOne(db)
}
```

## トランザクション

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    // トランザクション開始
    try db.beginTransaction()
    do {
        // 更新処理
            .
            .
            .
        
    } catch let error {
        // ロールバック
        try db.rollback()
        throw error
    }
    // コミット
    try db.commit()
}
```

## 生SQLの実行
##### 取得
```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    let rawSQL = """
        SELECT
          *
        FROM
          \(UserTable.databaseTableName)
    """

    let rows = try Row.fetchAll(db, rawSQL)

    // 出力
    rows.forEach { (row) in
        let id = row["id"]!
        let name = row["name"]!
        Log.d("\(id) - \(name)")
    }
}
```

##### 更新

```swift
let helper = DatabaseHelper()

helper.inDatabase { (db) in
    let rawSQL = """
        UPDATE
          \(UserTable.databaseTableName)
        SET
          name = 'foo'
        WHERE
          id = 3
    """

    try db.execute(rawSQL)
}
```

## プレースホルダ
SQL文の一部をパラメータに置き換える方法です。

GRDB.StatementArgumentsには配列を指定し、先頭から順にSQL内の「？」割り当てられます。

```swift
let rawSQL = """
    SELECT
      *
    FROM
      \(UserTable.databaseTableName)
    WHERE
      name = ?
"""
let args = GRDB.StatementArguments(["hoge"])
let rows = try Row.fetchAll(db, rawSQL, arguments: args, adapter: nil)
```

## 終わりに
基本的なデータ操作は上記のサンプルで確認できるかと思います。
まだまだ機能が多くて使いこなせておりませんが、手軽にSQLiteが扱えました。