Dockerで簡易版Aidemyをつくってみよう (その4) ~mongodbによるデータベース~

# 概要
今回は、Dockerで簡易版Aidemyをつくってみようシリーズのその4です。
前回までにフロントエンド・バックエンドを作成したので、今回はデータベースを作成します。

ちなみに、前回までのリンクは以下です。

Dockerで簡易版Aidemyをつくってみよう (その1) ~全体構成とDockerのセットアップ~
https://qiita.com/km42428/items/4e4653aa0e813282986b

Dockerで簡易版Aidemyをつくってみよう (その2) ~React.jsによるフロントエンド~
https://qiita.com/km42428/items/989dac5e7450501c452d

Dockerで簡易版Aidemyをつくってみよう (その3) ~Expressによるバックエンド~
https://qiita.com/km42428/items/f5f7e8f15546b0673d95

# 今回の目標
今回は `mondodb` というデータベースを作成し、エクササイズの情報を格納することと、それをバックエンドから呼び出せるようにすることを目標にします。こちらも、今までと同様に `docker-compose` で起動することを考えます。

# mongodb について
mongodbとは、オープンソースのデータベースで、SQLとは異なりドキュメントという形式でデータを保存しています。
公式ドキュメントはこちらです。
https://docs.mongodb.com/

ちなみにドキュメントはJSONないしBSON(Binary JSON)という形式で保存されています。
https://www.mongodb.com/json-and-bson

(ドキュメントという言葉がたくさんでてややこしい。。)

# mongodb のDocker Image
mongodbの公式イメージはDocker Hub で公開されています。
https://hub.docker.com/_/mongo/
今回記述していくDockerfileは、こちらのイメージをベースに作成していきます。

## docker-compose の更新
以下のように `docker-compose.yml` ファイルを更新します。

```yaml

version: "3"
services:
  aidemy-mongodb:
    image: mongo:4.0.3
    ports:
      - "28001:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin-user
      MONGO_INITDB_ROOT_PASSWORD: admin-password
      MONGO_INITDB_DATABASE: admin
    volumes:
      - ./db/:/docker-entrypoint-initdb.d/
  aidemy-frontend:
    build: ./aidemy-frontend
    image: aidemy-frontend
    container_name: aidemy-frontend
    environment:
      - ENV
    ports:
      - 5000:5000
  aidemy-backend:
    build: ./aidemy-backend
    image: aidemy-backend
    container_name: aidemy-backend
    environment:
      - ENV
    ports:
      - 4000:4000
    links:
      - aidemy-mongodb
```

# データの準備
`admin` というデータベースにexercise情報を入れていきます。

## aidemy-mongodb コンテナーの立ち上げ

```
$ docker-compose build
$ docker-compose up
```

## mongodbの中に入る

admin データベースには権限が必要なので、認証情報をふくめてmongodbにログインする。
参考: https://medium.com/@itseranga/enable-mongodb-authentication-with-docker-1b9f7d405a94

```shell

$ mongo -u admin-user -p admin-password --authenticationDatabase admin --port 28001
```

```
MongoDB shell version v4.0.4
connecting to: mongodb://127.0.0.1:28001/
Implicit session: session { "id" : UUID("1cdc7c13-25c1-4802-9f52-2a71a55f17cf") }
MongoDB server version: 4.0.3
Server has startup warnings:
2018-12-24T08:04:43.407+0000 I STORAGE  [initandlisten]
2018-12-24T08:04:43.407+0000 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2018-12-24T08:04:43.407+0000 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2018-12-24T08:04:44.050+0000 W CONTROL  [initandlisten]
2018-12-24T08:04:44.050+0000 W CONTROL  [initandlisten]
2018-12-24T08:04:44.050+0000 I CONTROL  [initandlisten]
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).

The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.

To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
> 
```

## exercisesデータを入れる

```
// データベース一覧確認
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

// adminデータベース選択
> use admin
switched to db admin

// exercises コレクション作成
> db.createCollection('exercises')
{ "ok" : 1 }

// exercise コレクションにexercise情報を挿入
> db.exercises.insertMany([
{
  exerciseId: "exercise1",
  title: "1. Hello world",
  script: "# Hello worldを出力しましょう\n",
  answer: "# Hello worldを出力しましょう\nprint('Hello world')"
},
{
  exerciseId: "exercise2",
  title: "2. コメントの入力",
  script: "# 3 + 5 の結果を出力しましょう\n",
  answer: "# 3 + 5 の結果を出力しましょう\nprint(3 + 5)"
}
])
{
        "acknowledged" : true,
        "insertedIds" : [
                ObjectId("5c20943891054e51384ed90d"),
                ObjectId("5c20943891054e51384ed90e")
        ]
}

// 挿入されたか確認
> db.exercises.find()
{ "_id" : ObjectId("5c20943891054e51384ed90d"), "exerciseId" : "exercise1", "title" : "1. Hello world", "script" : "# Hello worldを出力しましょう\n", "answer" : "# Hello worldを出力しましょう\nprint('Hello world')" }
{ "_id" : ObjectId("5c20943891054e51384ed90e"), "exerciseId" : "exercise2", "title" : "2. コメントの入力", "script" : "# 3 + 5 の結果を出力しましょう\n", "answer" : "# 3 + 5 の結果を出力しましょう\nprint(3 + 5)" }
>
```

※ もし、exercise情報挿入で間違ってしまった場合は、exercisesコレクションを削除する

```
// コレクションの削除
> db.exercises.drop()
true
```

# aidemy-backendからmongodbへアクセス。
aidemy-backend/index.js を以下を追加する。

```diff

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  res.header(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"
  );
  next();
});

+ const mongodb = require("mongodb");
+ const MongoClient = mongodb.MongoClient;
+ 
+ // mongodbへの接続文字列
+ var url = "mongodb://admin-user:admin-password@aidemy-mongodb:27017/admin";
+ var exercises;
+ // mongodbの準備ができたらexercisesを初期化する
+ (async function() {
+   console.log("init");
+   for (let i = 0; i < 10; i++) {
+     console.log(i);
+     try {
+       const client = await MongoClient.connect(url);
+       const db = client.db("admin");
+       db.collection("exercises", (err, collection) => {
+         collection.find().toArray((err, docs) => {
+           if (docs.length > 0) {
+             exercises = docs;
+             console.log(docs);
+           }
+         });
+       });
+       break;
+     } catch (err) {
+       console.log(err);
+       await new Promise(resolve => {
+         setTimeout(() => {
+           resolve();
+         }, 1000);
+       });
+       continue;
+     }
+   }
+ })();

- const exercises = [
- {
-   exerciseId: "exercise1",
-   title: "1. Hello world",
-   script: "# Hello worldを出力しましょう\n",
-   answer: "# Hello worldを出力しましょう\nprint('Hello world')"
- },
- {
-   exerciseId: "exercise2",
-   title: "2. コメントの入力",
-   script: "# 3 + 5 の結果を出力しましょう\n",
-   answer: "# 3 + 5 の結果を出力しましょう\nprint(3 + 5)"
- }
- ];

function getExercise(exerciseId) {
  return exercises.find(el => {
    return el.exerciseId === exerciseId;
  });
}

```

# 再度起動する

```
$ docker-compose build
$ docker-compose up
```

今回は以上になります。
次回はpythonを実行するサーバを作成します！