node.js から MongoDB にアクセス (Mongoose の紹介)

node.js から MongoDB にアクセススためのライブラリに Mongoose があります.今回はこれを紹介しようと思います.O/R Mapper っぽく使えるように設計されており,既存の O/R Mapper を使ったことがある人にとっては,比較的わかりやすい仕様です.

見出し

  • Mongoose とは?
  • インストール
  • 何はともあれ使い方を
  • Schema 定義について
  • ドキュメント生成 (保存)
  • ドキュメント読み取り
  • ドキュメント更新
  • ドキュメント削除
  • Embedded Document
  • 終わりに

Mongoose とは?

node.js 向けに開発された MongoDB アクセスライブラリです.

Mongoose is a MongoDB object modeling tool designed to work in an asychronous environment.
Mongoose とは,非同期環境において動作するよう設計された,MongoDB オブジェクト用モデリングツールである.[引用]

MongoDB に格納するモデルを定義し,O/R Mapper の様なノリで操作できるようになります.なお,あくまで "ノリ" であって,node.js で動く以上はイベント駆動なんだということは留意しておくべきです.

インストール

$ npm install mongoose

パッケージ管理があると楽だ.

何はともあれ使い方を

MongoDB のホストが localhost,データベースが sample_db であったとします.

var mongoose = require('mongoose');

// 定義フェーズ
var Schema   = mongoose.Schema;

var UserSchema = new Schema({
  name:  String,
  point: Number
});
mongoose.model('User', UserSchema);

// 使用フェーズ
mongoose.connect('mongodb://localhost/sample_db');

var User = mongoose.model('User');
var user = new User();
user.name  = 'KrdLab';
user.point = 777;
user.save(function(err) {
  if (err) { console.log(err); }
});

// ※注意:イベント駆動

User.find({}, function(err, docs) {
  for (var i=0, size=docs.length; i<size; ++i) {
    console.log(docs[i].doc.name);
  }
});

(個人的に,JavaScript において 'new' を使うオブジェクト生成は嫌いなのですが...今回はこらえて流儀に従います.)

上記のように User スキーマを定義 → save の流れでドキュメントが格納されます.なお,MongoDB の各ドキュメントに割り振られる '_id' は,そのまま 'user._id' で取得可能です (Schema.ObjectId オブジェクトとして扱われる).

find には (query, fields, options, callback) の 4 引数があります.query は必須,残りは省略可能です.ただし,callback を省略すると FindQuery オブジェクトが返ってきます.このオブジェクトはクエリのコンテキストであり,DataMapper (PofEAA のパターン名ではなく,ruby 向けの DataMapper) でいうところの Scopes and Chaining に相当する操作を可能とします (where や sort,skip,limit 等を積み重ねて,exec すると実際にクエリが発行される).
他にも,findOne,findById 等ありますが,詳しくは API の Model を参照してください.

上記サンプルでは save のあとすぐに find していますが,これは必ずしもうまくいきません (クエリが submit されても,どちらが先に終わるかわからないため).実際には save のハンドラ内で登録されるクロージャの中で find をすることになります.この辺は Reactor Pattern やらイベント駆動やらではおなじみです.

補足ですが,connect の呼び出しは使用直前まで遅らせることができます.

Mongoose buffers all the commands until it's connected to the database.
Mongoose は,データベースへの接続が確立されるまで,全てのコマンドをバッファする.[引用]

Schema 定義について

Validation をかけたり,default 値を設定できます (Model Definition).ここら辺は DataMapper の "property :hoge ..." の感じと同じです.

var User = new Schema({
  first_name: { type: String, required: true },
  last_name:  { type: String, required: true },
  point:      { type: Number, min: 0, default: 0 },
  created:    { type: Date, default: Date.now },
  updated:    { type: Date, default: Date.now }
});

Validation は save のタイミングでかかります.上記の例では,'first_name','last_name' は値を指定しないとエラー,'point' は 0 未満を指定するとエラー,といった感じです.
default 値は new のタイミングでオブジェクトに積まれます.上記の例では,'point' は 0,'created','updated' は現在の日時が格納されます.

MongoDB はスキーマレスが特徴の一つですが,要件として Validation をかけたい場合もあるでしょう.また,default 値指定は便利です.

ドキュメント生成 (保存)

先ほど出てきた通り.

User = mongoose.model('User');
var user = new User();
user.name  = 'KrdLab';
user.point = 777;

// user = new User({ name: 'KrdLab', point: 777 }); も可能

user.save(function(err) {
  if (err) { console.log(err); }
});

ドキュメント読み取り

まずは all 指定.

User.find({}, function(err, docs) {
  for (var i=0, size=docs.length; i<size; ++i) {
    // docs[i].doc
  }
});

次に query 指定.
指定形式は Querying とほぼ同じようです.

// SELECT * FROM users WHERE name='KrdLab' に相当
User.find({ name: 'KrdLab' }, function(err, docs) {
  // マッチしたドキュメントが docs[i].doc で取れる
});

そして,query + フィールド指定.

// SELECT point FROM users WHERE name='KrdLab' に相当
User.find({ name: 'KrdLab' }, ['point'], function(err, docs) {
  // マッチしたドキュメントが docs[i].doc で取れ,フィールドは point のみ
  // (default 値を指定したフィールドは,fields 引数に指定していなくても「default 値」が取れることに注意)
});

最後に,query + フィールド + オプション指定.
オプションには,limit,skip,sort 等が指定できます.指定形式は Advanced Queries とほぼ同じようです.

// SELECT point FROM users WHERE name='KrdLab' LIMIT 2 に相当
User.find({ name: 'KrdLab' }, ['point'], { limit: 2 }, function(err, docs) {
  // 略 (とにかく docs[i].doc で取れる)
});

ドキュメント更新

第 2 引数には Modifier Operations を指定します.

// UPDATE users SET point=999 WHERE name='KrdLab' に相当
User.update({ name: 'KrdLab' }, { $set: {point: 999} },
            { upsert: false, multi: true }, function(err) {
  // ...
});

補足:MongoDB は Atomic Operations にある通り,単一ドキュメントに対しのみ Atomic な操作を保証しています.lock や transaction はサポートされていません (v1.2.2).理由,並びに代替方法については前述のリンク先に記載されています.

ドキュメント削除

第 1 引数にクエリを指定します.'{}' 指定だと...全部消えます.

// DELETE FROM users WHERE name='KrdLab' に相当
User.remove({ name: 'KrdLab' }, function(err) {
  // ...
});

Embedded Document

MongoDB にはドキュメントの中にドキュメントを埋め込む embedded document という概念があります (Schema Design).Mongoose でも当然サポートされており,Schema 定義は以下のようになります.

// 公式のサンプルを若干修正
var CommentSchema = new Schema({
  title:  String,
  body:   String,
  date:   Date
});

var BlogPostSchema = new Schema({
  author: ObjectId,
  title:  String,
  body:   String,
  date:   Date,
  comments: [CommentSchema],
  metadata: {
    like: Number,
  }
});
mongoose.model('BlogPost', BlogPostSchema);

'comments' のように外部に Schema を定義して埋め込む方法と,'metadata' のようにスキーマ定義ごと埋め込む方法の 2 つがあります.なお,'metadata' のような埋め込みを行った場合は,BlogPost のコンストラクタで値を設定する必要があり,その後は get しかできないようです (要確認).
既存の BlogPost に対して comments を更新する場合は以下のように行います.

// 投稿済み BlogPost の _id を 'post_id' とする
BlogPost.findOne({ _id: post_id }, function(err, post) {
  if (!err) {
    var comment = {};
    comment.title = 'タイトル';
    comment.body  = '内容';
    comment.date  = Date.now();
    post.comments.push(comment);  // 追加
    post.save(function(err) {
      // ...
    });
  } else {
    console.log('error findOne: ' + err);
  }
});

新しいコメントを追加する場合,Comment を使わずに Object を生成し,push する必要があります (Comment を使って push すると RangeError が発生する).
save を使った場合は,追加した埋め込みドキュメント (comment) に '_id' が割り当てられます.しかし,以下のように update を使った場合は,単純に Object がそのまま追加されるのみであり,'_id' は割り当てられないようです.

var comment = {};
// ... set fields ...
BlogPost.update({ _id: post_id }, { $push: { comments: comment } },
                { upsert: false, multi: false }, function(err) {
  // ...
});

save 以外使うなってことか...

また,公式にて embedded document の削除は post.comments[0].remove() のように 'remove' 呼べよと紹介されていますが,呼び出しても消えません (v1.1.2).なぜ?

終わりに

Mongoose の I/F は,既存の O/R Mapper を使ったことのある人,または MongoDB のシェルをさわったことのある人が,比較的容易に使えるような感じで設計されています.
そしてやはり node.js と MongoDB は相性が良いですね.データの受け渡しが全て JSON で済んでしまうというのは楽です.
しばらくあれこれ使ってみようと思います.

お久しぶりです

今,大災害の発生により大変なときであり,ここ数日のニュースは見てるとしゃれになりません.
ひとまず今自分にできることを済ませた上で,できる限り平常運転で行こうと思います.何卒ご了承くださいますようお願い申し上げます.
...

大学時代の研究室仲間である id:veveve さんとサンボさんの無事は確認.よかった.
...

なお 1 〜 2 月は残業 + 休日出勤により,週一更新は守れませんでした...orz

Y combinator

最近は残業と休日出勤ばかりでうんざりの KrdLab です.
今回はちょっと毛色の違う話題を.内容はいつの間にか自分用のまとめになってしまいましたが...

Y combinator とは?

不動点コンビネータの一種.
不動点とは,ある関数 F を定義したとき,F(p) = p となる p のことを指す.ここで p を関数にまで拡張して考えてみる.すなわち関数 F は高階関数となる.この関数 F の不動点 p を返すような関数 g を考えると,p = g(F) の様に表すことができる.このとき関数 g を不動点コンビネータと呼ぶ.

特にラムダ計算では,

g := λf. (λx. f(x x)) (λx. f(x x))

と表される Y combinator がよく知られている.

なお,combinator については Combinatory logic - Wikipedia, the free encyclopedia にて以下のように説明されている.

A combinator is a higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. [引用]

combinator は,自身の定義に自由識別子 (自由変数と呼ばれる場合もある) が含まれていないため,実行時の環境に因らず,引数 (束縛された識別子) のみから自身の出力が決まる.

性質

不動点の関係を表しているのだから,g(F) = YF = p,かつ F(p) = p より,F(YF) = YF となるはず.実際に適用を進めると,確かにその性質が認められる.

YF = (λf. (λx. f(x x)) (λx. f(x x)))F
   =   (λx. F(x x)) (λx. F(x x))
   = F((λx. F(x x)) (λx. F(x x)))
   = F(YF)

さらに適用を続けると,以下のように再帰構造が現れる.

   = F(F(YF))
   = F(F(F...

つまり不動点 YF は,「関数を引数にとって関数を返す」関数 F による再帰を表しているとも解釈できる.

何ができるのか?

ラムダ式で再帰を表現することができる (つまり繰り返しを表現できる).このような再帰を無名再帰と呼ぶ.
ここでは階乗計算を例に説明する.プログラムでフツーに書けば以下のようになる.

fact 0       = 1
fact (n + 1) = (n + 1) * fact n

これをラムダ式のみで表すために Y を用いる.前述の性質から,「F の繰り返し適用 (再帰) の結果得られる関数」を表す YF に n を渡したものが,fact n と等しくなるような F を定義すればよい.

イメージとしては fact(n) := YFn = F(YF)n より,F は関数と数値を引数にとればよいことがわかる.

コードで書くと以下のような感じになる (大文字始まりなのは気にしないでください).

F = \f n -> case n of
              0       -> 1
              (m + 1) -> n * f m

上記 F に Y を適用することで引数 f は YF となり,再帰構造が生まれる.

よって,目的の fact は以下のようになる (大文字始まりなのは気にしないでください).

fact = Y F
--       ↓
       Y (\f n -> case n of
                    0       -> 1
                    (m + 1) -> n * f m)


実際に引数を与えると以下のように関数適用が進み,階乗計算が実現される (ラムダ式は長いので便宜上 F と表記しています).

(Y F) 3 -> F (Y F) 3
        -> 3 * (Y F) 2
        -> 3 * (F (Y F) 2)
        -> 3 * (2 * (Y F) 1)
        -> 3 * (2 * (F (Y F) 1))
        -> 3 * (2 * (1 * (Y F) 0))
        -> 3 * (2 * (1 * (F (Y F) 0)))
        -> 3 * (2 * (1 * (1)))
        -> 6

以上により,Y combinator を用いることで無名再帰を実現できたことがわかる.

補足

haskell で Y combinator を定義する方法は,ググるといくつか紹介されています.ちなみに fix = \f -> f (fix f) は反則 (?) なので悪しからず.
また,正格評価を基礎とする言語で Y combinator を記述した場合,展開が無限に進んでエラーとなってしまいます.こちらについてはここここで対応版が紹介されています.

OutOfMemoryError を分析する

最近仕事で Java ばかりの KrdLab です.そんな中,Eclipse の Memory Analyzer (MAT) が素晴らしかったので紹介.
http://www.eclipse.org/mat/

はじめに

Java ではメモリリークによって OutOfMemoryError (OOME) が発生する.このリークを特定する作業はなかなか困難な作業になることが多い (特に,誰が作ったのかわからない古いコードの保守で発生すると大変!).
今回はその作業コストを軽減するツールとして Memory Analyzer (MAT) を紹介する.

Eclipse Memory Analyzer とは?

JVM のヒープダンプを解析するツール.どのオブジェクトがリーク候補なのか,どこから参照されていたものだったのか,といった情報がグラフィカルに表示される.
他にもダンプ分析のための様々な機能があり,非常に強力なツールである.

ヒープダンプを取得するには?

OutOfMemoryError 発生時に取得する場合
以下のように HeapDump* オプションを JVM に指定する.

>java -Xmx128m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump.hprof -cp ...

上記であれば,128 MB を超えたところでヒープダンプ dump.hprof が出力される.
参考:http://blogs.sun.com/watt/resource/jvm-options-list.html


任意のタイミングで取得する場合
jmap で取得する.(他にもいくつか方法があるけど割愛)

>jmap -dump:format=b,file=test.hprof 5624

これで PID=5624 のヒープダンプが test.hprof として出力される.
参考:jmap - Memory Map


ただしサーバ系 OS の場合,実行権限の関係上 jmap が正常に実行できない場合がある.そのような場合は PsTools の PsExec を使用すると良い."-s" オプションをつけることで,jmap を System アカウントで実行できる.

>PsExec.exe -s jmap -dump:format=b,file=test.hprof 5624

参考:PsExec | Microsoft Docs

Memory Analyzer を使ってみる

以下のようなシンプルなコードを用意した.あからさまなリークについてはあしからず.

package com.example.test.mat;
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) throws Exception {
        final Thread th = new Thread(new Leaker(), "leak-thread");
        th.start();
        th.join();
    }
}

class Leaker implements Runnable {
    private final List<Integer[]> list = new ArrayList<Integer[]>();

    public void run() {
        for (int i = 0; i < 1024; ++i) {
            list.add(new Integer[1024 * 1024]);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException _ignore_) {
            }
        }
    }
}
// コンパイル:javac -d ./bin -cp ./src -encoding utf-8 src/com/example/test/mat/Main.java

前述した方法でヒープダンプを取得し,MAT で「Leak Suspects」を開く.

(※この円グラフで「ここヤバそう」というのがわかる)
「Details」リンクから詳細画面に移動すれば,個々のオブジェクトについて詳細な情報を得ることができる.

画面から,Leaker の ArrayList でリークしていることがわかる.実行スレッドの名前は "leak-thread" である.
各要素のリンクを選択することでさらに分析を進めていくことができる.

最後に

Eclipse Memory Analyzer はヒープダンプを分析するツールです.機能・操作性ともに素晴らしい.
実際に以下のようなことに使ってみて,とても便利でした.

  • OutOfMemoryError 発生箇所の特定と確認
  • メモリ使用量の推測値を検証

まぁ,機能は紹介しきれないため,実際にさわってみることをおすすめします.(^_^)

2011 年開始

あけましておめでとうございます.
2010 年は日記の更新が滞ってしまいました.
今年は週一ぐらいのペースで更新したいな,と思う元日.

あと,Lisys は現在 Google の Project Hosting サービスへの移行を検討しています.

ではでは.

DataMapper 0.10.2 Reading (その 4: dm-core/core_ext/)

このディレクトリに配置されたコードは,ruby の標準クラス (モジュール) を拡張するためのもの.具体的には以下の 3 つが拡張されている.

  • Enumerable
  • Kernel
  • Symbol

それぞれは小さなコードだが,影響範囲は広い.

Enumerable

empty?/one?/first/size メソッドを定義している.標準メソッドについては こちら を参照されたし.
Enumerable の規約に従い,全て each により定義されている.これらの内,ここでは one? メソッドを取り上げようと思う.one? メソッドは文字通り「1 つだけ?」かどうかを判定するわけだが,この判定基準にブロックを用いる.

module Enumerable
  def one?
    return one? { |entry| entry } unless block_given?

    matches = 0
    each do |entry|
      matches += 1 if yield(entry)
      return false if matches > 1
    end
    matches == 1
  end

与えられたブロックで各要素を評価し,true を返した要素の個数をカウントしていく.カウントが 1 であれば true を返すし,そうでなければ false を返す.

Kernel

Kernel では,DataMapper モジュールの repository メソッドを呼び出すためのメソッドが定義されている.

module Kernel
  private
  def repository(*args, &block)
    DataMapper.repository(*args, &block)
  end
end

ここで定義されているということは,self.repository の形であれば (self 以外に対して .repository としなければ) どこでも呼び出せる.
簡単な例を示すと,以下のような感じ.

module Kernel
  private
  def hoge
    puts "hogeっていうな"
  end
end

hoge
# => hogeっていうな

class A; end
a = A.new
a.hoge
# => NoMethodError

class A
  def call_hoge
    hoge
  end
end
a.call_hoge
# => hogeっていうな

Symbol

コードは短いが,検索に広く影響する条件用の Symbol を定義している.

class Symbol
  (DataMapper::Query::Conditions::Comparison.slugs | [ :not, :asc, :desc ]).each do |sym|
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{sym}
        #{"warn \"explicit use of '#{sym}' operator is deprecated (#{caller[0]})\"" if sym == :eql || sym == :in}
        DataMapper::Query::Operator.new(self, #{sym.inspect})
      end
    RUBY
  end
end

例えば,

class MyModel
  include DataMapper::Resource
  property :id, Serial
  property :name, String
end

というモデルを定義したとき,

m = MyModel.all(:name.like => 'Krd%')

という呼び出しの ".like" にあたる部分を定義していることになる.
この定義により,".like" と呼び出すことで,条件演算を表す Operator インスタンスが得られる.

なお,"Comparison.slugs" については後ほど.ここでは「条件記述用に定義された Symbol を配列で返す」ぐらいの認識でよいかと.