この記事では、非リレーショナルデータベースである MongoDB を Python Web Framework である Django で使用する方法を説明します。
Django は、リレーショナルデータベースである PostgreSQL や MariaDB、MySQL と一緒に使うのが一般的ですが、その理由は ORM が搭載されているからです。
MongoDBは非常に柔軟で、プロトタイピングを容易にするために、Flaskのような軽量なフレームワークとよく組み合わされます。
しかし、スケーラビリティ、動的構造、クエリのサポートにより、大規模なプロジェクトで使用されることも多くなっています。
Django MongoDB Engine は、スキーマを宣言的に定義するために使用されます。
注意: 執筆の時点では、このエンジンは Python 3.x をサポートしていません。
最新のサポートバージョンは Python 2.7 です。
非リレーショナルデータベースとリレーショナルデータベースの比較
このエンジンが他の一般的なエンジンと大きく異なる点は、Django アプリケーションがより一般的にリレーショナルデータベースで開発されるのに対し、ノンリレーショナルデータベースで動作するという点です。
この二つのアプローチのどちらを選ぶかは、あなたが取り組んでいるプロジェクトに帰結します。
なぜなら、それぞれのタイプには状況に応じて一定の長所と短所があるからです。
非リレーショナルデータベースは通常、より柔軟であり(長所でもあり短所でもある)、リレーショナルデータベースはより適合的です(これも長所でもあり短所でもある)。
また、多くのデータを保持するスケーラブルなシステムには、通常、非リレーショナル・データベースが適している。
しかし、小規模から中規模のシステムでは、リレーショナル・データベースの保守のしやすさが優先されることがよくある。
リレーショナルデータベース
リレーショナルデータベースは、列と行で構成されるテーブルにデータを格納する。
- 行はエンティティ (例:
Movie
) を表す。 - 列は実体の属性 (例:
映画名
、長さ
、公開年
など) を表す。 - 行は、データベース内の1つのエントリを表します (例:
{"The Matrix", 2h 16min, 1999.}
)。
すべてのテーブルの行は、その行だけを表すユニークなキー(ID)を持っていなければなりません。
リレーショナルデータベースの代表的なものは以下の通りである。
Oracle、PostgreSQL、MySQL、MariaDBなど。
インストールとセットアップ
Django MongoDB Engine をプロジェクトに実装するために、3つのものをインストールしたいと思います。
- Django-nonrel – 非リレーショナルデータベースのサポート (これは Django 1.5 もインストールしてくれ、以前にインストールしたバージョンはアンインストールします)。
- djangotoolbox – 非リレーショナルな Django アプリケーションのためのツール。
- Django MongoDB Engine – エンジンそのものです。
これらを pip
経由で、Django 自体と一緒にインストールしましょう。
$ pip install django
$ pip install git+https://github.com/django-nonrel/django@nonrel-1.5
$ pip install git+https://github.com/django-nonrel/djangotoolbox
$ pip install git+https://github.com/django-nonrel/mongodb-engine
まずは Django プロジェクトをコマンドラインから初期化してみましょう。
$ django-admin.py startproject djangomongodbengine
さて、いくつかの基本的なファイルを含むスケルトンプロジェクトができたので、 Django にどのエンジンを使いたいかを知らせたいと思います。
そのために、 settings.py
ファイルを更新し、より具体的には DATABASES
プロパティを更新します。
DATABASES = {
'default' : {
'ENGINE' : 'django_mongodb_engine',
'NAME' : 'example_database'
}
}
インストールとセットアップが終わったので、 Django MongoDB Engine でできることをいくつか見てみましょう。
モデル・フィールド
モデルを扱う場合、標準的な MVC (Model-View-Controller) アーキテクチャでは、古典的なアプローチは django.db.models
モジュールを使用することです。
Modelクラスには
CharFieldや
TextField` などがあり、基本的にモデルのスキーマを定義し、Django の ORM によってデータベースにマッピングされる方法を定義することができます。
では、 models.py
に Movie
というモデルを追加してみましょう。
from django.db import models
class Movie(models.Model)
name = models.CharField()
length = models.IntegerField()
ここでは、 Movie
モデルがあり、 name
と length
という2つのフィールドを持っています。
それぞれ、データベースのカラムを表す Field
の実装で、データ型は指定したものになります。
かなりの数のフィールドタイプがありますが、 models
モジュールは複数の値を持つフィールドをあまりサポートしていません。
これは、 models
モジュールが主にリレーショナルデータベースで使用されることを想定しているためです。
オブジェクトが複数の値を持つフィールドを持つ場合、例えば Movie
が多くの Actor
を持つ場合、他のテーブルと一対多のリレーションを持つことになります。
MongoDBでは、他のテーブルやドキュメントを参照することなく、そのドキュメント内のリストとして保存することができます。
ここで、 ListField
や DictField
などのフィールドがないことを実感します。
ListField
ListFieldはリスト型属性で、複数の値を保持することができる属性です。
これはdjangotoolbox.fields` モジュールに属しており、リストのような値を含むフィールドを指定し、BSONドキュメントに保存するために使用されます。
では、先ほどの Movie
のモデルをいじってみましょう。
from django.db import models
from djangotoolbox.fields import ListField
class Movie(models.Model):
name = models.CharField()
length = models.IntegerField()
year = models.IntegerField()
actors = ListField()
idフィールドを指定していないことに注意してください。
MongoDB は暗黙のうちにModelのインスタンスにそれを代入してくれるからです。
さらに、actorsフィールドを追加しました。
これはListField` です。
これで、Movie
のインスタンスを作成するときに actors
フィールドにリストを代入して、そのまま MongoDB のデータベースに保存できます。
Actor
インスタンスを格納するテーブルを別に作成して Movie
ドキュメントでそれを参照しなくてもいいのです。
movie = Movie.objects.create(
name = "The Matrix",
length = 136,
year = 1999,
actors = ["Keanu Reeves", "Laurence Fishburne"]
)
このコードを実行すると、MongoDB ドキュメントが作成されます。
{
"_id" : ObjectId("..."),
"name" : "The Matrix",
"length" : 136,
"year" : 1999,
"actors" : [
"Keanu Reeves",
"Laurence Fishburne"
]
}
ListFieldを
extend()` して、さらに値を追加することもできます。
movie.actors.extend(['Carrie-Ann Moss'])
この結果、BSON ドキュメントが更新されます。
{
"_id" : ObjectId("..."),
"name" : "The Matrix",
"length" : 136,
"year" : 1999,
"actors" : [
"Keanu Reeves",
"Laurence Fishburne",
"Carrie-Ann Moss",
"Carrie-Ann Moss"
]
}
SetField
SetFieldは
ListField` と同じですが、Pythonのセットとして解釈されるので、重複は許されません。
もし、同じアクターを2回追加すると
movie.actors.extend(['Carrie-Ann Moss'])
出力が少し変なことにすぐに気がつきます。
{
"_id" : ObjectId("..."),
"name" : "The Matrix",
"length" : 136,
"year" : 1999,
"actors" : [
"Keanu Reeves",
"Laurence Fishburne",
"Carrie-Ann Moss"
]
}
重複したエントリを避けたいので、各個人が実際の個人であることが望ましいのですが、 actors
を ListField
ではなく SetField
にするのがより理にかなっています。
from django.db import models
from djangotoolbox.fields import ListField
class Movie(models.Model):
name = models.CharField()
length = models.IntegerField()
year = models.IntegerField()
actors = SetField()
これで、複数のアクターを追加することができ、その中には重複しているものもありますが、ユニークなものだけが追加されることになります。
movie = Movie.objects.create(
name = "John Wick",
length = 102,
year = 2014,
actors = ["Keanu Reeves", "Keanu Reeves", "Bridget Moynahan"]
)
しかし、出来上がったドキュメントには "Keanu Reeves"
という、たったひとつの項目だけが残ります。
{
"_id" : ObjectId("..."),
"name" : "John Wick",
"length" : 102,
"year" : 2014,
"actors" : [
"Keanu Reeves",
"Bridget Moynahan"
]
}
DictField
DictField` は、Python辞書をBSONドキュメントとして、ドキュメント内に保存します。
これは、辞書がどのようなものであるかわからない場合、また、辞書の構造があらかじめ定義されていない場合に、望ましい方法です。
一方、構造がよく分かっている場合は、モデルの中のモデルとして、埋め込みモデルを使用することが推奨されます。
例えば、Actor
はそれ自身のモデルであり、Movie
モデルには複数のActor
モデルが埋め込まれるようにすることができます。
一方、可変長の値を追加する場合は、Key-Value 要素としてマッピングし、 DictField
として保存することができます。
例えば、 0..n
件のレビューを持つことができる reviews
フィールドを追加してみましょう。
レビューは予測可能な構造 (
name,
grade,
comment) を持っていますが、
actorsと
reviewsのために別々の
Modelを作る前に、それらを
DictField` として実装することにしましょう。
from django.db import models
from djangotoolbox.fields import SetField
from djangotoolbox.fields import DictField
class Movie(models.Model):
name = models.CharField()
length = models.IntegerField()
year = models.IntegerField()
actors = SetField()
reviews = DictField()
これで、ムービーを作成するときに、レビュアーの辞書と、そのムービーのレビューを追加することができるようになりました。
movie = Movie.objects.create(
name = "Good Will Hunting",
length = 126,
year = 1997,
actors = ["Matt Damon", "Stellan Skarsgard"],
reviews = [
{"Portland Oregonian" : "With its sweet soul and sharp mind..."},
{"Newsweek" : "Gus Van Sant, working from the tangy, well-written script..."}
]
)
このコードを実行すると、次のような結果が得られます。
{
"_id" : ObjectId("..."),
"name" : "Good Will Hunting",
"length" : 126,
"year" : 1997,
"actors" : [
"Matt Damon",
"Stellan Skarsgard"
],
"reviews" : [
{"Portland Oregonian" : "With its sweet soul and sharp mind..."},
{"Newsweek": "Gus Van Sant, working from the tangy, well-written script..."}
]
}
エンベデッドモデル
さて、レビュー
フィールドは、間違いなく、名前
の後にコメント
が続くという、同じような構造になるでしょう。
俳優は名前だけでなく、
姓や
生年月日` などの特徴も持っています。
この2つについては、リレーショナルデータベースと同じように、独立したモデルを作成することができます。
しかしリレーショナルデータベースの場合、それぞれのテーブルに保存して、 Movie
テーブルからリンクすることになります。
しかし MongoDB では、それらを埋め込みモデル (ドキュメント全体を別のドキュメントに埋め込んだもの) にすることができます。
もう一度 Movie
を変更してみましょう。
from django.db import models
from djangotoolbox.fields import ListField, EmbeddedModelField
class Movie(models.Model):
name = models.CharField(max_length=100)
length = models.IntegerField()
year = models.IntegerField()
actors = SetField(EmbeddedModelField("Actor"))
reviews = SetField(EmbeddedModelField("Review"))
ここでは、actors
と reviews
の両方に SetField
(これは ListField
のようなものでもよい) を作っています。
しかし、今回は SetField
のコンストラクタに EmbeddedModelField
を渡すことで、他のモデルの SetField
にしています。
また、EmbeddedModelField
クラスのコンストラクタで、どのモデルであるかを指定しています。
では、この2つも models.py
ファイルで定義してみましょう。
class Actor(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
date_of_birth = models.CharField(max_length=11)
class Review(models.Model):
name = models.CharField(max_length=30)
comment = models.CharField(max_length=300)
これで、Movie
オブジェクトを作成してデータベースに保存するときに、新しい Actor
と Review
インスタンスを追加できるようになりました。
movie = Movie.objects.create(
name = "Focus",
length = 105,
year = 2015,
actors = [
Actor(
first_name="Will",
last_name="Smith",
date_of_birth="25.09.1968."
)
],
reviews = [
Review(
name = "Portland Oregonian",
comment = "With its sweet soul and sharp mind..."
),
Review(
name = "Newsweek",
comment = "Gus Van Sant, working from the tangy, well-written script..."
)
]
)
これは、セット内の Actor
と Review
それぞれに対して新しい BSON ドキュメントを作成し、それらを movie
ドキュメントに埋め込みオブジェクトとして保存します。
{
"_id" : ObjectId("..."),
"name" : "Focus",
"length" : 105,
"year" : 2015,
"actors" : [
{
"name" : "Will",
"last_name" : "Smith",
"date_of_birth" : "25.09.1968"
}
],
"reviews" : [
{
"name" : "Portland Oregonian",
"comment" : "With its sweet soul and sharp mind..."
},
{
"name" : "Newsweek",
"comment" : "Gus Van Sant, working from the tangy, well-written script..."
}
]
}
BSON配列の reviews
の各エントリは、個々の Review
のインスタンスです。
Actors` も同様です。
ファイル操作
MongoDB には GridFS と呼ばれるファイルシステムにファイルを保存/検索する仕様が組み込まれており、Django MongoDB Engine でもそれを使っています。
注意: MongoDB はファイルを 255kB の大きさに分割して保存します。
ファイルがアクセスされると、GridFS はその断片を集めてマージします。
GridFSのシステムをインポートするには、django_mongodb_engine_storage
モジュールにアクセスします。
from django_mongodb_engine.storage import GridFSStorage
gridfs = GridFSStorage()
uploads_location = GridFSStorage(location = '/uploaded_files')
また、GridFSField()
というフィールドを利用することで、GridFSシステムを利用してデータを格納するフィールドを指定することができます。
class Movie(models.Model):
name = models.CharField()
length = models.IntegerField()
year = models.IntegerField()
actors = SetField(EmbeddedModelField("Actor"))
reviews = SetField(EmbeddedModelField("Review"))
poster = GridFSField()
これで、この画像はチャンクに保存され、必要に応じてレイジーローディングされるようになります。
結論
まとめると、Django MongoDB Engine はかなり強力なエンジンで、これを使う主な欠点は、古いバージョンの Django (1.5) と Python (2.7) で動作することですが、Django は現在 3.2 LTS で 1.5 のサポートはかなり前に終了しています。
Pythonは3.9で、2.7のサポートは昨年終了しています。
それに加えて、Django MongoDB Engineは2015年にさらなる開発を停止したようです。