Tips

初心者向けにNode.jsでmongooseを使ったMongoDB操作を解説【Schema, connect, model,findById, findByIdAndDelete, savem, Router, Controller,dotenv,bcrypt】

  • このエントリーをはてなブックマークに追加

「MongoDBとmongooseがどういう関係になっているのかイメージできない」
「ネットにあるソースコードをコピペしているけどMySQLと違いすぎてよくわからない」
「MongoDBとmongooseについての日本語の情報が少ないと感じている」

本日はそんな方に向けてNode.jsでmongooseを使ったMongoDBの操作法を解説していきます。

MongoDBは柔軟性と拡張性に優れNode.js開発者の間で人気を博していますが、MongoDBを効果的に操作するには適切なツールと技術が必要です。

その中でもmongooseはMongoDBをNode.jsで操作する際に非常に便利なツールです。

mongooseを使えばMongoDBとの対話が簡潔かつ効果的になります。

しかし初心者にとってはその使い方を理解することが難しいかもしれません。

この記事ではNode.jsでmongooseを使ってMongoDBを操作する方法を解説します。

具体的にはSchemaの定義方法から始め、データベースへの接続方法、そしてModelの作成について詳しく説明します。

これにより初心者の方々もMongoDBを使ったアプリケーション開発において、自信を持ってスタートできるでしょう。

https://youtu.be/Uy05ZhhnLIg
https://youtu.be/G3p44qegvL0

MongoDBでデータベースを作る

まずMongoDBでアカウントを作成しましょう、Googleログインが便利です。

https://www.mongodb.com/ja-jp

こちらのページに遷移できますので、右上の「Try Free」をクリックします。

サインアップをします、本記事ではGoogleでサインアップしました。

有料と無料で3プランあるので一番右の「M0」という無料プランを選択して「CreateDeployment」という右下の緑ボタンをクリックします。

ユーザーの作成を求められますので任意のユーザー名とパスワードを決めて覚えておきます。

「Create Datamase User」という左下のボタンをクリックします。

「Choose a connection method」という右下の緑ボタンをクリックします。

APIキーを発行しますので「Connect to your application」の「Derivers」をクリックします。

「3.Add your connection string into your application code」という部分にあるURLをコピーしてエディタなどに貼り付けておきましょう。

URLを保管できたら右下の「Review setup step」という緑ボタンをクリックします。

右下の「Done」という緑ボタンをクリックします。

ダッシュボードメニューに戻りますので左メニューの「DEPLOYMENT」の中にある「Database」をクリックします。

「Cluster0」と書かれている行と同じ場所にある「Brows Collections」というボタンをクリックします。

左パネルの上部にある「Create Database」というボタンをクリックしてデータベースを作成します。

1段目の「Database name」と2段目の「Collection name」を入力します。

本記事ではデータベース名を「sample-db」として、コレクション名を「blogs」とします。

以下のように作成したデータベースが左パネルに表示されます。

続いてコードの環境構築をしていきます。

作業ディレクトリで必要なライブラリをインストールします、すでにインストールされていればスキップして大丈夫です。

npm i express
npm i morgan
npm i ejs
npm i nodemon

server.jsというファイルを作成してインストールしたライブラリをインポート、インスタンス化します。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

app.listen('3000');

ローカルサーバーを立ち上げておきます。

// node.jsが旧来のバージョンは↓
nodemon server

// node.jsが最新の場合は↓
npx nodemon

MongoDBはデータベースですので操作するにはSQL文が必要になります。

近年のプログラミングではSQLをそのまま実行するのはコードの保全上あまり推奨されておらずAPI経由で操作することがトレンドです。

そこでMongoDBではmongooseというライブラリが用意されていて、mongooseを使うことでSQLではなくAPI経由でMongoDBを操作できるようになります。

本記事でもmongooseを使用しますのでインストールします。

npm i mongoose

続いてmongooseをインポートします。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
// ここを追加
const mongoose = require('mongoose');

app.listen('3000');

APIのURLが必要になり、先ほど保存しておいた以下のURLをコピーしてコードに貼り付けます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');

// ここを追加
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/?retryWrites=true&w=majority&appName=mongo-basic';

app.listen('3000');

URLの前段の方にユーザー名とパスワードがあるのが確認できます、これで先ほど開設したデータベースにアクセスできるようになっています。

また「mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/〇〇?retry」の場所にデータベース名も追加します。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');

// URLにデータベース名を追加
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

app.listen('3000');

「sample-db」というデータベース名は先ほどの設定で作った以下のデータベース名のことです。

コードを追記していきます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

// ここを追加
mongoose
  .connect(dbUrl)
  .then((res) => console.log("DB接続"))
  .catch((err) => console.log(err));

app.listen('3000');

API経由でDBに接続するため非同期処理として実行する形になります。

mangooseにはconnectというメソッドがあり引数に先ほどのURLを渡すことでMongoDBにアクセスできて、アクセスできた場合にはthenメソッドでアクセスした直後に実行したい処理を書くことができます。

上記コードではコンソールログで「DB接続」と表示させていますが、ローカルサーバーを起動できるようにすることが一般的です。

以下のようにapp.listen(“3000”)をthenメソッドの処理に移動させましょう。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

// ここを変更
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

MongoDBの操作結果をNode.jsで画面に表示する方法

ここまでは環境構築でしたのでブログサイトを作成しているとして画面にデータを表示させてみます。

EJSをインストールしたので以下のように画面を先に作っておきます。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs

-server.js
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>

    <% if(blogs.length > 0){ %>
        <% blogs.forEach(blog => { %>
          <h2><%= blog.title %></h2>
          <p><%= blog.content %></p>
        <% }) %>
    <% } else { %>
        <p>投稿はありません</p>
    <% } %>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>About</h1>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>
    <form>
      <label>
        タイトル
        <input type="text" class="title" />
      </label>
      <label>
        本文
        <textarea type="text" class="content"></textarea>
      </label>
      <button>作成</button>
    </form>
  </body>
</html>

create.ejsではブログ投稿をしたいと思います。

mongooseではモデルという機能を使ってデータをMongoDBに送信して保存させることができます。

以下のようにmodelsというフォルダを作成して、その中にblog.jsというファイルを作成します。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs
- models
   |- blog.js
-server.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const blogSchema = new Schema(
  {
    title: {
      type: String,
      required: true,
    },
    content: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);
const Blog = mongoose.model('Blog', blogSchema);
module.exports = Blog;

mongooseをインポートしておきSchemaというクラスをインスタンス化して、保存したいデータをオブジェクト形式で作成しました。

スキーマは保存する型や必須有無など保存設定を書くようなものです。

ブログ記事を想定していますのでタイトルをtitleとして本文をcontentとしました。

typeは型名のことで文字列にして、requiredは必須有無のことで必須にしました。

またタイムスタンプを自動で付与することができて{timestamps: true}と追加すると記事データが追加された日時も一緒に保存でキアmす。

さらにmongooseにあるmodelメソッドをインスタンス化します。

第一引数にドキュメント名で頭文字を大文字にして、第二引数に先ほど作ったスキーマが代入された変数を指定します。

第一引数のドキュメント名はコレクションの単数系に相当するもので、コレクションがデータ群とした場合にドキュメントは1個のデータになっています。

前章のデータベース構築でコレクションをblogsとしていたので単数系でBlogがモデルにおけるドキュメント名になる具合です。

これでモデルが完成したので最後にBlogという名前でエクスポートしておきました。

エクスポートしたのでserver.jsで使用することができます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

// ここを追加
const Blog = require('./models/blog');

続いてexpressのルーティング設定を作っていきます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

// ここを追加
app.get('/create', (req, res) => {
  const blog = new Blog({
    title: 'テスト4',
    content: '本文です。',
  });
});

app.get('/', (req, res) => {

});

app.get('/about', (req, res) => {

});

http://localhost:3000/createにアクセスするとデータを投稿するように/createのルーティングにてモデルBlogをインスタンス化しました。

通常の投稿であればユーザーにタイトルと本文を入力してもらうのですが今回は手動でオブジェクトを書きました。

/models/blog.jsで作ったBlogをインポートしてインスタンス化し直す時の引数にデータを入れることでMongoDBにデータが送られる仕組みです。

上記コードでは定数blogにデータが格納されたので実際に送信するには以下のように追記します。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  const blog = new Blog({
    title: 'テスト4',
    content: '本文です。',
  });

 // ここを追加
  blog
    .save()
    .then((data) => {
      res.send(data);
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {

});

app.get('/about', (req, res) => {

});

mongooseにあるsaveメソッドはデータを保存する非同期処理として動作して、thenメソッドでexpressのsendメソッドを実行しておくことでデータをローカルサーバーに返すようにしました。

そのためhttp://localhost:3000/createにアクセスすると送信したデータがブラウザ上に表示されます。

続いてトップページであるhttp://localhost:3000にアクセスしたときに保存した内容を一覧にして表示するようにしてみます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  const blog = new Blog({
    title: 'テスト4',
    content: '本文です。',
  });

  blog
    .save()
    .then((data) => {
      res.send(data);
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
 // ここを追加
 Blog.find()
    .then((data) => {
      res.render('index', {
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

app.get('/about', (req, res) => {

});

mongooseのfindメソッドを実行するとMongoDBに保存されているデータを探してくれるようになります。

こちらも非同期処理になっていてthenメソッドではexpressのrenderメソッドを使ってindex.ejsにデータを渡すようにしています。

index.ejsはすでにデータをループで処理して画面に表示するように作っていました。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>

    <% if(blogs.length > 0){ %>
        <% blogs.forEach(blog => { %>
          <h2><%= blog.title %></h2>
          <p><%= blog.content %></p>
        <% }) %>
    <% } else { %>
        <p>投稿はありません</p>
    <% } %>
  </body>
</html>

上記コードのblogsにはmongooseのfindメソッドで検索したデータのリストが格納されていることになります。

http://localhost:3000にアクセスしてみます。

ここでもう一度http://localhost:3000/createにアクセスして同じデータを追加で保存してみます。

URLにアクセスするだけで再度データ送信が実行されます。

そしてhttp://localhost:3000に戻ってくるとデータが2件に増えていることが確認できます。

確認のためにMongoDBの管理画面も見てみましょう。

左メニューから「Database」をクリックします。

sample-dbというデータベースのblogsというコレクションをクリックするとデータが2件あります。

ブラウザと同じものがMongoDB側にも表示されていることが確認できました。

データのプロパティにはtitleとcontentがあるのに加えて_idというものがあります。

こちらは自動で追加されるプロパティで投稿を見分けるための識別番号です。

一覧にして表示するのではなく個別のデータだけを画面に表示する際に使用します。

それではhttp://localhost:3000/aboutで個別のデータを表示させてみます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');
const mongoose = require('mongoose');
const dbUrl ='mongodb+srv://admin1234@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000');
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  const blog = new Blog({
    title: 'テスト4',
    content: '本文です。',
  });

  blog
    .save()
    .then((data) => {
      res.send(data);
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
 Blog.find()
    .then((data) => {
      res.render('index', {
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

app.get('/about', (req, res) => {
 // ここを追加
  Blog.findById('6615ff154220721526868335')
    .then((data) => {
      res.send(data);
    })
    .catch((err) => console.log(err));
});

mongooseのfindByIdいうメソッドは引数に先ほどの_idの値を文字列にして入れることで対象のデータ1件を取得してくれます。

またこちらも非同期処理になっていてthenメソッドでは取得した単体のデータをブラウザに返すようにしました。

http://localhost:3000/aboutにアクセスしてみます。

http://localhost:3000/createの時と同じで作成したデータが再度表示できました。

このようにしてmongooseのメソッドを利用してAPI経由でMongoDBを操作することができます。

Node.jsのExpressの中でMongoDBを操作する【post,get,delete】

前章ではデータを手書きで書いていましたが実務ではユーザーが操作する場所です。

Expressにはユーザーの操作に合わせてMongoDBを操作するメソッドが用意されています。

ここでは投稿であるpostメソッド、取得であるgetメソッド、削除であるdeleteメソッドの使い方とmongooseの設定について紹介していきます。

Node.jsのExpressでpostメソッドを使ってMongoDBにデータを送信する

まずは投稿であるpostメソッドから紹介していきます。

mongoDBをmongooseのメソッドを使って連携する部分までは前章と同じになりますので割愛しますが以下のようなコードになります。

const express = require('express');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

モデルは以下のようになっていて、こちらも前章と同じ内容です。

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const blogSchema = new Schema(
  {
    title: {
      type: String,
      required: true,
    },
    content: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);
const Blog = mongoose.model('Blog', blogSchema);
module.exports = Blog;

まず投稿画面はhttps://localhost:3000/createというページにするためにルーティングを書きます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

// ここを追加
app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

続いてcreate.ejsを作ります。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>
    <form action="/" method="POST">
      <label for="title">タイトル:</label>
      <input type="text" id="title" name="title" required>
      <label for="body">本文:</label>
      <textarea id="content" name="content" required></textarea>
      <button>送信</button>
    </form>

  </body>
</html>

こちらの画面でタイトルと本文を入力して送信ボタンをクリックしたらMongoDBにデータを送信して、トップページhttp://localhost:3000にリダイレクトするようにします。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');


app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

// ここを追加
app.use(express.urlencoded({ extended: true }));
app.post('/', (req, res) => {
  console.log(req);
});

画面の表示がapp.getに対して送信はapp.postとして、第一引数には送信後にリダイレクトしたいパス、第二引数にはコールバック関数を書きます。

引数のreqにはリクエストオブジェクトが入っており、コンソールでreqを出すと送信したデータを確認できます。

またpostメソッドの直前でミドルウェアでurlencodedというメソッドを実行しておきます。

こちらでURLエンコードされたデータを取得してリクエストオブジェクト内に使用可能なオブジェクトを変換します。

引数のreqをコンソールで確認できるようにするために書いています。

http://localhost:3000/createからデータを送信してみます。

コンソールに大量のログがあり最後の方に以下のようになっていてbodyプロパティにデータがあることが確認できます。

レスポンスオブジェクトのbodyだけを指定するようにconsole.logを変更します。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');


app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
// ここを変更
  console.log(req.body);
});

それではもう一度送ってみましょう。

今度はシンプルな構成のオブジェクトでtitleとcontentに作成したデータがあるのが確認できました。

こちらのデータをBlogモデルをインスタンス化するときの引数に渡してあげれば良さそうですね。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');


app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
// ここを変更
  const blog = new Blog(req.body);
});

あとはインスタンスメソッドのsaveを使うとMongoDBに送信されます。

非同期処理のthenメソッドではexpressのredirectメソッドを使ってトップページに戻るようにしてみます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');


app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {

  const blog = new Blog(req.body);

// ここを追加
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

MongoDBのコレクションを確認すると前章と同じくデータがオブジェクト形式で格納されているのがわかります。

2回やったので同じデータが2件あるのは問題ありません。

最後にトップページにリダイレクトするのでExpressのgetメソッドを使ったルーティングと、ejsファイルで投稿データを表示するように作っておきます。

以下のコードになりますが前章でやったものと同じ内容です。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
  const blog = new Blog(req.body);
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

// ここを追加
app.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>

    <% if(blogs.length > 0){ %>
        <% blogs.forEach(blog => { %>
          <a href="">
            <h2><%= blog.title %></h2>
            <p><%= blog.content %></p>
          </a>
        <% }) %>
    <% } else { %>
        <p>投稿はありません</p>
    <% } %>
  </body>
</html>

http://localhost:3000に再度アクセスするとデータが表示されています。

説明を省略していましたがexpressのgetメソッドは何かしらのデータを取得して画面に表示するときに使用します。

前章ではMongDBを使ってなかっただけでブラウザに何かテキストなど表示するには必ずgetメソッドを使っていたはずです。

Node.jsのExpressでgetメソッドを使ってMongoDBからデータを取得する

それではデータの一覧はトップページで表示できたので、http://localhost:3000/aboutで1件だけの詳細ページを作ってみます。

WordPressのようなCMSツールを使われたことがあればイメージできるかと思いますが1件だけを取得するにはhttp://localhost:3000/idのようにURLの最後が動的なコードになるはずですね。

Expressのルーティングでも同じように書きます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
  const blog = new Blog(req.body);
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

// ここを追加
app.get('/about/:id', (req, res) => {
  console.log(req);
});

データの表示はgetメソッドで行うのですがデータは引数のreqオブジェクトに格納されております。

前章の復習ですが保存したデータはMongoDB側で_idというプロパティでIDが自動生成されます。

前章ではID名をコピペして手書きで書いていましたが、実際には_idを指定して動的にIDをリンクする方が好ましいです。

index.ejsに戻って投稿データの表示部分にaタグのhref属性を設定しておきましょう。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>Home</h1>

    <% if(blogs.length > 0){ %>
        <% blogs.forEach(blog => { %>

    <!-- ここを修正 -->
          <a href="/about/<%= blog._id %>">
            <h2><%= blog.title %></h2>
            <p><%= blog.content %></p>
          </a>
        <% }) %>
    <% } else { %>
        <p>投稿はありません</p>
    <% } %>
  </body>
</html>

これでそれぞれのデータから_idプロパティを指定したことになります。

検証ツールを確認するとわかります。

それではabout.ejsを作っておきましょう。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>ブログ詳細ページ</h1>
    <h3><%= blog.title %></h3>
    <p><%= blog.content %></p>
    <button data-doc="">削除</button>
  </body>
</html>

トップページからどれかのデータをクリックしてみます。

コンソールでリクエストオブジェクトが長い文章で表示されていてparamsというオブジェクトのidプロパティに_idで見た値が格納されていることがわかります。

そのためconsole.log(res.params.id)とすれば_idの値だけ取得できることがわかります。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
  const blog = new Blog(req.body);
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

app.get('/about/:id', (req, res) => {
// ここを修正
  console.log(req.params.id);
});

一度トップページに戻ってから再度データをクリックしてみましょう。

_idだけを取得できましたね。

あとはこれを持ってコールバック関数でmongooseのfindByIdメソッドを前章でやったように書くとデータの表示まで作れます。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
  const blog = new Blog(req.body);
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

app.get('/about/:id', (req, res) => {
 // ここを修正
  const blogId = req.params.id;
  Blog.findById(blogId)
    .then((data) => {
      res.render('about', { blog: data, title: 'ブログ詳細' });
    })
    .catch((err) => console.log(err));
});

findByIdメソッドのthenの部分ではabout.ejsを表示するようにしていますので、再度トップページに戻ってからデータをクリックすると以下のように1件だけの表示に切り替わります。

Node.jsのExpressでdeleteメソッドを使ってMongoDBからデータを削除する

最後にMongoDBに保存されているデータを削除したいと思います。

about.ejs側で削除ボタンを作っていたのですが、こちらはformタグの中にあるボタンではないので投稿の時とは違う方法が必要です。

formタグが無い以上はブラウザの移動ができません。

そのため非同期通信で画面の削除と裏側でMongoDBの中でデータの削除をすることになります。

まずはserver.jsでExpressのdeleteメソッドを書きます。

削除するデータは先ほどの1件取得と同じく_idの値を指定します。

const express = require('express');
const morgan = require('morgan');
const app = express();
app.set('view engine', 'ejs');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

app.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

app.post('/', (req, res) => {
  const blog = new Blog(req.body);
  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

app.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

app.get('/about/:id', (req, res) => {
  const blogId = req.params.id;
  Blog.findById(blogId)
    .then((data) => {
      res.render('about', { blog: data, title: 'ブログ詳細' });
    })
    .catch((err) => console.log(err));
});

// ここを追加
app.delete('/about/:id', (req, res) => {
  const blogId = req.params.id;
  Blog.findByIdAndDelete(blogId)
    .then(() => {
      res.json({ redirect: '/' });
    })
    .catch((err) => console.log(err));
});

_idの取得方法は先ほどと同じでreq.params.idとしているのと、削除するにはmongooseのfindByIdAndDeleteというメソッドの引数に_idを入れます。

こちらのthenメソッドではトップページにリダイレクトするようにしています。

しかし今のままだとブラウザ側では画面移動はしないためレスポンスでリダイレクト先のパスが返ってくるだけで、画面は削除されたはずのabout.ejsが表示され続けることになります。

そこでabout.ejsではserver.jsで返されたリダイレクト先のパスをもらってフロントでの画面移動を行います。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blogs/create">New</a></li>
      </ul>
    </nav>
    <h1>ブログ詳細ページ</h1>
    <h3><%= blog.title %></h3>
    <p><%= blog.content %></p>

    <!-- ここを修正 -->
    <button data-doc="<%= blog._id %>">削除</button>

   <!-- ここを追加 -->
    <script>
      const deleteItem = document.querySelector("button");

      deleteItem.addEventListener("click", (e) => {
        const endpoint = `/about/${deleteItem.dataset.doc}`;

        fetch(endpoint, {
          method: "DELETE",
        })
        .then((res) => res.json())
        .then((data) => {
          console.log(data);
          window.location.href = data.redirect;
        })
        .catch((err) => console.log(err));
      })
    </script>
  </body>
</html>

1件取得でやったように今いるページの_idを知りたいのでbuttonタグにdata属性を使って設定するように変更しました。

data属性に置いていた_idの値を使ってfetchメソッドを実行してwindow.loacation.hrefで画面移動をしています。

削除は別ページを作ってそこではformタグを設置して投稿と同じような流れで削除することも可能ですが、今回は簡易的にabout.ejsの中で削除ボタンを設置したので上記コードのようになりました。

とはいえ近年のプログラミングではフロントエンドでの画面移動も当たり前になりましたので上記のような使い方は初心者の方も練習しておくと良いかもしれません。

それでは削除ボタンをクリックしてみます。

前章で2件のデータを作成したので1件だけになっているはずです。

MongoDBも確認してみると画面と同じく1件だけのデータになっていますね。

Node.jsのExpressでMVCを使ったルーティングのグループ化

前章までで基本的なデータベースの操作方法を紹介してきましたがserver.jsの中身がやや長くなってきました。

リファクタリングの意味もこめてMVCモデルを紹介しておきます。

まずroutesというフォルダを作成し、その中にroutes.jsというファイルを作成しておきます。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs
-models
   |- blog.js
-routes
   |- routes.js
-server.js

ExpressにはRouterというクラスがインスタンス化することで現在のプロジェクトまでのルートパスを取得してくれます。

それによって今までserver.jsで書いていた関数を/routes/routes.jsに切り出すことができます。

const express = require('express');
const Blog = require('../models/blog');
const router = express.Router();

router.get('/create', (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
});

router.post('/', (req, res) => {
  const blog = new Blog(req.body);

  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
});

router.get('/', (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
});

router.get('/about/:id', (req, res) => {
  console.log(req.params.id);
  const blogId = req.params.id;
  Blog.findById(blogId)
    .then((data) => {
      res.render('about', { blog: data, title: 'ブログ詳細' });
    })
    .catch((err) => console.log(err));
});

router.delete('/about/:id', (req, res) => {
  const blogId = req.params.id;
  Blog.findByIdAndDelete(blogId)
    .then(() => {
      res.json({ redirect: '/' });
    })
    .catch((err) => console.log(err));
});

module.exports = router;

またコードの最後にmodules.exportsを使ってエクスポートしておき、関数を取り除いたserver.js側でインポートして/routes/routes.jsから関数を読み込むようにします。

関数を読み込む方法ですがuseメソッドを使ったミドルウェアとして読み込ます形になります。

ミドルウェアという仕組みを使うことでリクエストとレスポンスの間で関数を呼び出すことになり、結果的にもともとserver.jsに関数を書いていたのと同じ動作を実現できるわけです。

const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));

// ここを追加
const router = require('./routes/routes');

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

// ここを追加
app.use(router);

これでserver.js側は綺麗になりましたが/routes/routes.jsの中身が結局は見づらい状況になっただけです。

そこでExpressのControllerというクラスを使って関数を名前付きの変数に書き直して、名前として呼び出すことができるようになります。

以下のようにcontrollersというフォルダを作って、その中にcontroller.jsというファイルを作成します。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs
-models
   |- blog.js
-routes
   |- routes.js
-controllers
   |- controller.js
-server.js

/controllers/controller.jsの中ではまずControllerというクラスをインスタンス化しておきます。

続いて/routes/routes.jsに切り出した関数をアロー関数の形に作って変数に格納する書き方にします。

const Blog = require('../models/blog');

const blogCreatePage = (req, res) => {
  res.render('create', { title: 'ブログ投稿' });
};

const createdBlogPost = (req, res) => {
  const blog = new Blog(req.body);

  blog
    .save()
    .then((data) => {
      res.redirect('/');
    })
    .catch((err) => console.log(err));
};

const blogIndex = (req, res) => {
  Blog.find()
    .then((data) => {
      res.render('index', {
        title: 'トップページ',
        blogs: data,
      });
    })
    .catch((err) => console.log(err));
};

const blogAbout = (req, res) => {
  const blogId = req.params.id;
  Blog.findById(blogId)
    .then((data) => {
      res.render('about', { blog: data, title: 'ブログ詳細' });
    })
    .catch((err) =>
      res.status(404).render('404', {
        title: 'ページが見つかりませんでした',
      })
    );
};

const blogDelete = (req, res) => {
  const blogId = req.params.id;
  Blog.findByIdAndDelete(blogId)
    .then(() => {
      res.json({ redirect: '/' });
    })
    .catch((err) => console.log(err));
};

module.exports = {
  createdBlogPost,
  blogCreatePage,
  blogIndex,
  blogAbout,
  blogDelete,
};

最後に作った変数化したアロー関数をエクスポートして/routes/routes.jsでインポートするようにしておきます。

const express = require('express');
const router = express.Router();

const controller = require('../controllers/controller');

router.get('/create', controller.blogCreatePage);

router.post('/', controller.createdBlogPost);

router.get('/', controller.blogIndex);

router.get('/about/:id', controller.blogAbout);

router.delete('/about/:id', controller.blogDelete);

module.exports = router;

/routes/routes.jsの中身でgetメソッド、postメソッド、deleteメソッド共に第一引数は実行するパス名、第二引数が関数が格納された変数名だけを書く方法に変わります。

最終的にserver.jsにエクスポートするので、controller.js→routes.js→server.jsと中継しながら処理の内容が渡されserver.js内のミドルウェアとしてリクエストとレスポンスの間で読み込まれ実行されます。

Node.jsでdotenvを使ったキーやURLの非表示

最後にdotenvというミドルウェアのライブラリを紹介します。

まずはインストールしてインポートしておき、configというメソッドを実行しておきます。

npm i dotenv
const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));
const router = require('./routes/routes');

// ここを追加
const dotenv = require('dotenv');
// ここを追加
dotenv.config();

const dbUrl =
  'mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/sample-db?retryWrites=true&w=majority&appName=mongo-basic';

const mongoose = require('mongoose');
mongoose
  .connect(dbUrl)
  .then((res) => app.listen('3000'))
  .catch((err) => console.log(err));

const Blog = require('./models/blog');

// ここを追加
app.use(router);

JavaScriptのプロジェクトでは「.env」というファイルを作るとソースコード上でAPIキーやURL、パスワードなど大事な情報を非表示にして書くことができるようになります。

上記のコードで言うところの定数dbUrlとapp.listenのポート番号になります。

こちらをまず「.env」ファイルに逃しておきましょう。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs
-models
   |- blog.js
-routes
   |- routes.js
-controllers
   |- controller.js
-server.js
-.env
PORT = "3000"
MONGO_DB = "mongodb+srv://admin:ttc19930104@mongo-basic.hwnaw5l.mongodb.net/mongo-db?retryWrites=true&w=majority&appName=mongo-basic"

隠したい値は変数のように特定のキーワードで差し替える形になるので、上記コードのようにPORTとMONGO_DBというキーワードに代入しておきます。

続いてserver.jsに戻ってキーワードに差し替えますが、proccess.envと続けることで「.env」の中で書いたキーワードから候補を探してくれるようになります。

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

const dotenv = require('dotenv');
dotenv.config();

// ここを修正
const dbUrl = process.env.MONGO_DB;
mongoose
  .connect(dbUrl)
  .then(
    // ここを修正
    () => app.listen(process.env.PORT, () => console.log(`${process.env.PORT}`))
  )
  .catch((err) => console.log(err));

定数dbUrlについてはMONGO_DBというキーワードに代入しておいたのでprocess.env.MONGO_DBとして、app.listenのポート番号についてはPORTというキーワードに代入しておいたのでprocess.env.PORTと書きます。

これでソースコードに値を表示させずに動作を行うことができます。

またGithubにあげるときには.envを.gitignoreというファイルの中に書きます。

.envに逃しても結局Githubに上げてしまったら確認できてしまいます。

そこで.gitignoreという特殊なファイルを作成すると、その中に書いたファイルはGitでプッシュされないことになります。

-views
   |- index.ejs
   |- about.ejs
   |- create.ejs
-models
   |- blog.js
-routes
   |- routes.js
-controllers
   |- controller.js
-server.js
-.env
-.gitignore
.env

ユーザーから見たら違いはわかりませんがプロジェクトを安全に共有する上では必須の知識になり、チーム開発では登場する言葉なので初心者の方も理解しておくようにしてください。

Node.jsでMongoDBにハッシュ化したパスワードを保存する

セキュリティ対策の別の例としてパスワードのハッシュ化というものも紹介します。

ユーザー情報を保存するときにパスワードをMongoDBに保存するわけですが、実際にはパスワードをそのまま保存するのはあまり好ましくないです。

MongoDBの管理画面を覗かれたときに備えてユーザーのパスワードはダミーのテキストに変換しておくのが実務でよくやるパターンです。

専門用語でハッシュ化とも呼ばれていてNode.jsではbcryptというライブラリを使うと簡単に実装できます。

bcryptはサードパーティ製のライブラリですのでインストールしておきます。

npm i bcrypt

続いてユーザー登録のモデルを作成します。

usernameがユーザー名でpasswordがパスワードになり2項目をユーザー情報として保存する想定で作りました。

const mongoose = require('mongoose');

const userSchema = mongoose.Schema(
  {
    username: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);
const userModel = mongoose.model('Users', userSchema);
module.exports = userModel;

コントローラーでモデルとbcryptをインポートします。

bcryptはgenSaltというメソッドでハッシュ化したいパスワードの文字数を指定します。

以下の例では8桁にしています。

またhashというメソッドがあり第一引数にパスワードが格納された変数、第二引数にgenSaltの戻り値を格納した変数を入れます。

そうするとhashメソッドの戻り値にダミーテキストに変換されたパスワードが保存される仕組みです。

あとはモデル側でパスワードをpasswordという項目で用意していましたが、ハッシュ化されたhashedPassに置き換えておけばOKです。

const Blog = require('../models/AuthModel');

// bcryptをインポート
const bcrypt = require('bcrypt');

const registerUser = async (req, res) => {
  const { username, password } = req.body;

  // パスワードの桁数を指定
  const salt = await bcrypt.genSalt(8);
  // ハッシュ化の実行
  const hashedPass = await bcrypt.hash(password, salt);

  const newUser = new userModel({
    username,
    password: hashedPass,
  });

  try {
    await newUser.save();
    res.status(200).json(newUser);
  } catch (error) {
    res.status(500).json({
      message: error.message,
    });
  }
};

module.exports = { registerUser };

例えば「test1234」というパスワードを送信しても以下のような形で保存されます。

また今回参考にした書籍はこちらになりますので良ければどうぞ。

  • このエントリーをはてなブックマークに追加