Node.js用MongoDBライブラリ mongooseの基本操作まとめ（すぐ試せるサンプル付き）

Node.jsアプリからMongoDBを操作するためのライブラリであるmongooseについて、その使い方をまとめる。

## 試したバージョン
* Node.js: v0.10.40
* mongoose: 4.4.9
* MongoDB: 3.0.7

## 前提
* MongoDBが`localhost:27017`で動いている
* Node.jsがインストールされている
* Mongooseがインストールされている

## スキーマ
登録するドキュメントは日本酒データ。
日本酒の銘柄`brand`、特定名称`type`および飲んだ感想`impressions`を複数登録できるようにする。
特定名称および飲んだ時の温度帯`temperature`については別コレクションを参照するようにする。

日本酒

```js
var Sake = new Schema({
  brand: String,
  type: { type: Number, ref: 'SakeType' },
  impressions: [{
    temperature: { type: Number, ref: 'Temperature' },
    impression: String
  }]
});
```

特定名称マスタ

```js
var SakeType = new Schema({
  _id: Number,
  type: String
});
```

温度帯マスタ

```js
var Temperature = new Schema({
  _id: Number,
  temperature: String
});
```

## マスタデータのインポート
特定名称マスタと温度帯マスタを先に登録する。

特定名称は"純米酒"や"吟醸酒"など９種類。
飲んだ時の温度帯は"常温"や"熱燗"など１０種類。

```sake-types.json
[
  { "_id": 1, "type": "普通酒" },
  { "_id": 2, "type": "本醸造酒" },
  { "_id": 3, "type": "特別本醸造酒" },
  { "_id": 4, "type": "吟醸酒" },
  { "_id": 5, "type": "大吟醸酒" },
  { "_id": 6, "type": "純米酒" },
  { "_id": 7, "type": "特別純米酒" },
  { "_id": 8, "type": "純米吟醸酒" },
  { "_id": 9, "type": "純米大吟醸酒" }
]
```

```temperatures.json
[
  { "_id": 1, "temperature": "飛び切り燗(55℃以上)" },
  { "_id": 2, "temperature": "熱燗(50℃前後)" },
  { "_id": 3, "temperature": "上燗(45℃前後)" },
  { "_id": 4, "temperature": "ぬる燗(40℃前後)" },
  { "_id": 5, "temperature": "人肌燗(35℃前後)" },
  { "_id": 6, "temperature": "日向燗(30℃前後)" },
  { "_id": 7, "temperature": "常温" },
  { "_id": 8, "temperature": "涼冷え(15℃前後)" },
  { "_id": 9, "temperature": "花冷え(10℃前後)" },
  { "_id": 10, "temperature": "雪冷え(5℃前後)" }
]
```

MongoDBが`localhost:27017`で待ち受けていると仮定し、以下のコマンドでインポートする。

```bash
mongoimport -h localhost:27017 --db sake --collection temperatures --drop --jsonArray --file temperatures.json
mongoimport -h localhost:27017 --db sake --collection saketypes --drop --jsonArray --file sake-types.json
```

## MongoDBに接続し、スキーマからモデルをコンパイルする
以下の内容で`model.js`を作成する。

```model.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

// スキーマ
var SakeType = new Schema({
  _id: Number,
  type: String
});

var Temperature = new Schema({
  _id: Number,
  temperature: String
});

var Sake = new Schema({
  brand: String,
  type: { type: Number, ref: 'SakeType' },
  impressions: [{
    temperature: { type: Number, ref: 'Temperature' },
    impression: String
  }]
});

// MongoDBへの接続
mongoose.connect('mongodb://localhost/sake');

// スキーマからモデルをコンパイルし、モデルをエクスポートする
exports.Temperature = mongoose.model('Temperature', Temperature);
exports.SakeType = mongoose.model('SakeType', SakeType);
exports.Sake = mongoose.model('Sake', Sake);
```

## ドキュメント操作まとめ
以降のコードは`model.js`をインポートすることで実行可能。

```js
var model = require('./model');
var Sake = model.Sake;
var SakeType = model.SakeType;
var Temperature = model.Temperature;

... // ここにコードを書く
```

### ドキュメントの保存
"醸し人九平次"の"純米大吟醸"を飲んだとしよう。あれはめちゃくちゃ美味かった。ここでは"雪冷え"と"常温"で飲んだことにする。

ドキュメントはモデルクラスをnewして作成し、`save()`メソッドで保存する。

```js
// ドキュメントの作成
var kuheiji = new Sake({
  brand: '醸し人九平次',
  type: 9,
  impressions: [
    { temperature: 7, impression: 'めちゃうま' },
    { temperature: 10, impression: '激うま' }
  ]
});

// ドキュメントの保存
kuheiji.save(function(err) {
  if (err) throw err;
});
```

最近飲んだ"上喜元"も美味かった。これも登録しておこう。

```js
var jokigen = new Sake({
  brand: '上喜元',
  type: 9,
  impressions: [
    { temperature: 7, impression: 'フルーティ' },
    { temperature: 9, impression: 'フレッシュ' }
  ]
});

jokigen.save(function(err) {
  if (err) throw err;
});
```

### ドキュメントの検索
まずは基本の`find()`。第１引数にMongoDBのセレクタを指定し、第２引数にコールバック関数を指定する。

コールバック関数には引数が２つ渡され、クエリ実行時にエラーが起きた場合は`err`にエラーオブジェクトが格納され、`result`はnullになる。クエリが成功した場合は`err`がnullになり、`result`にクエリの実行結果が格納される。

```js
Sake.find({ brand: '醸し人九平次' }, function(err, result) {
  if (err) throw err;
  console.log(result[0].brand);  // '醸し人九平次'が出力される
  console.log(result[1].brand);  // '上喜元'が出力される
});
```

次は`findOne()`。こちらはヒットしたドキュメントのうちの最初のドキュメントを返してくれる。セレクタには特定名称"純米大吟醸酒"を指定してみる。

```js
Sake.findOne({ type: 9 }, function(err, result) {
  if (err) throw err;
  console.log(result.brand);  // 登録順の関係でこちらも'醸し人九平次'が出力される
  console.log(result.type);   // '9'が出力される
});
```

他にも_idフィールドで検索する`findById()`や検索と同時に更新を行う`findOneAndUpdate()`などがある。

### Population
MongoDBにはJOINがないが、Populationという機能を使うと他のコレクションのドキュメントを参照することができる。

先ほどの検索の例では、`type`は数値として取得していた。スキーマを確認すると`type`は`SakeType`スキーマを参照している。ここではPopulationを行うことで特定名称マスタと温度帯マスタを参照し、"純米大吟醸酒"や"常温"などの具体的な情報を取得する方法を紹介する。

```js
Sake.find({ brand: '醸し人九平次' })
  .populate('type impressions.temperature')
  .exec(function(err, result) {
    if (err) throw err;
    console.log(result[0].type.type);  // '純米大吟醸酒'が出力される
    console.log(result[0].impressions[0].temperature.temperature);  // '常温'が出力される
  });
```

### ドキュメントの更新
`update()`を使ってドキュメントの更新を行う。"醸し人九平次"の銘柄名を"獺祭"に変更してみよう。

```js
Sake.update(
  { brand: '醸し人九平次' },
  { $set: { brand: '獺祭' } },
  function(err) {
    if (err) throw err;
  }
);
```

第１引数にはセレクタ、第２引数にはMongoDBのシェルと同じ表記で更新するフィールドの指定をすることができる。`update()`は更新したオブジェクトの取得は行わないため、最後のコールバック関数にはエラーオブジェクトのみが渡される。ドキュメントの取得もしたければ`findOneAndUpdate()`などを使う。

今度は"上喜元"に感想を追加してみる。

```js
Sake.update(
  { brand: '上喜元' },
  { $push: { impressions: { temperature: 1, impression: 'ツンてする' } } },
  function(err) {
    if (err) throw err;
  }
);
```

### ドキュメントの削除
最後にドキュメントの削除方法について。

```js
Sake.remove({ brand: '獺祭' }, function(err) {
  if (err) throw err;
});
```

他にも`findOneAndRemove()`などがある。