PythonでMongoDBのODMを使用しよう

# 前書
この記事は[PythonでMongoDBを入門しよう](https://qiita.com/Syoitu/items/db192385a4b2e4884ed5)という記事の続編になりますが、
前回の記事を読んでいなくても内容がわかる構成にしています。
もしMongoDBに興味があれば、最後まで付き合っていただけると幸いです。

# 環境構築

### MongoDB 
この記事ではDockerを使用して環境構築行います。
ローカル内で直接MongoDBを入れたい方は前回の記事を参考にしてください。:point_up_tone1:

```yml:docker-compose.yml
version: '3.1'

services:

  mongo:
    image: mongo
    restart: always
    ports:
      - 27017:27017      
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example

  mongo-express:
    image: mongo-express
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
```
コンテナを立ち上げます。

```shell
docker-compose up -d mongo mongo-express
```
Mongo Expressにアクセス、表示出来たらMongoDBの環境構築は完了です。
<img width="660" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/3923c2d8-9783-de7d-451f-161f3acb64dd.png">

### Python

Python側は `mongoengine`というライブラリを使用します。

```
pip install mongoengine
```
試しに接続します。
デフォルトで入ってる`local`データベースに接続してみます。

```python:test.py
from mongoengine import connect

connect(db='local',
        username="root",
        password="example",
        host='192.168.99.100',
        port=27017,
        authentication_mechanism='SCRAM-SHA-1',
        authentication_source='admin'
        )
```
実行し、エラーが出なければ成功です。
その他の接続方法はこちらを参考にしてください:point_right_tone1:[PythonでMongoDBを入門しよう](https://qiita.com/Syoitu/items/db192385a4b2e4884ed5)

# MongoDBのODM

ODMとは何か、文字通りで言うとオブジェクトドキュメントマッピングになります。
MongoDBのようなドキュメントデータベースにオブジェクトをマッピングします。
使用するメリットとしてはデータの構造を縛れることです。

### 実例を見ていきます

```python:test.py

from mongoengine import connect, Document, EmbeddedDocument, \
    StringField, IntField, DateTimeField, ListField, EmbeddedDocumentField
from datetime import datetime

connect(db='company',
        username="root",
        password="example",
        host='192.168.99.100',
        port=27017,
        authentication_mechanism='SCRAM-SHA-1',
        authentication_source='admin'
        )


class Employee(EmbeddedDocument):
    """
        社員詳細
    """
    name = StringField(required=True)
    age = IntField(required=False)


SCALE_CHOICES = (
    ("venture", "ベンチャー"),
    ("major", "大手")
)


class Company(Document):
    """
        会社モデル
    """
    name = StringField(required=True, max_length=32)
    scale = StringField(required=True, choices=SCALE_CHOICES)
    created_at = DateTimeField(default=datetime.now())
    members = ListField(EmbeddedDocumentField(Employee))
```
`Django`や`sqlalchemy` 使用してモデルを定義したことがあれば、馴染む構成になっています。
実際に定義されたモデルを使用してデータをMongoDBに入れてみます。
下記のコードを`test.py`に追記し、実行します。
:point_up_tone1:テスト用のデータベースをあらかじめ作る必要はありません。

```test.py

class TestMongoEngine:
    def add_one(self):
       c_obj = Company(
           name="有名ベンチャー",
           scale="venture",
       )
       c_obj.save()
       return c_obj


if __name__ == "__main__":
        t = TestMongoEngine()
        t.add_one()
```

Mongo Expressから、データが入れられたことが確認できます。

<img width="628" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/82fda68e-f3e6-f53c-c2ad-e84db2588599.png">

`EmbeddedDocumentField`はモデルの内部に更に構造を定義したい場合に、使用すると役に立つので使ってみます。

先ほど`test.py`に追記されたコードを以下のように修正し、再度実行します。

```python:test.py

class TestMongoEngine:
    def add_one(self):
       member1 = Employee(
               name="memberA",
               age=40,
       )
       member2 = Employee(
               name="memberB",
               age=35,
       )
       c_obj = Company(
           name="有名ベンチャーA",
           scale="venture",
           members=[member1, member2]
       )
       c_obj.save()
       return c_obj


if __name__ == "__main__":
        t = TestMongoEngine()
        t.add_one()
```
Mongo Expressから結果を確認します。
<img width="581" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/bce2b85d-cb81-4f64-038a-e821c8f7a38a.png">

# ODM使用してCRUD
C(Create)新規追加する例は最初の例で紹介したので、データの読み込みから紹介していきます。
## R(Retrieve)データの読み込み
#### 単一読み込み
先程新規追加したデータベースから一つのデータを読み込みます。

```python:test.py
...
class TestMongoEngine: 
    def get_one(self):
        return Company.objects.first()

if __name__ == "__main__":
        t = TestMongoEngine()
        rest = t.get_one()
        print(rest.id)
        print(rest.name)
```
結果

```shell

5e7ed47419d1a75baa2bc3f3
有名ベンチャー
```
#### 全部読み込み

```python:test.py
...
class TestMongoEngine:
    def get_more(self):
        return Company.objects.all()

if __name__ == "__main__":
        t = TestMongoEngine()
        rest = t.get_more()
        print(rest)
```
結果

```shell
[<Company: Company object>, <Company: Company object>, <Company: Company object>]
```
#### id検索で読み込み

```python:test.py
...
class TestMongoEngine:
      def get_from_oid(self, oid):
          return Company.objects.filter(pk=oid).first()

if __name__ == "__main__":
        t = TestMongoEngine()
        rest = t.get_from_oid("5e7ed47419d1a75baa2bc3f3")
        print(rest.id)
        print(rest.name)
```
結果

```shell
5e7ed47419d1a75baa2bc3f3
有名ベンチャー
```
### 補足
データ読み込みする際に順番を並び替えたい場合、 `meta`を会社モデルに追記します。

```python:test.py
class Company(Document):
    """
        会社モデル
    """
    name = StringField(required=True, max_length=32)
    scale = StringField(required=True, choices=SCALE_CHOICES)
    created_at = DateTimeField(default=datetime.now())
    members = ListField(EmbeddedDocumentField(Employee))
    meta = {
        'ordering': ['-created_at']  # metaを追記
    }
```
[ドキュメント](http://docs.mongoengine.org/guide/defining-documents.html#ordering)

## データの更新(Update)
### 単一のデータ修正

```python:test.py
...
class TestMongoEngine:
    def update(self):
        rest = Company.objects.filter(name="有名ベンチャー").update_one(name="普通のベンチャー")
        return rest

if __name__ == "__main__":
        t = TestMongoEngine()
        rests = t.update()
```
実行後、Mongo Expressからデータ変更されたことが確認できます。
<img width="610" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/55cb7850-ad48-a406-6a3d-dc66d4b1c791.png">

### 複数のデータ修正
MongoDBに`name`が`有名ベンチャーA`になってる記録が二つ存在します、それらを修正していきます。
<img width="579" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/f54d84b6-7fc5-fcc7-dd51-f8dca1bc5f13.png">


```python:test.py
class TestMongoEngine:
    def update(self):
        rest = Company.objects.filter(name="有名ベンチャーA").update(name="有名ベンチャーB")
        return rest

if __name__ == "__main__":
        t = TestMongoEngine()
        rests = t.update()
        print(rests)
```
実行結果

```shell
2
```
Mongo Expressからもデータ修正されたことが確認できます。
<img width="574" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/26b9d276-b5d6-0556-7a5f-ac59f802b665.png">

## データの削除(Delete)

### 単一のデータ削除

```python:test.py
class TestMongoEngine:
    def delete(self):
        rest = Company.objects.filter(name="普通のベンチャー").first().delete()
        return rest

if __name__ == "__main__":
        t = TestMongoEngine()
        rests = t.delete()
        print(rests)
```
実行後

<img width="607" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/25b23b09-03c6-3acb-fbce-0efe14383f55.png">

### 複数のデータ削除

`name`が`有名ベンチャーB`になってる二件のデータを削除します。

```python:test.py
...
class TestMongoEngine:
    def delete(self):
        rest = Company.objects.filter(name="有名ベンチャーB").delete()
        return rest

if __name__ == "__main__":
        t = TestMongoEngine()
        rests = t.delete()
        print(rests)
```
実行結果

```shell
2
```
Mongo Expressから確認すると、CompanyDBに入ってるデータすべてが削除されました。

# 後書
[MongoEngineのドキュメント](http://docs.mongoengine.org/index.html)
今度時間あればFlaskとMongoDB使用してWebサービス作る記事を書きたいと思います。