Old Sunset Days

Node.jsでMongooseからMongoDBを利用する

日付 タグ node.js mongodb カテゴリ node.js

目次

Node.jsでMongooseからMongoDBを利用する

以前の記事でnode-mongodb-nativeを利用してNode.jsからMongoDBを扱う記事を書いた。

( ==> Node.jsからMongoDBを利用する

MongoDBを扱うにはMongooseというORMも有名で良く利用されているので、今回の記事ではMongooseを利用してNode.jsからMongoDBのデータを扱うサンプルを作ってみる。

https://www.npmjs.com/package/mongoose - Mongoose

なお、今回作ったサンプルのソースコードは参照用としてGithubの以下に置いてある。 https://github.com/hugodeblog/node-mongoose

テストディレクトリ作成とnode-mongodb-nativeのインストール

今回もテストディレクトリの作成とモジュールのインストールから

$ mkdir node-mongoose
$ cd node-mongotest
$ npm init
$ npm install mongoose --save

本記事を書いている時点ではmongooseはバージョン5.11.12が入ったようだ。

Mongooseを利用するアプリ

今回は簡単にMongoDBのデータの作成、更新、削除のような基本操作ができるかをテストするアプリを作る。

データは朝食、昼食、夕食に何を食べたかメモ書きするような目的、データを想定した。


mongoose-test.mjs

import mongoose from 'mongoose';
import util from 'util';

const Schema = mongoose.Schema;

// 食事のタイプを定義
const MealTypeSchema = new Schema({
    _id: Number,
    type: String
});

const MealTypeModel = mongoose.model('MealType', MealTypeSchema);

// mealtypes.jsonにデータが入っているので、それを予めMongoDBに登録しておく
// mongoimport -h localhost --db meal --collection mealtypes --drop --jsonArray --file mealtypes.json

// 食事データを定義
const MealSchema = new Schema({
    type: { type: Number, ref: 'MealType' },
    foods: [{
        menu: String
    }]
});

const MealModel = mongoose.model('Meal', MealSchema);

var db;

const dbname = 'meal';

// MongoDBへの接続
const connectDB = async () => {
    try {
        await mongoose.connect(`mongodb://localhost/${dbname}`);
        db = mongoose.connections;
        console.log(util.inspect(db));
        console.log(`Connected to database on Worker process: ${process.pid}`);
    } catch (err) {
        console.error(`Connection error: ${error.stack} on Worker process: ${process.pid}`);
        process.exit(1)
    }
}

// MongoDB接続切る
const closeDB = async () => {
    console.log('closeDB called');
    await mongoose.disconnect();
    db = undefined;
}

// DB接続コールバック
mongoose.connection.on('connected', () => {
    console.log('connected');
});

mongoose.connection.on('error', (err) => {
    console.log('connection error: ' + err);
});

mongoose.connection.on('disconnected', () => {
    console.log('disconnected');
});

mongoose.connection.on('close', () => {
    console.log('connection closed');
});

(async () => {
    try {
        // DB接続
        await connectDB();

        var monday_morning = new MealModel({
            type: 1,
            foods: [
                { menu: '目玉焼き' },
                { menu: '牛乳' },
                { menu: 'トースト' }
            ]
        });

        await monday_morning.save();

        var monday_lunch = new MealModel({
            type: 2,
            foods: [
                { menu: 'ナポリタン' },
                { menu: 'アイスコーヒー' }
            ]
        });

        await monday_lunch.save();

        var monday_dinner = new MealModel({
            type: 3,
            foods: [
                { menu: '肉じゃが' },
                { menu: '納豆' },
                { menu: '味噌汁' }
            ]
        });

        await monday_dinner.save();

        var thuesday_morning = new MealModel({
            type: 1,
            foods: [
                { menu: 'スクランブルエッグ' },
                { menu: '牛乳' },
                { menu: 'トースト' }
            ]
        });

        await thuesday_morning.save();

        // 朝食に食べたのを検索
        console.log('朝食に食べたのを検索します');
        console.log('------------------');
        {
            const result = await MealModel.find({ type: 1 }).populate('type').exec();
            console.log(JSON.stringify(result, null, 2));
        }

        // 肉じゃががメニューに入っていたのを検索
        console.log('肉じゃががメニューに入っていたのを検索します');   
        console.log('------------------');
        {
            const result = await MealModel.find({ 'foods.menu': '肉じゃが' }).populate('type').exec();
            console.log(JSON.stringify(result, null, 2));
        }

        // トーストがメニューに入っていたのを検索
        console.log('トーストがメニューに入っていたのを検索します');
        console.log('------------------');
        {
            const result = await MealModel.find({ 'foods.menu': 'トースト' }).populate('type').exec();
            console.log(JSON.stringify(result, null, 2));
        }

        // 牛乳 => オレンジジュースを入れ替え
        console.log('牛乳 => オレンジジュースを入れ替えします');
        console.log('------------------');
        {
            const result = await MealModel.updateMany(
                { 'foods.menu': '牛乳' },
                { '$set': { 'foods.$.menu': 'オレンジジュース' } }
            );
            console.log(JSON.stringify(result, null, 2));
        }

        // 全コレクションをチェック
        console.log('全コレクションをチェックします');
        console.log('------------------');
        {
            const result = await MealModel.find().populate('type').exec();;
            console.log(JSON.stringify(result, null, 2));
        }

        // 夕食の全データを削除
        console.log('夕食の全データを削除します');
        console.log('------------------');
        {
            const result = await MealModel.deleteMany({ type: 3 });
            console.log(JSON.stringify(result, null, 2));
        }

        // 全コレクションをチェック
        console.log('全コレクションをチェックします');
        console.log('------------------');
        {
            const result = await MealModel.find().populate('type').exec();;
            console.log(JSON.stringify(result, null, 2));
        }

    } catch (err) {
        console.error(err);
    } finally {
        // DBクローズ
        await closeDB();
    }
})();

スキーマとデータモデル

Mongooseでデータを扱うには、どのようなデータ型を持つデータがコレクションに入るかというスキーマ定義が必要になる。

// 食事のタイプを定義
const MealTypeSchema = new Schema({
    _id: Number,
    type: String
});

// 食事データを定義
const MealSchema = new Schema({
    type: { type: Number, ref: 'MealType' },
    foods: [{
        menu: String
    }]
});

そこで朝食、昼食、夕食というカテゴリー分けのためにMealTypeSchema というスキーマ定義を作り、


mealtypes.json

[
    {
        "_id": 1,
        "type": "朝食"
    },
    {
        "_id": 2,
        "type": "昼食"
    },
    {
        "_id": 3,
        "type": "夕食"
    }
]

上記のデータを持つようにする。その上でMealSchema スキーマを定義している。

これらの定義したスキーマからデータモデルを作成しているのは以下。

const MealTypeModel = mongoose.model('MealType', MealTypeSchema);

const MealModel = mongoose.model('Meal', MealSchema);

これによりMongoDB上のcollection名としては、それぞれmealtypes (複数形)、meals (複数形)として保存されることになる。

例えば、月曜日の朝食に食べたメニューデータを追加したい場合は、

var monday_morning = new MealModel({
  type: 1,
  foods: [
    { menu: '目玉焼き' },
    { menu: '牛乳' },
    { menu: 'トースト' }
  ]
});

のようなデータを用意すればいいことになる。

データ定義が準備できたところでMongoDBに接続してデータを保存してみたりしよう。

MongoDBへの接続と解除

MongoDBヘの接続としては、ローカルPC環境上で動作しているMongoDBに接続することを前提として、データベース名をmeal として

const dbname = 'meal';
await mongoose.connect(`mongodb://localhost/${dbname}`);

アプリ終了時にはMongoDBヘの接続を解除するため、

await mongoose.disconnect();

上記を実行している。

MongoDBヘ接続されたタイミングなどはコールバックを利用すれば把握できる。

// DB接続コールバック
mongoose.connection.on('connected', () => {
    console.log('connected');
});

mongoose.connection.on('error', (err) => {
    console.log('connection error: ' + err);
});

mongoose.connection.on('disconnected', () => {
    console.log('disconnected');
});

mongoose.connection.on('close', () => {
    console.log('connection closed');
});

MongoDBのデータを生成する

var monday_morning = new MealModel({
  type: 1,
  foods: [
    { menu: '目玉焼き' },
    { menu: '牛乳' },
    { menu: 'トースト' }
  ]
});

await monday_morning.save();

月曜の朝食メニューの登録だが、上記のようにモデルのインスタンスを作った上でsave() メソッドを呼び出せば良い。

MongoDBのコレクションからデータ読み込み

登録されたデータから朝食データを抜き出したい場合、type: 1 のデータを検索すれば良いので、

await MealModel.find({ type: 1 }).populate('type').exec();

として検索している。

populatetype の参照情報を取ってきて、結果表示するかどうか。

populate した結果

朝食に食べたのを検索します
------------------
[
  {
    "_id": "60069901ed2e6dfc6995efbf",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069901ed2e6dfc6995efc0",
        "menu": "目玉焼き"
      },
      {
        "_id": "60069901ed2e6dfc6995efc1",
        "menu": "牛乳"
      },
      {
        "_id": "60069901ed2e6dfc6995efc2",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069901ed2e6dfc6995efca",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069901ed2e6dfc6995efcb",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "60069901ed2e6dfc6995efcc",
        "menu": "牛乳"
      },
      {
        "_id": "60069901ed2e6dfc6995efcd",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]

populate を外した場合

朝食に食べたのを検索します
------------------
[
  {
    "_id": "600699f84cf54bfe24a9bf6d",
    "type": 1,
    "foods": [
      {
        "_id": "600699f84cf54bfe24a9bf6e",
        "menu": "目玉焼き"
      },
      {
        "_id": "600699f84cf54bfe24a9bf6f",
        "menu": "牛乳"
      },
      {
        "_id": "600699f84cf54bfe24a9bf70",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "600699f84cf54bfe24a9bf78",
    "type": 1,
    "foods": [
      {
        "_id": "600699f84cf54bfe24a9bf79",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "600699f84cf54bfe24a9bf7a",
        "menu": "牛乳"
      },
      {
        "_id": "600699f84cf54bfe24a9bf7b",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]

MongoDBのデータの更新

月曜の朝食と火曜日の朝食のメニューに牛乳が入っていたが、データを誤って登録していたので、これをオレンジジュースに書き換えたいという場合

await MealModel.updateMany(
  { 'foods.menu': '牛乳' },
  { '$set': { 'foods.$.menu': 'オレンジジュース' } }
);

updateMany は該当するデータが複数あった場合、それらを全部更新するということである。

最初にマッチするのを1つのみ更新したい場合はupdateOne を使う。

MongoDBのデータの削除

夕食のデータを全部削除したいのであれば、

await MealModel.deleteMany({ type: 3 });

上記のようにdeleteMany を使う。

最初にマッチする1つのみ削除したい場合であれば、deleteOne を使う。

サンプルアプリの実行

最初にMongoDBのサーバーをローカルで立ち上げておく。

その上でMealTypeSchema として、mealtypes.json を以下のコマンドで読み込ませておく。

$ mongoimport -h localhost --db meal --collection mealtypes --drop --jsonArray --file mealtypes.json

ここで一旦MongoDBにcliで接続してデータを見てみよう。

$ mongo
> use meal
switched to db meal
> db.mealtypes.find()
{ "_id" : 3, "type" : "夕食" }
{ "_id" : 2, "type" : "昼食" }
{ "_id" : 1, "type" : "朝食" }
>

参照されるデータは正しく登録されているようだ。

その上で、アプリを実行してみよう。

$ node mongoose-test.mjs 
Connected to database on Worker process: 67330
朝食に食べたのを検索します
------------------
[
  {
    "_id": "60069f5d7e898d0703f3a463",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a464",
        "menu": "目玉焼き"
      },
      {
        "_id": "60069f5d7e898d0703f3a465",
        "menu": "牛乳"
      },
      {
        "_id": "60069f5d7e898d0703f3a466",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a46e",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46f",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "60069f5d7e898d0703f3a470",
        "menu": "牛乳"
      },
      {
        "_id": "60069f5d7e898d0703f3a471",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]
肉じゃががメニューに入っていたのを検索します
------------------
[
  {
    "_id": "60069f5d7e898d0703f3a46a",
    "type": {
      "_id": 3,
      "type": "夕食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46b",
        "menu": "肉じゃが"
      },
      {
        "_id": "60069f5d7e898d0703f3a46c",
        "menu": "納豆"
      },
      {
        "_id": "60069f5d7e898d0703f3a46d",
        "menu": "味噌汁"
      }
    ],
    "__v": 0
  }
]
トーストがメニューに入っていたのを検索します
------------------
[
  {
    "_id": "60069f5d7e898d0703f3a463",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a464",
        "menu": "目玉焼き"
      },
      {
        "_id": "60069f5d7e898d0703f3a465",
        "menu": "牛乳"
      },
      {
        "_id": "60069f5d7e898d0703f3a466",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a46e",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46f",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "60069f5d7e898d0703f3a470",
        "menu": "牛乳"
      },
      {
        "_id": "60069f5d7e898d0703f3a471",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]
牛乳 => オレンジジュースを入れ替えします
------------------
{
  "n": 2,
  "nModified": 2,
  "ok": 1
}
全コレクションをチェックします
------------------
[
  {
    "_id": "60069f5d7e898d0703f3a463",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a464",
        "menu": "目玉焼き"
      },
      {
        "_id": "60069f5d7e898d0703f3a465",
        "menu": "オレンジジュース"
      },
      {
        "_id": "60069f5d7e898d0703f3a466",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a467",
    "type": {
      "_id": 2,
      "type": "昼食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a468",
        "menu": "ナポリタン"
      },
      {
        "_id": "60069f5d7e898d0703f3a469",
        "menu": "アイスコーヒー"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a46a",
    "type": {
      "_id": 3,
      "type": "夕食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46b",
        "menu": "肉じゃが"
      },
      {
        "_id": "60069f5d7e898d0703f3a46c",
        "menu": "納豆"
      },
      {
        "_id": "60069f5d7e898d0703f3a46d",
        "menu": "味噌汁"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a46e",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46f",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "60069f5d7e898d0703f3a470",
        "menu": "オレンジジュース"
      },
      {
        "_id": "60069f5d7e898d0703f3a471",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]
夕食の全データを削除します
------------------
{
  "n": 1,
  "ok": 1,
  "deletedCount": 1
}
全コレクションをチェックします
------------------
[
  {
    "_id": "60069f5d7e898d0703f3a463",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a464",
        "menu": "目玉焼き"
      },
      {
        "_id": "60069f5d7e898d0703f3a465",
        "menu": "オレンジジュース"
      },
      {
        "_id": "60069f5d7e898d0703f3a466",
        "menu": "トースト"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a467",
    "type": {
      "_id": 2,
      "type": "昼食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a468",
        "menu": "ナポリタン"
      },
      {
        "_id": "60069f5d7e898d0703f3a469",
        "menu": "アイスコーヒー"
      }
    ],
    "__v": 0
  },
  {
    "_id": "60069f5d7e898d0703f3a46e",
    "type": {
      "_id": 1,
      "type": "朝食"
    },
    "foods": [
      {
        "_id": "60069f5d7e898d0703f3a46f",
        "menu": "スクランブルエッグ"
      },
      {
        "_id": "60069f5d7e898d0703f3a470",
        "menu": "オレンジジュース"
      },
      {
        "_id": "60069f5d7e898d0703f3a471",
        "menu": "トースト"
      }
    ],
    "__v": 0
  }
]
closeDB called
disconnected
connection closed

以上、Mongooseを利用してデータ作成、更新、削除、閲覧ができることが確認できた。