Node.jsでLevelDB

Node.js+Expressでタスク管理アプリを作った時のメモ。
今回はデータストアに選択したLevelDBの話。
bbpink/node-shpapad · GitHub

LevelDBを選択した背景

  • 単純なタスク管理アプリだから高機能なデータストアが必要なかった
  • Node.js Bindingsが存在する
  • クソ速そう

使い方

まずはパッケージをインストールします。

npm install levelup
npm install leveldown

shpapadではpackage.jsonに書かれています。
leveldownはLevelDBのレイヤーが低い部分で、サーバサイドで使うのであればあったほうが良いみたいです。

DBへはput、get、delでCRUDを行います。

var levelup = require("levelup");
var db = levelup("./data/db/shpapad_db"); //データを保存するディレクトリを指定してコンストラクト

var hashedID = "hashed!"; //ハッシュ化されたユーザID

//ユーザIDが存在する場合はログイン、存在しない場合はユーザを作成
db.get(hashedID, function(err, value) {
  if (err) {
    if (err.notFound) {
      //create new user
      db.put(hashedID, "", function(err) {
        if (err) {
          res.redirect(303, "/login");
        } else {
          //auth OK
          req.session.user = userid;
          res.redirect(303, "/list");
        }
      });
    } else {
      //キー存在なし以外のなんかのエラー
      res.redirect(303, "/login");
    }
  } else {
    //auth OK
    req.session.user = userid;
    res.redirect(303, "/list");
  }
});

データ構造

shpapadに必要なデータは「ユーザ」「リスト」「タスク」の3種類であり、
「ユーザ」が頂点に位置する木構造で表現する必要があります。

LevelDBのデータはキーでソートされているため、キー値を"!"で区切って木構造を表現しました。
「キー:バリュー」で表現した場合、上記のデータはこんな感じになります。

ハッシュ化されたユーザAさんのID:""
ハッシュ化されたユーザAさんのID!リスト作成時UnixDateTime:{name:リスト名(明日やること),リストに紐付くタスク一覧の件数(1)}
ハッシュ化されたユーザAさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(掃除する)}
ハッシュ化されたユーザBさんのID:""
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime:{name:リスト名(好きな人),リストに紐付くタスク一覧の件数(3)}
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(篠崎愛)}
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(星野源)}
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(ウーピー・ゴールドバーグ)}
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime:{name:リスト名(ドラッグストアで買うものリスト),リストに紐付くタスク一覧の件数(1)}
ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(トイレットペーパー)}

また、db.get()ではキーを指定して1件ずつしかデータを取得できないため、db.createReadStream()を使用してキー値を範囲指定でがさっと取得します。

リストを取得する
app.get("/list", function(req, res) {
  if (req.session.user) {
    var hashedID = crypto.createHash("sha256").update(req.session.user).digest("hex");

    //get lists
    var lists = [];
    db.createReadStream({gt: hashedID + "!", lte: hashedID + "!9999999999999" })
      .on("data", function(data) {
        var obj = JSON.parse(data["value"]);
        lists.push({key:data["key"], name:obj["name"], count:obj["count"]});
      })
      .on("end", function(data) {
        res.render("list", { menu: [{href: "/list", value:"リスト"}], logoutable: true, lists: lists });
      });

  } else {
    res.redirect(303, "/login");
  }
});

1人のユーザに紐付くリストを取得する場合、キー値の検索条件は"ハッシュ化されたユーザID!" から"ハッシュ化されたユーザID!UnixDateTimeのMAX値" の範囲です。
タスクの場合も階層が1つ深くなるだけでリストの場合と同様の処理となります。

続く!(たぶん)