Node.jsでSQLiteデータベースのCRUD処理を行う

# 概要
本記事では、Node.jsでSQLite3のデータベースのCRUD処理をES2015(ES6)で実装した例を示します。

sqlite3というNodeのモジュールを使用しています。このライブラリはすべて非同期のAPIであるため、ES2015のawait/asyncを使って呼び出すようにしています。

# 準備

## プロジェクト作成
Node.jsでプロジェクトを作るには以下のコマンドを実行します。

```
mkdir usertable
cd usertable
npm init
(適当に答える, 以下の例はentry pointだけmain.jsにしています)
```

## ES2015を使えるようにする
babelとはES2015のファイルをJSに変えるトランスパイラです。
Nodeで何か書くときは、以下のようにしてbabelを使えるようにすると大変便利です。

このプロジェクトでES2015を扱うには`babel-cli`と`babel-preset-env`を入れます。  

```
npm i -S babel-cli babel-preset-env
(npm i -S は、npm install --saveと同じ）
```
package.jsonのscriptsを以下のように書き換えます。

```json:package.json
  "scripts": {
    "start": "babel-node main.js --presets env"
  },
```
nodeで実行するのではなく、babelでトランスパイルして実行するという意味です。

## node-sqlite3 のinstall
SQLiteは、バイナリファイルのデータベースです。   
Macの場合デフォルトでインストールされています。Windowsの場合はインストールが必要になります。
NodeでSQLiteのデータベースを扱うには、以下のsqlite3モジュールを使います。
- https://github.com/mapbox/node-sqlite3 

```
npm i -S sqlite3
```

# 実装
実装は、２ファイルです。CRUD操作を呼び出す`main.js`とデータベース操作をする`usertable.js`です。

## 呼び出し側の実装
先に`main.js`のコードをお見せし、その後データベース操作のコードを説明します。
Userというデータを１０件作成し、３件を更新、カウントしてリストアップしたのち、削除するという処理です。

検索はページング処理しています。（この例は結局メモリにすべてあげているため、あまり意味はないです）


```javascript:main.js
import UserTable, { DBCommon, User } from "./usertable"

const main = async () => {
  DBCommon.init()
  console.log("--- create table ---")
  await UserTable.createTableIfNotExists()

  console.log("--- save 10 users ---")
  for (let i = 0; i < 10; i++) {
    const user = new User(`account${i}`, `name${i}`, `aaa${i}@bbb.ccc`)
    await UserTable.save(user)
  }

  console.log("--- count after saving ---")
  let count = await UserTable.count()
  console.log(count)

  console.log("--- update 3 users ---")
  for (let i = 3; i < 6; i++) {
    const user = new User(`account${i}`, `new-name${i}`, `xxx${i}@yyy.zzz`)
    await UserTable.save(user)
  }

  console.log("--- list ---")
  let start = 0
  const limit = 3
  let users = []
  while (true) {
    users = users.concat(await UserTable.list(start, limit))
    start = start + limit
    if (start >= count) break
  }
  console.log(users)

  console.log("--- delete 10 users ---")
  users.forEach(user => { UserTable.delete(user) })

  console.log("--- count after deleting ---")
  count = await UserTable.count()
  console.log(count)
}
```

`npm start`を実行すると以下のようにコンソール出力されます。

```console
--- create table ---
--- save 10 users ---
--- count after saving ---
10
--- update 3 users ---
--- list ---
[ User { account: 'account0', name: 'name0', email: 'aaa0@bbb.ccc' },
  User { account: 'account1', name: 'name1', email: 'aaa1@bbb.ccc' },
  User { account: 'account2', name: 'name2', email: 'aaa2@bbb.ccc' },
  User { account: 'account3', name: 'new-name3', email: 'xxx3@yyy.zzz' },
  User { account: 'account4', name: 'new-name4', email: 'xxx4@yyy.zzz' },
  User { account: 'account5', name: 'new-name5', email: 'xxx5@yyy.zzz' },
  User { account: 'account6', name: 'name6', email: 'aaa6@bbb.ccc' },
  User { account: 'account7', name: 'name7', email: 'aaa7@bbb.ccc' },
  User { account: 'account8', name: 'name8', email: 'aaa8@bbb.ccc' },
  User { account: 'account9', name: 'name9', email: 'aaa9@bbb.ccc' } ]
--- delete 10 users ---
--- count after deleting ---
0
```

## 共通クラスDBCommonの作成
DB操作に関する処理はすべてusertable.jsにまとめます。DBCommonは別に切り出しても良いかもしれません。

複数のテーブル操作を想定して、dbを扱うための共通クラスを作成します。
シングルトン的に１つのファイルに対して１つのインスタンスを使う想定です。

```javascript:usertable.js
import sqlite3 from "sqlite3"

// ファイルに対応した、ただ１つのインスタンス
let database

export class DBCommon {
  static init() {
    database = new sqlite3.Database("user.sqlite3")
  }
  static get() {
    return database
  }
}
```

## Userクラス（エンティティ）
Userクラスは、シンプルにアカウント、名前、E-mailを持つクラスとします。

```javascript
export class User {
  constructor(account, name, email) {
    this.account = account
    this.name = name
    this.email = email
  }
}
```

# CRUDの実装
CRUD処理は、すべてUserTableというクラスのstaticメソッドで実装します。

sqlite3モジュールの以下のAPIを使用しています。
- **open** ... データベース作成またはオープン
- **serialize** ... 内部の処理を同期的に実行する
- **run** ... SQLの実行（DDLやinsert/deleteなど）

クエリ系 ... callbackを渡して非同期で実行します。
  - **get** ... クエリを実行して１番目の結果だけにcallbackを実行する
  - **all** ... クエリを実行して全ての結果を１度だけcallbackを実行する（メモリ上に展開されるため取り扱い注意）
  - **each** ... クエリを実行してそれぞれの結果に対してcallbackを実行する。 （本例では未使用）

## テーブル作成 - create table if not exists
SQLiteには、テーブルがなければ作るという便利なCREATE TABLE構文があるのでそれを使います。  
- `create table if not exist <table_name> (<column_definitions>)`

SQLの実行は、**run**を使います。

```javascript
const userTableName = "users"

export default class UserTable {
  static async createTableIfNotExists() {
    const db = DBCommon.get()
    return new Promise((resolve, reject) => {
      try {
        db.serialize(() => {
          db.run(`create table if not exists ${userTableName} (
            account text primary key,
            name text,
            email text
          )`)
        })
        return resolve()
      } catch (err) {
        return reject(err)
      }
    })
  }
```

## データ作成/更新 - insert or replace
SQLiteには`insert or replace`というデータ更新の構文があるためそれを`save`というメソッドとして実装しています。

```javascript
  static async save(user) {
    const db = DBCommon.get()
    return new Promise((resolve, reject) => {
      try {
        db.run(`insert or replace into ${userTableName} 
        (account, name, email) 
        values ($account, $name, $email)`,
          user.account, user.name, user.email
        )
        return resolve()
      } catch (err) {
        return reject(err)
      }
    })
  }

```


## カウント
`select count(*) from <table_name>`を呼ぶだけです。
**get**をつかって最初の結果であるカウントだけ取得します。

```javascript
  static async count() {
    const db = DBCommon.get()
    return new Promise((resolve, reject) => {
      db.get(`select count(*) from ${userTableName}`, (err, row) => {
        if (err) return reject(err)
        return resolve(row["count(*)"])
      })
    })
  }
```

## 検索
**all**を使って全ての結果を操作するため、offsetとlimitを指定するようにしています。

callbackの引数であるrowsは、JSの普通のオブジェクトの配列となって帰ってきます。
ページングするため、順序を一意になるようにしています。

```javascript
  static async list(offset, limit) {
    const db = DBCommon.get()
    const result = []
    return new Promise((resolve, reject) => {
      db.serialize(() => {
        db.all(`select account, name, email from ${userTableName}
        order by account limit ${limit} offset ${offset}`,
          (err, rows) => {
            if (err) return reject(err)
            rows.forEach(row => {
              result.push(new User(row["account"], row["name"], row["email"]))
            })
            return resolve(result)
          })
      })
    })
  }
```

## データ削除
**run**を使って`delete`文を実行するだけです。

```javascript
  static async delete(user) {
    const db = DBCommon.get()
    return new Promise((resolve, reject) => {
      try {
        db.run(`delete from ${userTableName} where account = $account`, user.account)
        return resolve()
      } catch (err) {
        return reject(err)
      }
    })
  }
```

# まとめ

まとめです。
- ES2015をNodeで使えるようにするには、babel-cli, babel-preset-envを入れる
- node-sqliteの操作は、open, run, get, all, eachで行う
- 非同期処理は、await/asyncで扱えば、呼び出し側は簡単に記述できる。

１番目が地味にメモしておきたかったのもこの記事を書いた理由です。

以上。