SQLite.swift でSQLiteを操作する

# まえがき
Swiftに入門中の友人に向けて説明している記事になります。

# SQLite.swift
https://github.com/stephencelis/SQLite.swift

# 導入手順
今回はSwift Package Managerで導入する

1. Xcodeから File > Swift Package > Add Package Dependency を選択
![スクリーンショット 2021-06-06 16.29.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/0656ebc2-c42e-083f-59f0-1a88f75005dc.png)

2. https://github.com/stephencelis/SQLite.swift.git で検索
![スクリーンショット 2021-06-06 16.32.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/c99e79d2-4732-1786-b9b2-9b31ee63ee3a.png)

3. Versionを選択（今回は特に何も考えずデフォルト設定）
![スクリーンショット 2021-06-06 16.33.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/4ccc7239-75a1-6ada-e1fa-d36a52d34bbb.png)

4. Finish!
![スクリーンショット 2021-06-06 16.34.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/50ce4bc4-54ed-5a88-2f63-7dfc253b59ac.png)

XcodeのUI上で入れたPackageも確認できる！
![スクリーンショット 2021-06-06 16.39.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/39453bb4-2418-88e1-bee4-8aa6a84f16be.png)

# SQLiteを操作してみる

今回やることは以下
- データベースファイルを作成
- userテーブルを作成
- userの追加（Insert）
- userの検索（Select）

### 1. データベースファイルを作成
Databaseクラス作成を作成して下記を実装
（ViewControllerクラスで実装してもいいけど分けておいたほうがわかりやすそうなので今回はクラスを分ける）

```swift:Database.swift
import Foundation
import SQLite

let FILE_NAME = "sample.db"

class Database {
  var db: Connection
  init() {
    // DBファイルの作成先のパスを生成
    let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path
    // DBファイル作成/開く
    db = try! Connection(filePath)
  }
}
```

ちゃんとViewController側で初期化することを忘れずに。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
  var database = Database()
 
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}
```

実行後Finderでディレクトリを見てみると無事ファイルが生成されています。
![スクリーンショット 2021-06-06 17.15.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/234dd82e-a4fa-2a9d-7cd3-a0eca7dadb2e.png)

### 2.userテーブルを作成

```Database.swift
class Datastore {
  var db: Connection
  init() {
    // DBファイルの作成先のパスを生成
    let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path
    // DBファイル作成/開く
    db = try! Connection(filePath)

    // DBにテーブル作成
    let users = Table("users")
    do {
      try db.run(users.create { t in
          t.column(Expression<Int64>("id"), primaryKey: true)
          t.column(Expression<String?>("name"))
          t.column(Expression<String>("email"), unique: true)
      })
    } catch {}
  }
}
```

`do {} catch {}` でくくってる理由は２回目移行で動かす時にすでにテーブルが作成されている状態になるのでもう一度作ろうとしてエラーになるのでそれを無視するため。（もっといい書き方ありそうなきもするけど一旦これでいいかな）

SQLiteの中を見れるGUIで確認するとちゃんと作成できてることが確認できる。
![スクリーンショット 2021-06-06 17.30.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/0628af91-5acc-6b8b-c682-6b8dd56e02d7.png)

ごちゃごちゃしてきたのでUserDatastoreってクラス作ってUserテーブルはそっちで触るような実装に整理することにした。

```swift:Database.swift
class Database {
  let db: Connection
  let userDatastore: UserDatastore
  
  init() {
    // DBファイルの作成先のパスを生成
    let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path
    // DBファイル作成/開く
    db = try! Connection(filePath)
    
    // UserDatastoreを初期化
    userDatastore = UserDatastore(db: db)
  }
}
class UserDatastore {
  private let table = Table("users")
  private let id = Expression<Int64>("id")
  private let name = Expression<String>("name")
  private let email = Expression<String>("email")
  private let db: Connection

  init(db: Connection) {
    self.db = db
    do {
      try self.db.run(table.create { t in
        t.column(Expression<Int64>("id"), primaryKey: true)
        t.column(Expression<String>("name"))
        t.column(Expression<String>("email"), unique: true)
      })
    } catch {}
  }  
}
```

### 3. userの追加（Insert）
初期データでも入れてみる。
userテーブルの初期データというていで実装する（マイグレーション）

```swift:Database.swift
class Database {
  省略...
}

class UserDatastore {
  private let table = Table("users")
  private let id = Expression<Int64>("id")
  private let name = Expression<String>("name")
  private let email = Expression<String>("email")
  private let db: Connection
  
  init(db: Connection) {
    self.db = db
    do {
      try self.db.run(table.create { t in
        t.column(Expression<Int64>("id"), primaryKey: true)
        t.column(Expression<String>("name"))
        t.column(Expression<String>("email"), unique: true)
      })
      // 初期データを入れる
      let migrationItems = [
        ["name": "Alice", "email":"alice@mac.com"],
        ["name": "Bob", "email":"bob@mac.com"]
      ]
      migrationItems.forEach { row in
        try? insert(name: row["name"]!, email: row["email"]!)
      }
    } catch {}
  }
  
  func insert(name: String, email: String) throws {
    let insert = table.insert(self.name <- name, self.email <- email)
    try db.run(insert)
  }
}
```
内容的にはinsertの関数を作成、データでぐるぐるループしてinsertの関数を実行する。

`try self.db.run(table.create...` の実行で２回目移行はエラーが発生して後続の処理（ループしてインサートする処理）が動かなくなるので初回の１回目しか実行されないため初期データが２重で入ることもない。
（ちゃんとマイグレーションするなら作り込んだほうがいいかもだけど一旦これで。）

中みるとちゃんとデータが入ってる。（２回実行されてもデータが増えない）
![スクリーンショット 2021-06-06 18.12.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122008/d8b5f760-8802-8abe-9c47-4a1d574c2c30.png)

データが入らない場合はsample.dbのファイルごと一回消してやってみる。


### 4.userの検索（Select）
Userテーブルに入ってるすべてのデータを取得してTableViewで表示してみる

```swift:Database.swift
class UserDatastore {
  省略...    
  func find() -> [User] {
    var results = [User]()
    // エラー起こした場合は空の配列を返却
    guard let users = try? db.prepare(table) else {
      return results
    }
    for row in users {
      results.append(User(id: row[self.id], name: row[self.name], email: row[self.email]))
    }
    return results
  }  
}

class User {
  let id: Int64
  let name: String
  let email: String
  init(id: Int64, name: String, email: String) {
    self.id = id
    self.name = name
    self.email = email
  }
}
```

Userクラスを追加したのとUserDatastoreクラスにfindの関数を実装

```swift:ViewController.swift
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
  
  var database = Database()
  var datas = [User]()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    datas = database.userDatastore.find()
  }
    
  // Sectionの個数
  func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }
    
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return datas.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let identifier = "HogeCell"
    let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
    cell.textLabel?.text = datas[indexPath.row].name
    return cell
  }
}
```

ユーザIDで検索もしたい。

```swift:Database.swift
class UserDatastore {
  省略...    
  func findById(id: Int64) -> [User] {
    var results = [User]()
    // エラー起こした場合は空の配列を返却
    guard let users = try? db.prepare(table.where(self.id == id)) else {
      return results
    }
    for row in users {
      results.append(User(id: row[self.id], name: row[self.name], email: row[self.email]))
    }
    return results
  }
}
```

```swift:ViewController.swift
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
  
  var database = Database()
  var datas = [User]()
  
  override func viewDidLoad() {
    super.viewDidLoad()
//    datas = database.userDatastore.find()
    datas = database.userDatastore.findById(id: 2)
  }
  省略...
```

# 感想
初めてちゃんとQita書いた気がするけどなんかちょっとモチベーション上がったから定期的にかけるといいな。
Swift Package Manager 初めて使ってみたがわかりやすくて好き。