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-clibabel-preset-envを入れます。

npm i -S babel-cli babel-preset-env
(npm i -S は、npm install --saveと同じ)

package.jsonのscriptsを以下のように書き換えます。

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

実装

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

呼び出し側の実装

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

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

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を実行すると以下のようにコンソール出力されます。

--- create table ---
--- save 10 users ---
--- count after saving ---
10
--- update 3 users ---
--- list ---
[ User { account: 'account0', name: 'name0', email: '[email protected]' },
  User { account: 'account1', name: 'name1', email: '[email protected]' },
  User { account: 'account2', name: 'name2', email: '[email protected]' },
  User { account: 'account3', name: 'new-name3', email: '[email protected]' },
  User { account: 'account4', name: 'new-name4', email: '[email protected]' },
  User { account: 'account5', name: 'new-name5', email: '[email protected]' },
  User { account: 'account6', name: 'name6', email: '[email protected]' },
  User { account: 'account7', name: 'name7', email: '[email protected]' },
  User { account: 'account8', name: 'name8', email: '[email protected]' },
  User { account: 'account9', name: 'name9', email: '[email protected]' } ]
--- delete 10 users ---
--- count after deleting ---
0

共通クラスDBCommonの作成

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

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

usertable.js
import sqlite3 from "sqlite3"

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

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

Userクラス(エンティティ)

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

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 ... クエリを実行して1番目の結果だけにcallbackを実行する
- all ... クエリを実行して全ての結果を1度だけcallbackを実行する(メモリ上に展開されるため取り扱い注意)
- each ... クエリを実行してそれぞれの結果に対してcallbackを実行する。 (本例では未使用)

テーブル作成 - create table if not exists

SQLiteには、テーブルがなければ作るという便利なCREATE TABLE構文があるのでそれを使います。

- create table if not exist <table_name> (<column_definitions>)

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

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というメソッドとして実装しています。

  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をつかって最初の結果であるカウントだけ取得します。

  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の普通のオブジェクトの配列となって帰ってきます。
ページングするため、順序を一意になるようにしています。

  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文を実行するだけです。

  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で扱えば、呼び出し側は簡単に記述できる。

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

以上。