MongoDB+Express+AngularJS+Node.jsでシンプルなCRUDアプリ作成

# 手作りMEAN Stack

業務アプリをAngularJS+PHP+MySQLで組んでいたのですが、JavaScriptとPHPの文法の微妙な違いのおかげでケアレスミスが多く出てきてしまいました。

そこで、フロントエンドもバックエンドもJavaScriptで統一できる、流行りのMEAN Stack(MongoDB+Express+AngularJS+Node.js)を試してみようと考えた次第です。

MEAN Stackのひな形を作るツールもいろいろあるのですが、今回は手作りで作成してみます。
WindowsとUbuntuの導入方法を挙げますが、他のOSでもほとんど同じ感じでいけると思います。

# M: MongoDBのインストール・設定

言わずと知れたNoSQL界の雄です。

## Windowsの場合

http://www.mongodb.org/ こちらからダウンロードしてインストールします。今回は ```C:\MongoDB``` にインストールしました。

### 設定

標準ではDB格納フォルダが ```/data/db``` に作られますが、ちょっと分かりにくいので ```C:\MongoDB\data``` に格納することにします。予め ```C:\MongoDB\data``` フォルダを作成しておきます。
 
サービスとしてインストールする場合は、ログを格納する場所も必要なので、 ```C:\MongoDB\log``` フォルダも合わせて作成しました。

### サービスとしてインストールする

```
mongod --install --dbpath=C:\MongoDB\data --logpath=C:\MongoDB\log\mongo.log --logappend
```

```--logpath``` はファイル名まで指定する必要があるので注意。

## Ubuntuの場合

http://docs.mongodb.org/master/tutorial/install-mongodb-on-ubuntu/
こちらを参考にリポジトリを追加してインストールします。root権限で

```
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
apt update
apt install mongodb-org
```

データは ```/var/lib/mongodb``` 、ログは ```/var/log/mongodb``` に入ります。

## Mongoシェル

インストールが終わったら、シェル（mongoコマンド）を起動していろいろ試してみます。

[Mongoシェルと戯れる](http://kozy4324.github.io/blog/2012/04/28/mongodb-basic-ope/)
[MongoDBのshellを使い倒す](http://tech.nifty.co.jp/2011/11/25/mongodb%E3%81%AEshell%E3%82%92%E4%BD%BF%E3%81%84%E5%80%92%E3%81%99/)

等の記事を参考にしました。JavaScriptがそのまま使えるのでいろんなことが出来そうです。

## GUIツール

GUIでデータベースを操作できるツールもいろいろあるようですが、中でも[Robomongo](http://robomongo.org/)と[RockMongo](http://rockmongo.com/)が使いやすいと思いました。

# N: Node.jsのインストール

サーバーサイドJavaScriptです。

## Windowsの場合

公式サイト http://nodejs.org/ から簡単にインストールできます。特に設定はしませんでした。

## Ubuntuの場合

PPAからリポジトリを追加してインストールします。

```
add-apt-repository -y ppa:chris-lea/node.js
apt update
apt install nodejs
```

# E: Expressのインストールと設定

Node.js用のウェブアプリケーションフレームワークです。Node.js単体でも頑張ればなんとかなりますが、こちらを導入したほうが圧倒的に楽にアプリを作成できます。

ExpressはNode.jsのパッケージマネージャであるnpmでインストールします。今回のアプリ用に適当なフォルダを作成し、その中で以下のコマンドを実行します。

```
npm install express
```

```node_modules``` フォルダ以下にインストールされます。

または、グローバルインストールしてそちらにシンボリックリンクを張っても良いと思います。

```
npm install -g express
npm link express
```

最近のWindowsではちゃんとシンボリックリンクを張ってくれます。

## body-parserのインストール

リクエストボディの解釈をするために追加モジュールで ```body-parser``` をインストールします。

```
npm install body-parser
```

## node-mongodb-nativeドライバのインストール

```
npm install mongodb
```

Node.jsからMongoDBをアクセスするための基本的なモジュールです。[Mongoose](http://mongoosejs.com/)が有名ですが、今回はよりネイティブなこちらを選択しました。

# A: AngularJS

AngularJSはCDNから引っ張ってくるので特にインストールする必要はありません。

# CRUDアプリの作成

## 初期データの投入

Mongoシェルを起動すると、

```
connecting to: test
```

と表示されてデータベースtestに接続しているようなので、それをそのまま使います。以下のコマンドを実行します。

```
db.users.insert([
  {name: "勅使河原", age: 19},
  {name: "長宗我部", age: 20},
  {name: "小比類巻", age: 21}
])
```

dbは現在接続しているデータベースtest、usersはコレクション（RDBで言うところのテーブルのようなもの）名です。コレクションが存在しないときは自動的に作成されます。JSONと似た形式でドキュメント（RDBで言うところのレコードのようなもの）を挿入できます。

投入されたデータは以下のコマンドで確認できます。

```
db.users.find()
```

自動的にObjectId（RDBで言うところのプライマリキー）が付与されています。

## ソースコード

作成が面倒な方は、まとめてこちらに置いてあります。
https://github.com/naga3/mean-basic

### ファイル・フォルダ構成

```
アプリのフォルダ
├app.js →エントリポイント
├node_modules/ →npmで導入したファイル群
└api/
　└users →usersコレクションを読み書きするAPIアドレス
└front/ →フロントエンド
　　├index.html
　　├list.html
　　└edit.html
```

作る必要のあるものは、app.jsファイルとfront/フォルダとそれ以下のファイルです。

### バックエンド

```js:app.js
var express = require('express');
var bodyParser = require('body-parser');
var mongodb = require('mongodb');

var app = express();
var users;

app.use(express.static('front'));
app.use(bodyParser.json());
app.listen(3000);

mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {
  users = database.collection("users");
});

// 一覧取得
app.get("/api/users", function(req, res) {
  users.find().toArray(function(err, items) {
    res.send(items);
  });
});

// 個人取得
app.get("/api/users/:_id", function(req, res) {
  users.findOne({_id: mongodb.ObjectID(req.params._id)}, function(err, item) {
    res.send(item);
  });
});

// 追加・更新
app.post("/api/users", function(req, res) {
  var user = req.body;
  if (user._id) user._id = mongodb.ObjectID(user._id);
  users.save(user, function() {
    res.send("insert or update");
  });
});

// 削除
app.delete("/api/users/:_id", function(req, res) {
  users.remove({_id: mongodb.ObjectID(req.params._id)}, function() {
    res.send("delete");
  });
});
```

### フロントエンド

#### index

```html:front/index.html
<!doctype html>
<html lang="ja" ng-app="app">
<meta charset="utf-8">
<title>ユーザー管理</title>
<div ng-view></div>
<script src="//code.angularjs.org/1.3.15/angular.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-resource.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-route.min.js"></script>
<script>
  var app = angular.module('app', ['ngResource', 'ngRoute']);

  app.config(function($routeProvider) {
    $routeProvider.when('/users', {
      templateUrl: 'list.html', controller: 'ListCtrl'
    }).when('/users/:_id', {
      templateUrl: 'edit.html', controller: 'EditCtrl'
    }).otherwise({
      redirectTo: '/users'
    });
  });

  app.factory('User', function($resource) {
    return $resource('/api/users/:_id');
  });

  app.controller('ListCtrl', function($scope, $route, User) {
    $scope.users = User.query();
    $scope.delete = function(_id) {
      User.delete({_id: _id}, function() {
        $route.reload();
      });
    };
  });

  app.controller('EditCtrl', function($scope, $routeParams, $location, User) {
    if ($routeParams._id != 'new') $scope.user = User.get({_id: $routeParams._id});
    $scope.edit = function() {
      User.save($scope.user, function() {
        $location.url('/');
      });
    };
  });
</script>
</html>
```

#### 一覧

```html:front/list.html
<a ng-href="#/users/new">新規</a>
<table border="1">
  <tr><th>&nbsp;</th><th>氏名</th><th>年齢</th></tr>
  <tr ng-repeat="user in users">
        <td>
          <a ng-href="#/users/{{user._id}}">編集</a>
          <button ng-click="delete(user._id)">削除</button>
        </td>
        <td>{{user.name}}</td>
        <td>{{user.age}}</td>
  </tr>
</table>
```

#### 追加・編集

```html:front/edit.html
氏名 <input ng-model="user.name"><br>
年齢 <input ng-model="user.age"><br>
<button ng-click="edit()">登録</button>
<a ng-href="/#/users">戻る</a>
```

## 実行方法

アプリのトップフォルダで

```
node app
```

でapp.jsを実行し、ブラウザで

```
http://localhost:3000/
```

にアクセスします。

### 一覧画面

![list.png](https://qiita-image-store.s3.amazonaws.com/0/32030/7b8f4089-f4c1-cfb2-a642-6a69e7e53b78.png)

### 編集画面

![edit.png](https://qiita-image-store.s3.amazonaws.com/0/32030/af75529e-557f-f5d2-4281-9e6616bd6909.png)

## app.js 解説

```js
var app = express();
```

expressアプリケーションのインスタンスを作成します。

```js
app.use(express.static('front'));
app.use(bodyParser.json());
```

expressの追加機能であるミドルウェアを読み込んでいます。

staticミドルウェアは、指定したフォルダ内のファイルを、静的ファイルとして公開できます。

bodyParserミドルウェアは、POSTメソッドで送られてきたリクエストボディを解析し、JSONデータは自動的にオブジェクトにしてくれます。

```js
app.listen(3000);
```

3000番ポートで接続を待ち受けます。

```js
mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {
```

localhostのMongoDBのデータベース「test」に接続します。コールバック引数のdatabaseにデータベースオブジェクトが返ってきます。

MongoDBのデフォルトポート番号は27017です。

```js
  users = database.collection("users");
```

変数usersにコレクションusersのオブジェクトが入ります。

### GET /api/users

```js
app.get("/api/users", function(req, res) {
  users.find().toArray(function(err, items) {
    res.send(items);
  });
});
```

コレクションの一覧を配列で返します。 ```users.find()``` でコレクション全体を取得し、 ```toArray``` で配列にして出力しています。 ```$resource``` の ```query()``` に対応する部分です。

### GET /api/users/:_id

```js
app.get("/api/users/:_id", function(req, res) {
  users.findOne({_id: mongodb.ObjectID(req.params._id)}, function(err, item) {
    res.send(item);
  });
});
```

ドキュメントひとつを返します。指定したObjectIdのドキュメントを ```users.findOne()``` で取得し出力しています。ObjectIdは文字列型では無いので変換しています。 ```$resource``` の ```get()``` に対応する部分です。

### POST /api/users

```js
app.post("/api/users", function(req, res) {
  var user = req.body;
  if (user._id) user._id = mongodb.ObjectID(user._id);
  users.save(user, function() {
    res.send("insert or update");
  });
});
```

リクエストボディに入ってくるJSONデータによってドキュメントを挿入・更新します。saveメソッドは_idが存在しなければinsert、存在すればupdateになります。_idは文字列型→ObjectId型に変換しています。

### DELETE /api/users/:_id

```js
app.delete("/api/users/:_id", function(req, res) {
  users.remove({_id: mongodb.ObjectID(req.params._id)}, function() {
    res.send("delete");
  });
});
```

指定したObjectIdのドキュメントを削除します。

## index.html 解説

```html
<div ng-view></div>
```

ngRouteの機能により、テンプレートのHTMLファイルがここに埋め込まれます。

```html
<script src="//code.angularjs.org/1.3.15/angular.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-resource.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-route.min.js"></script>
```

CDNよりAngularJSと、REST APIを扱うためのngResource、ルーティングするためのngRouteを読み込みます。

```js
  app.config(function($routeProvider) {
    $routeProvider.when('/users', {
      templateUrl: 'list.html', controller: 'ListCtrl'
    }).when('/users/:_id', {
      templateUrl: 'edit.html', controller: 'EditCtrl'
    }).otherwise({
      redirectTo: '/users'
    });
  });
```

ルーティングの設定です。

```/#/users``` にアクセスされたときに ```list.html``` を読み込んで展開します。

```/#/users/_id``` (_idは各ドキュメントのObjectId）にアクセスされたときに ```edit.html``` を読み込んで展開します。

その他のときは ```/#/users``` にリダイレクトします。

```js
  app.factory('User', function($resource) {
    return $resource('/api/users/:_id');
  });
```

app.jsで定義したREST APIを使うためのサービス ```User``` を定義しています。

```js
  app.controller('ListCtrl', function($scope, $route, User) {
    $scope.users = User.query();
    $scope.delete = function(_id) {
      User.delete({_id: _id}, function() {
        $route.reload();
      });
    };
  });
```

一覧画面のコントローラです。 ```User.query()``` で ```GET /api/users``` が呼び出されコレクションusersの一覧が取得されます。
削除リンクが押されたときは ```DELETE /api/users/:_id``` が呼び出され、ドキュメントを削除して画面を更新します。

```js
  app.controller('EditCtrl', function($scope, $routeParams, $location, User) {
    if ($routeParams._id != 'new') $scope.user = User.get({_id: $routeParams._id});
    $scope.edit = function() {
      User.save($scope.user, function() {
        $location.url('/');
      });
    };
  });
```

編集画面のコントローラです。URLが ```/#/users/new``` のときは新規、その他のときは編集になります。
登録ボタンが押されたときは ```POST /api/users``` が呼び出され、ドキュメントの挿入・更新を行います。

# 終わりに

慣れていない技術の組み合わせなので、無駄や間違っているところがあると思います。指摘して頂けたら嬉しいです。