Windows 10 に MongoDB をインストールして Python から利用する

## はじめに

Azure Cosmos DB (Document + MongoDB API + Python) の利用に向けた準備として、過去に手元でまとめていた MongoDB の情報を見直してみました。

## MongoDB とは

> MongoDBは、オープンソースのドキュメント指向データベース（NoSQL）です。
> RDBMSのようにレコードをテーブルに格納するのではなく、「ドキュメント」と呼ばれる構造的データをJSONライクな形式で表現し、そのドキュメントの集合を「コレクション」として管理します。

最近では、様々な RDBMS が JSON を直接扱えるようになってきているので、MongoDB でなければならないというメリットはあまりないかもしれませんね。スケールの特徴というか目的に応じてといったところでしょうか。

## Azure Cosmos DB とは

> Azure Cosmos DB は、ターンキーグローバル配布と透過的なマルチマスターレプリケーションを備えたフルマネージドデータベースサービスです。99パーセンタイルでの10ミリ秒未満の読み書き待機時間、世界規模のスループットとストレージの自動エラスティックスケーリング、99.999% の高可用性が実現し、明確に定義された5種類の整合性オプションが用意されています。これらはすべて、業界トップレベルの包括的な SLA の対象となります。

だそうですが、こんな感じです。

> Azure Cosmos DB は、データの保存方式とAPIを選択できる分散データベース基盤で、データの保存方式は、Document、Graph、Key-Value の３つの NoSQL データモデルから選択できます。そして、それぞれのデータモデルで利用できる API を提供しています。

Azure Cosmos DB を使ってみるという目標に向けて、今回は MongoDB + PyMongo API + Python を試してみます。

## 環境

* Windows 10 Pro x64 1909
* Power Shell 6 x64
* Visual Studio Code 1.42.1 x64
* Python 3.7.6 x64
* Nodist 0.9.1 ( Node v12.16.0 ) Mongo Express 用

## MongoDB のインストール

1. MongoDB Community Server のインストーラーを[ダウンロード](https://www.mongodb.org/)します。

1. ダウンロードしたインストーラー（例：mongodb-win32-x86_64-2012plus-4.2.3-signed.msi）を実行します。

1. [Custom Setup] を選択し、インストール先のパス（例：D:\Develop\Bin\MongoDB\Server\4.2\）を指定します。

1. [Install MongoD as a Service] をチェックして進みます。（Windowsサービスとして登録するため）

1. [Install MongoDB Compass] をチェックして進みます。（MongoDB の GUI CRUD ツール）

1. インストールを行います。

## MongoDB の設定

MongoDB のパスを環境変数に追加します。

1. MongoDBの「bin」ディレクトリ（例：D:\Develop\Bin\MongoDB\Server\4.2\bin）をPATHに追加します。

1. MongoDBのPATHが設定されたことを確認します。

    ```powershell
    > Get-PSDrive
    > Set-Location Env:
    > Get-ChildItem | Where-Object { $_.Name -eq 'Path' } | Format-List
    ```

## Install MongoDB as a Service をチェックしなかった場合（その１）

手動で data ディレクトリと logs ディレクトリを作成し、MongoDB の起動を確認します。

1. インストールディレクトリにデータベース用の「data」ディレクトリとログ用の「logs」ディレクトリを作成します。

    ```powershell
    > cd D:\Develop\Bin\MongoDB\Server\4.2
    > mkdir data
    > mkdir logs
    ```

1. MongoDBの起動を確認してみます。

    ```powershell
    > mongod --dbpath D:\Develop\Bin\MongoDB\Server\4.2\data --logpath D:\Develop\Bin\MongoDB\Server\4.2\mongod.log
    ```

1. netstatでポートを確認します。（デフォルトポート:27017）

    ```powershell
    > Get-NetTCPConnection | Where-Object { $_.LocalPort -eq 27017 -or $_.RemotePort -eq 27017 }
    > Get-NetTCPConnection -LocalPort 27017 （こちらでもOK）
    ```

## ユーザーの登録・更新・削除

1. MongoDBが起動している状態で、ユーザーの登録を行います。

    ```powershell
    > mongo
    > use admin
    > db.createUser( { user:"admin", pwd:"password", roles:[{ "role" : "root", "db" : "admin" }] } );
    > use test
    > db.createUser( { user:"hoge", pwd:"password", roles:[{ "role" : "dbOwner", "db" : "test" }] } );
    > db.createUser( { user:"fuga", pwd:"password", roles:[{ "role" : "readWrite", "db" : "test" }] } );
    > exit
    ```

1. ユーザーを確認します。

    ```powershell
    > mongo
    > use admin
    > db.system.users.find();
    > exit
    ```

1. ユーザーを更新します。

    ```powershell
    > use test
    > db.updateUser( "hoge", { roles:[{ "role" : "readWrite", "db" : "test" }]  } );
    ```

1. 参考までにユーザー削除。

    ```powershell
    > use admin
    > db.system.users.remove( { "_id" : "admin" } );
    ```

## Install MongoDB as a Service をチェックしなかった場合（その２）

手動で mongod.conf ファイルを作成し、MongoDB をサービスとして登録します。

1. ユーザーを登録したら「Ctrl+C」でMongoDBを終了します。

1. インストールディレクトリの mongod.cfg ファイルを編集します。

    ```powershell
    > cd D:\Develop\Bin\MongoDB\Server\4.2\bin
    > code mongod.cfg
    ```

1. mongod.cfg ファイルに下記内容を記述します。

    ```text
    storage:
      dbPath: D:\Develop\Bin\MongoDB\Server\4.2\data
      journal:
        enabled: true

    systemLog:
      destination: file
      logAppend: true
      path:  D:\Develop\Bin\MongoDB\Server\4.2\log\mongod.log

    net:
      port: 27017
      bindIp: 127.0.0.1

    security:
      authorization: enabled
    ```

    > auth=trueで認証を有効にしています。そのために事前にユーザーを登録しています。

1. 「mongod.cfg」ファイルを指定してWindowsサービスとして登録します。（管理者として実行）

    ```powershell
    > mongod --config "D:\Develop\Bin\MongoDB\Server\4.2\bin\mongod.cfg" --install --serviceName MongoDB
    ```

    > サービスとして登録しない場合は「--install」を指定しません。

1. 参考までにサービス削除。（サービスが停止している状態で実行）

    ```powershell
    > mongod --remove
    ```

## MongoDB サービスの開始と停止

1. コントロールパネル - 管理ツール - サービス にMongoDBが登録されていることを確認します。

1. netコマンドを利用してサービスの開始と停止を制御できます。（管理者として実行）

    ```powershell
    > net start MongoDB
    > net stop MongoDB
    ```

## MongoDB の利用

1. MongoDB のコマンドラインクライアントが起動できることを確認します。

    ```powershell
    > mongo
    ```

    > exitでコマンドラインクライアントを終了できます。

1. 認証を必要としている（認証していない）場合は、以下のコマンドで認証します。

    ```powershell
    > use test
    > db.auth("hoge", "password");
    ```

1. MongoDB のコマンドラインクライアントを起動する際に指定することもできます。

    > adminユーザーの場合はadminデータベースを指定する必要があることに注意します。

    ```powershell
    > mongo [ホスト名:ポート番号/DB名] -u [ユーザー名] -p
    > mongo localhost:27017/test -u hoge -p
    > mongo localhost:27017/admin -u admin -p

    > mongo [DB名] -u [ユーザー名] -p
    > mongo test -u hoge -p
    ```

1. データベースの一覧を表示します。

    ```powershell
    > show dbs
    ```

1. データベースを作成します。

    ```powershell
    > use test
    ```

1. データベースを切り替えます。

    ```powershell
    > use test
    ```

1. データベースを削除します。

    ```powershell
    > use test
    > db.dropDatabase();
    ```

1. コレクション（テーブル）の一覧を表示します。

    ```powershell
    > use test
    > show collections
    ```

1. コレクション（テーブル）を作成します。

    ```powershell
    > use test
    > db.createCollection("[コレクション名]");
    ```

1. コレクション（テーブル）を削除します。

    ```powershell
    > use test
    > db.[コレクション名].drop();
    ```

1. ドキュメント（レコード）を追加します。

    ```powershell
    > db.things.save( {"name":"Taro", "age":36} );
    ```

1. ドキュメント（レコード）の一覧を表示します。

    ```powershell
    > db.things.find();
    > db.things.find({name: "Taro"});
    ```

1. ドキュメント（レコード）を更新します。

    ```powershell
    > db.things.update( {"name":"Taro", "age":36}, {"name":"Jiro", "age":18} );
    ```

1. ドキュメント（レコード）内の数値を加算します。

    ```powershell
    > db.things.update( {"name":"Jiro", "age":18}, { $inc : {"age":1} } );
    ```

1. ドキュメント（レコード）を削除します。

    ```powershell
    > db.things.remove( {"name":"Jiro"} );
    ```

1. JavaScriptを利用することもできます。

    ```powershell
    > for (var i = 0; i < 1000000; i++) {
        db.things.save( {"name":"Xxxx", "age":i} );
      }
    ```

1. インデックスを設定します。

    ```powershell
    > db.bench.ensureIndex({age:1}, {unique: true});
    ```

## mongo-express の利用（参考）

MongoDBをGUI（Webブラウザ）で管理するためのmongo-expressを利用してみます。

> Node.js および Express で作成された Web ベースの MongoDB 管理インターフェイス  
> 最近は MongoDB Compass を利用するほうが一般的です。

1. mongo-expressをインストールします。

    ```powershell
    > npm install -g mongo-express
    ```

1. mongo-expressのディレクトリに移動します。

    ```powershell
    > cd D:\Develop\Bin\Nodist\bin\node_modules\mongo-express
    ```

1. config.default.js をコピーして config.js を作成します。

    ```powershell
    > cp config.default.js config.js
    > code config.js
    ```

1. config.js を編集します。

    ```  javascript
    // 必要に応じてラベルを修正します。
    var dbLabel = 'mongodb-2.4';
    ↓
    var dbLabel = 'mongodb-4.2';

    // adminでの利用を強制します。
    admin: process.env.ME_CONFIG_MONGODB_ENABLE_ADMIN ? process.env.ME_CONFIG_MONGODB_ENABLE_ADMIN.toLowerCase() === 'true' : false,
    ↓
    admin: true,

    // adminでの利用のため空指定とします。（ユーザーとDBを固定とした場合はそのDBのみ利用可能となります。）
    auth: [
      {
        database: process.env.ME_CONFIG_MONGODB_AUTH_DATABASE || mongo.db,
        username: getFileEnv(dbAuthUsername) || mongo.username,
        password: getFileEnv(dbAuthPassword) || mongo.password,
      },
    ],
    ↓
    auth: [],

    // adminのユーザー名とパスワードを指定します。
    adminUsername: getFileEnv(adminUsername) || '',
    adminPassword: getFileEnv(adminPassword) || '',
    ↓
    adminUsername: getFileEnv(adminUsername) || 'admin',
    adminPassword: getFileEnv(adminPassword) || 'password',

    // BASIC認証のユーザー名とパスワードを指定します。
    basicAuth: {
      username: getFileEnv(basicAuthUsername) || 'admin',
      password: getFileEnv(basicAuthPassword) || 'pass',
    },
    ↓
    basicAuth: {
      username: getFileEnv(basicAuthUsername) || 'hoge',
      password: getFileEnv(basicAuthPassword) || 'password',
    },
    ```

1. mongo-expressを起動します。

    ```powershell
    > node app.js
    ```

1. ブラウザを起動し、http://localhost:8081 にアクセスするとページが表示されます。

    > config.jsで指定したBASIC認証のユーザー名とパスワードを用いてアクセスします。  
    > ポート番号はconfig.jsで指定したものです。

1. Ctrl + C で停止します。

## mongo-express のデーモン化（参考）

mongo-expressをデーモン化して実行してみます。

> mongo-express などの Node.js のアプリケーションをデーモン化するには、forever を使用します。

1. foreverをインストールします。

    ```powershell
    > npm install -g forever
    ```

1. mongo-expressを起動する際に指定していたnodeをforeverに変更することでデーモン化できます。

    ```powershell
    > forever start app.js
    ```

1. 実行中のデーモンプロセスを確認します。

    ```powershell
    > forever list
    ```

1. 実行中のデーモンプロセスを停止します。

    ```powershell
    > forever stop [uid（４桁の英数字）]
    ```

## MongoDB Compass の利用

MongoDB の GUI CRUD ツールを利用してみます。

> MongoDB の GUI として MongoDB Compass を使用すると、ドキュメントの構造、クエリ、インデックス作成、ドキュメントの検証などについてより賢明な決定を下すことができます。

1. MongoDB Compass を起動します。

1. New Connection にて接続情報を入力し、CONNECT をクリックします。

    ```text
    書式
    mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]
    admin接続例
    mongodb://admin:password@localhost:27017/admin
    ```

1. 接続されると、MongoDB のデータベースが表示されます。

1. 画面下部の + をクリックすることでデータベースとコレクションを作成できます。

1. データベースを選択すると、MongoDB のコレクションが表示されます。

1. コレクションには、JSON ファイルや CSV ファイルを利用した取り込みが可能です。

## Python + PyMongo を利用した MongoDB 利用サンプル

Python と PyMongo を利用したサンプルプログラムを作成します。

### プロジェクトディレクトリの作成

```powershell
> mkdir mongo_sample
> cd mongo_sample
```

### Python 仮想環境の構築

virtualenv で仮想環境を構築して有効化します。

```powershell
> virtualenv venv
> activate
```

### PyMongo と pprint のインストール

```powershell
(venv) > pip install pymongo
(venv) > pip install pprint
```

### プログラムの作成

main.py ファイルを作成します。

```powershell
(venv) > ni main.py
```

MongoDB 利用サンプルプログラムを拵えます。

```python
from pymongo import MongoClient
from bson.objectid import ObjectId
import pprint
import os

# データベースへの接続
client = MongoClient("mongodb://localhost:27017/")
# データベースの取得
db = client.test
# 認証
db.authenticate(name="hoge",password="password")

# コレクションの取得
users = db.users

# コレクションへ単一ドキュメントを挿入
print("コレクションへ単一ドキュメントを挿入")
new_user = {"name":"Taro", "age":36}
user_id = users.insert_one(new_user).inserted_id

# コレクションから単一ドキュメントを取得（条件なし）
print("コレクションから単一ドキュメントを取得（条件なし）")
user = users.find_one()
pprint.pprint(user)

# コレクションから単一ドキュメントを取得（条件指定：名前=Taro）
print("コレクションから単一ドキュメントを取得（条件指定：名前=Taro）")
user = users.find_one({"name": "Taro"})
pprint.pprint(user)

# コレクションから単一ドキュメントを取得（条件指定：_id）
print("コレクションから単一ドキュメントを取得（条件指定：_id）")
user = users.find_one({"_id": user_id})
pprint.pprint(user)

# コレクションから単一ドキュメントを取得（条件指定：文字列のIDを_idに変換）
# Webアプリのクエリパラメータから取得したIDを使用する場合などは変換が必要
print("コレクションから単一ドキュメントを取得（条件指定：文字列のIDを_idに変換）")
string_id = str(user_id)
user = users.find_one({"_id": ObjectId(string_id)})
pprint.pprint(user)

# コレクションへ複数ドキュメントを挿入（5件）
print("コレクションへ複数ドキュメントを挿入（5件）")
new_users = [{"name":"Hoge", "age":19},
             {"name":"Fuga", "age":32},
             {"name":"Moge", "age":27},
             {"name":"Page", "age":38},
             {"name":"Nazo", "age":35}]
result = users.insert_many(new_users)
pprint.pprint(result.inserted_ids)

# コレクションから複数ドキュメントを取得（全件）
print("コレクションから複数ドキュメントを取得（全件）")
for user in users.find():
    pprint.pprint(user)

# コレクションの単一ドキュメントを更新（条件指定：_id、年齢を18に更新）
print("コレクションの単一ドキュメントを更新（条件指定：_id、年齢を18に更新）")
users.update_one({"_id": user_id}, {"$set": {"age":18}})
for user in users.find():
    pprint.pprint(user)

# コレクションの複数ドキュメントを更新（条件指定：年齢<20、年齢に3を加算）
print("コレクションの複数ドキュメントを更新（条件指定：年齢<20、年齢に3を加算）")
users.update_many({'age': {"$lt": 20}}, {'$inc': {'age': 3}})
for user in users.find():
    pprint.pprint(user)

# コレクションから単一ドキュメントを削除（条件指定：_id）
print("コレクションから単一ドキュメントを削除（条件指定：_id）")
users.delete_one({"_id": user_id})
for user in users.find():
    pprint.pprint(user)

# コレクションから複数ドキュメントを削除（条件指定：年齢>=30）
print("コレクションから複数ドキュメントを削除（条件指定：年齢>=30）")
users.delete_many({'age': {"$gte": 30}})
for user in users.find():
    pprint.pprint(user)

# コレクションから複数ドキュメントを取得（条件指定：名前=Moge）
print("コレクションから複数ドキュメントを取得（条件指定：名前=Moge）")
for user in users.find({"name": "Moge"}):
    pprint.pprint(user)

# コレクションから複数ドキュメントを取得（条件指定：年齢<30）
print("コレクションから複数ドキュメントを取得（条件指定：年齢<30）")
for user in users.find({"age": {"$lt": 30}}).sort("age"):
    pprint.pprint(user)

# コレクションのドキュメント件数を取得
print("コレクションのドキュメント件数を取得")
count = users.count_documents({})
print(count)
```

### プログラムの実行

main.py ファイルを実行します。

```powershell
(venv) > python main.py
```

実行結果

```powershell
コレクションへ単一ドキュメントを挿入
コレクションから単一ドキュメントを取得（条件なし）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 36, 'name': 'Taro'}
コレクションから単一ドキュメントを取得（条件指定：名前=Taro）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 36, 'name': 'Taro'}
コレクションから単一ドキュメントを取得（条件指定：_id）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 36, 'name': 'Taro'}
コレクションから単一ドキュメントを取得（条件指定：文字列のIDを_idに変換）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 36, 'name': 'Taro'}
コレクションへ複数ドキュメントを挿入（5件）
[ObjectId('5e5740378d00d876059ce6f3'),
 ObjectId('5e5740378d00d876059ce6f4'),
 ObjectId('5e5740378d00d876059ce6f5'),
 ObjectId('5e5740378d00d876059ce6f6'),
 ObjectId('5e5740378d00d876059ce6f7')]
コレクションから複数ドキュメントを取得（全件）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 36, 'name': 'Taro'}
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 19, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f4'), 'age': 32, 'name': 'Fuga'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
{'_id': ObjectId('5e5740378d00d876059ce6f6'), 'age': 38, 'name': 'Page'}
{'_id': ObjectId('5e5740378d00d876059ce6f7'), 'age': 35, 'name': 'Nazo'}
コレクションの単一ドキュメントを更新（条件指定：_id、年齢を18に更新）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 18, 'name': 'Taro'}
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 19, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f4'), 'age': 32, 'name': 'Fuga'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
{'_id': ObjectId('5e5740378d00d876059ce6f6'), 'age': 38, 'name': 'Page'}
{'_id': ObjectId('5e5740378d00d876059ce6f7'), 'age': 35, 'name': 'Nazo'}
コレクションの複数ドキュメントを更新（条件指定：年齢<20、年齢に3を加算）
{'_id': ObjectId('5e5740368d00d876059ce6f2'), 'age': 21, 'name': 'Taro'}
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 22, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f4'), 'age': 32, 'name': 'Fuga'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
{'_id': ObjectId('5e5740378d00d876059ce6f6'), 'age': 38, 'name': 'Page'}
{'_id': ObjectId('5e5740378d00d876059ce6f7'), 'age': 35, 'name': 'Nazo'}
コレクションから単一ドキュメントを削除（条件指定：_id）
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 22, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f4'), 'age': 32, 'name': 'Fuga'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
{'_id': ObjectId('5e5740378d00d876059ce6f6'), 'age': 38, 'name': 'Page'}
{'_id': ObjectId('5e5740378d00d876059ce6f7'), 'age': 35, 'name': 'Nazo'}
コレクションから複数ドキュメントを削除（条件指定：年齢>=30）
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 22, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
コレクションから複数ドキュメントを取得（条件指定：名前=Moge）
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
コレクションから複数ドキュメントを取得（条件指定：年齢<30）
{'_id': ObjectId('5e5740378d00d876059ce6f3'), 'age': 22, 'name': 'Hoge'}
{'_id': ObjectId('5e5740378d00d876059ce6f5'), 'age': 27, 'name': 'Moge'}
コレクションのドキュメント件数を取得
2
```

## おわりに

久しぶりの MongoDB でしたが、単純な実装であれば問題なくできそうです。
やはりドキュメントをガンガン保存できるのは良いですね。
次は、Azure Cosmos DB を利用してみようと思います。