Django-rest-frameworkのDBにMongoDBを設定する方法

# はじめに
Django-rest-framework(DRF)とMongoDBの連携で検索すると英語の記事が色々と出てはくるのですがベストな方法が分からず、色々と試した結果、個人的にはDjongoを用いるのが良いという結論になったので自分なりにまとめてみました。

この記事では主に、

- DRF + Djongo + MongoDBの連携
- Djongoを利用したModel定義

の二点ついて簡単に紹介してきたいと思います。

Django/DRF初心者の方、これからDRFとMongoDBの連携を考える方の参考になれば幸いです。

# 環境
python 3.7.3
macOS Mojave 10.14.6
# 利用するフレームワーク/ライブラリ
Django                             2.1.7
djangorestframework                3.9.2
djongo                             1.2.33

手順に進む前にこれらをpip installする必要があります。

pythonからMongoDBにアクセスするためのライブラリにはPyMongo[^1]やmongoengine[^2]などがありますが、今回はDjangoとの連携を簡単に行えるDjongoを採用しています。

公式サイト↓
https://nesdis.github.io/djongo/
公式のstartガイドが参考になります↓
https://nesdis.github.io/djongo/integrating-django-with-mongodb/

# 手順
それでは早速手順に入ります。

まず下記を参考に雛形となるサンプルのREST APIを実装します。
（いきなり丸投げです、すいませんw 全てコピペでOKです。）
https://www.django-rest-framework.org/tutorial/quickstart/

完成すると次のような構造になるはずです。

```
tutorial
├── db.sqlite3
├── manage.py
└── tutorial
    ├── __init__.py
    ├── quickstart
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── serializers.py
    │   ├── tests.py
    │   └── views.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
```

この時点ではSQLiteがバックエンドDBとして動いています。
ここからMongoDBを利用するように設定を変更していきます。手順は大きく二つです。

+ settings.pyの変更
+ modelの修正

## settings.pyの変更
まずsettings.pyでDATABASESの部分にdjongoを指定します。

```tutorial/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'djongo',
        'NAME': 'sample',
    }
}
```
また、INSTALLED_APPSに作成したアプリを登録します。

```tutorial/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'tutorial.quickstart', # これを追加
]
```

## modelの修正
次にmodel定義を修正します。
今回はdjango-rest-frameworkのsampleと違い、自作のモデルAuthor, Bookを作成してみます。

ここで重要なのは、djangoのmodelではなく、djongoのmodels.Modelをimportして継承させるところです。djongoのmodelはdjangoのデフォルトのmodelと基本的に同じインターフェースになっているので、同じ構文でスキーマ定義ができ、インスタンス化したオブジェクトを介してMongoDBに書き込むことも可能です。[^3]
また、modelのfieldには[Djangoのfield](https://docs.djangoproject.com/ja/2.1/ref/models/fields/)だけでなく、[Djongoで独自に定義されるfield](https://nesdis.github.io/djongo/api/djongo.models/)が利用できます。


```tutorial/quickstart/models.py
from djongo import models # djongoのモデルを利用する

class Author(models.Model):
    name = models.TextField()
    age = models.IntegerField(blank=True)
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.TextField()
    description = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
```

modelが修正できたので、serializers.py, views.py, urls.pyを今定義したモデルに対応するよう書き換えていきます。

```tutorial/quickstart/seliarizers.py
from tutorial.quickstart.models import Author, Book
from rest_framework import serializers

class AuthorSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Author
        fields = ['name', 'age']

class BookSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Book
        fields = ['title', 'description', 'author']

```

```tutorial/quickstart/views.py
from tutorial.quickstart.models import Author, Book
from rest_framework import viewsets
from tutorial.quickstart.serializers import AuthorSerializer, BookSerializer

class AuthorViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer

class BookViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer

```

(余談ですが、views.pyではdjango-rest-frameworkのviewsetの機能によりたったこれだけのコードでCRUDのAPIが全て実装できてしまいます。viewsetとても便利ですね。)

```tutorial/urls.py
from django.urls import include, path
from rest_framework import routers
from tutorial.quickstart import views

router = routers.DefaultRouter()
router.register(r'authors', views.AuthorViewSet)
router.register(r'books', views.BookViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
```

これで実装は完了です。
正しく動作するか確かめるため起動してみます。

## migration & サーバー起動

MongoDBを起動します。

```
sudo mongod
```


続いて、djangoのコマンドを利用してmigrationとサーバーを起動を行います。

```
$python manage.py makemigrations quickstart
$python manage.py migrate
$python manage.py runserver
```
成功すればlocalhost:8000でリッスンするはずです。

ブラウザーのurlにlocalhost:8000/authors/と入れてみます。
![get_author.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553908/cae59c34-045b-c1d2-41f7-43f942026961.png)
画面が表示されました！

この画面からauthorの参照、登録ができます。

- 画面下部の入力欄を埋めてPOSTボタンを押すと、authorが追加され画面上部にjson形式で表示されます。
- 画面上部のGETボタンを押すと登録されたauthorの一覧が見れます。
![post_author.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553908/ea27b366-ee48-9974-8b73-68a15a2364b5.png)


また、localhost:8000/books/にアクセスすれば追加したauthorにひもづくbookが登録できます。
![post_book.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553908/497af318-7f10-657c-a396-52aaf7fec5f6.png)


最後に、mongoDBの中に本当にデータが入っているか確認してみます。
mongoの対話シェルを起動します。

```
mongo
```

対話シェル上で次のようにコマンドを打つことで確認できます。

```
> use sample
switched to db sample
>
> db.quickstart_author.find({})
{ "_id" : ObjectId("5e049aa6b7135ae9f9ce6862"), "id" : 2, "name" : "奈須きのこ", "age" : 46 }
> 
> db.quickstart_book.find({})
{ "_id" : ObjectId("5e049ae7b7135ae9f9ce6865"), "id" : 2, "title" : "空の境界", "description" : "『空の境界』（からのきょうかい）は、奈須きのこによる日本の長編伝奇小説（Wikipediaより）", "author_id" : 2 }
```
UIと同じデータがちゃんとMongoDBにも登録されていますね。

## 補足

mongoの中身を見るとquickstart_author, quickstart_bookのようにcollectionが二つに分かれて登録されているのがわかると思います。これはmodel定義の時、以下のようにforeignkeyでモデルをつないでいるためです。

```
author = models.ForeignKey(Author, on_delete=models.CASCADE)
```

よりMongoDBらしくデータを格納するには、Djongo独自の[EmbeddedModelField](https://nesdis.github.io/djongo/embedded-model-field/)を利用すれば、bookの中にauthorが入れ子になった状態で一つのcollectionに格納できるようになります。ここは要件に応じて使けになるでしょう。詳しくは[こちら](http://masa-w.hatenablog.com/entry/20101130/1291084939)の記事が分かりやすく参考になるかと思います。

# 最後に

以上で手順は全て完了です。ありがとうございました！
少しでもお役に立てると嬉しいです。


[^1]: PythonのMongoDB clientとしてはこれが最もメジャーなようです。ただしこれ単体ではスキーマバリデーションなどはできません。

[^2]: PyMongoの拡張版で、PythonとMongoDBの間のO/R-mapperとして働きます。特にDjangoとの連携を考慮しない場合はこちらが利用可能です。

[^3]: 内部ではSQLをMongoDBのqueryに変換するといった処理が行われているそうです。