Webアプリケーションを構築する場合、ほとんどの場合、データベースからのデータを扱うことになります。好みに応じて様々なデータベースから選択することができます。
このガイドでは、最も人気のある NoSQL データベースの 1 つである MongoDB を Flask マイクロフレームワークと統合する方法を見ていきます。
このガイドでは、人気のライブラリ MongoEngine とそのラッパー Flask-MongoEngine を使って MongoDB と Flask を統合する方法を探ります。
あるいは、Flask-PyMongo を使って MongoDB を統合することもできます。
Flask-MongoEngine
MongoEngine は Python のクラス (モデル) を MongoDB のドキュメントにマッピングする ODM (Object Document Mapper) で、コードから直接ドキュメントを作成したり操作したりすることが簡単にできるようになります。
セットアップとコンフィギュレーション
MongoEngine の機能を調べるために、Movie
インスタンスに対して CRUD 操作を行うシンプルなムービー API を作成します。
まず、Flask をインストールしてください。
$ pip install flask
MongoDB はクラウドインスタンスである MongoDB Atlas を提供しており、無料で使うことができますが、今回はローカルにインストールしたインスタンスを使うことにします。MongoDBの入手とインストールの方法は、公式ドキュメントに記載されています。
また、Flask-MongoEngine ライブラリもインストールします。
$ pip install flask-mongoengine
MongoDBデータベースインスタンスへの接続
Flask と Flask-MongoEngine をインストールしたので、次は Flask アプリと MongoDB インスタンスを接続します。
まず Flask と Flask-MongoEngine をアプリにインポートします。
from flask import Flask
from flask_mongoengine import MongoEngine
次に Flask のアプリオブジェクトを作ります。
app = Flask(__name__)
このオブジェクトを使って MongoEngine
オブジェクトを初期化します。しかし、初期化する前に MongoDB インスタンスへの参照が必要です。
この参照は app.config
のキーで、その値として接続パラメータを含む dict を指定します。
app.config['MONGODB_SETTINGS'] = {
'db':'db_name',
'host':'localhost',
'port':'27017'
}
代わりに接続 URI を指定することもできます。
app.config['MONGODB_SETTINGS'] = {
'host':'mongodb://localhost/db_name'
}
設定が完了したら、次は MongoEngine
オブジェクトを初期化します。
db = MongoEngine(app)
初期化には MongoEngine
オブジェクトの init_app()
メソッドを利用することもできます。
db = MongoEngine()
db.init_app(app)
設定と初期化が終わったら、MongoEngine の素晴らしい機能のいくつかを試してみましょう。
モデルクラスの作成
ODM である MongoEngine は、データベース内のドキュメントを表すのに Python のクラスを使います。
MongoEngine にはいくつかのタイプのドキュメントクラスがあります。
- ドキュメント
- 埋め込みドキュメント
- DynamicDocument (動的文書)
- DynamicEmbeddedDocument (動的埋め込み文書)
ドキュメント
これはデータベース内に独自のコレクションを持つドキュメントを表し、 mongoengine.Document
または MongoEngine
のインスタンス (db.Document
) を継承して作成されます。
class Movie(db.Document):
title = db.StringField(required=True)
year = db.IntField()
rated = db.StringField()
director = db.ReferenceField(Director)
cast = db.EmbeddedDocumentListField(Cast)
poster = db.FileField()
imdb = db.EmbeddedDocumentField(Imdb)
MongoEngine は、ドキュメントのフィールドが取るべきデータの型を記述して検証するためのクラスや、 各フィールドに詳細や制約を追加するためのオプションの修飾子も提供します。
フィールドの例は次のとおりです。
- 文字列の値を表す
StringField()
です。 - int値用の
IntField()
。 - リストを扱う
ListField()
。 -
FloatField()
浮動小数点数の値を表す。
- 他のドキュメントを参照するための
ReferenceField()
。 -
EmbeddedDocumentField()
埋め込み文書などのためのフィールド
- ファイルを格納するための
FileField()
(これについては後で詳しく説明します)
これらのフィールドには、以下のようなモディファイアを適用することもできます。
required
(必須)default
unique
(ユニーク)primary_key
など。
これらのいずれかを True
に設定することで、そのフィールドにのみ適用されるようになります。
EmbeddedDocument
これは、データベースに独自のコレクションを持たず、他のドキュメントに埋め込まれたドキュメントを表し、 EmbeddedDocument
クラスを継承することで作成される。
class Imdb(db.EmbeddedDocument):
imdb_id = db.StringField()
rating = db.DecimalField()
votes = db.IntField()
DynamicDocument
これは、MongoDB の動的な性質を利用してフィールドを動的に追加していくドキュメントです。
他のドキュメントタイプと同様に、 MongoEngine
は DynamicDocument
クラスを提供します。
class Director(db.DynamicDocument):
pass
DynamicEmbeddedDocument
これは DynamicDocument
と EmbeddedDocument
のすべてのプロパティを持つ。
class Cast(db.DynamicEmbeddedDocument):
pass
すべてのデータクラスを作り終えたら、次は MongoEngine の機能を調べてみましょう。
ドキュメントにアクセスする
MongoEngine はデータベースへのクエリーがとても簡単で、次のようにすればデータベース内のすべてのムービーを取得することができます。
from flask import jsonify
@app.route('/movies')
def get_movies():
movies = Movie.objects()
return jsonify(movies), 200
に GET リクエストを送ると、以下のようになります。
に GET リクエストを送ると
これはすべてのムービーを JSON リストとして返します。
localhost:5000/movies/
このようなクエリで大きな結果を扱う場合、結果を切り捨てて、エンドユーザーが必要に応じてゆっくりと読み込めるようにしたいと思うでしょう。
Flask-MongoEngineを使うと、とても簡単に結果をページ分割することができます。
[
{
"_id": {
"$oid": "600eb604b076cdbc347e2b99"
},
"cast": [],
"rated": "5",
"title": "Movie 1",
"year": 1998
},
{
"_id": {
"$oid": "600eb604b076cdbc347e2b9a"
},
"cast": [],
"rated": "4",
"title": "Movie 2",
"year": 1999
}
]
Movie.objects.paginate(page=page, per_page=limit)は
.itemsプロパティにムービーリストを含む
Pagination` オブジェクトを返し、そのプロパティを繰り返しながら、選択したページでムービーを取得します。
@app.route('/movies')
def get_movies():
page = int(request.args.get('page',1))
limit = int(request.args.get('limit',10))
movies = Movie.objects.paginate(page=page, per_page=limit)
return jsonify([movie.to_dict() for movie in movies.items]), 200
1つのドキュメントを取得する
Movie.objects()メソッドにパラメータとして id を渡すことで、
Movie` の結果を 1 つだけ取得することができます。
[
{
"_id": {
"$oid": "600eb604b076cdbc347e2b99"
},
"cast": [],
"rated": "5",
"title": "Back to The Future III",
"year": 1998
},
{
"_id": {
"$oid": "600fb95dcb1ba5529bbc69e8"
},
"cast": [],
"rated": "4",
"title": "Spider man",
"year": 2004
},
...
]
Movie.objects(id=id)
は、 id
がパラメータにマッチするすべてのムービーのセットを返し、 first()
はクエリーセットの最初の Movie
オブジェクトを返します (複数のオブジェクトが存在する場合)。
もし、GET リクエストを
@app.route('/movies/<id')
def get_one_movie(id: str):
movie = Movie.objects(id=id).first()
return jsonify(movie), 200
にGETリクエストを送ると、このような結果になります。
localhost:5000/movies/600eb604b076cdbc347e2b99
ほとんどのユースケースでは、指定した id
にマッチするドキュメントがなければ 404_NOT_FOUND
エラーを発生させたいと思うでしょう。Flask-MongoEngine は first_or_404()
と get_or_404()
というカスタムクエリセットで私たちをカバーしてくれています。
{
"_id": {
"$oid": "600eb604b076cdbc347e2b99"
},
"cast": [],
"rated": "5",
"title": "Back to The Future III",
"year": 1998
}
ドキュメントの作成と保存
MongoEngine では、モデルを使った新しいドキュメントを簡単に作成できます。モデルクラスのインスタンスに対して、以下のように save()
メソッドを呼び出すだけでよいのです。
@app.route('/movies/<id')
def get_one_movie(id: str):
movie = Movie.objects.first_or_404(id=id)
return movie.to_dict(), 200
**body
は body
辞書を名前付きパラメータとして Movie
オブジェクトにアンパックします。例えば、body = {"title": "映画のタイトル", "年": 2015}
,
すると Movie(**body)
は Movie(title="Movie Title", year=2015)
と同じになります。
このリクエストを localhost:5000/movies/
に送ると、以下のようになります。
@app.route('/movies/', methods=["POST"])
def add_movie():
body = request.get_json()
movie = Movie(**body).save()
return jsonify(movie), 201
ドキュメントが保存され、返されます。
$ curl -X POST -H "Content-Type: application/json" \
-d '{"title": "Spider Man 3", "year": 2009, "rated": "5"}' \
localhost:5000/movies/
EmbeddedDocumentsによるドキュメントの作成
埋め込みドキュメントを追加するには、まず埋め込むドキュメントを作成し、それをムービーモデルの適切なフィールドに割り当てる必要があります。
{
"_id": {
"$oid": "60290817f3918e990ba24f14"
},
"cast": [],
"director": {
"$oid": "600fb8138724900858706a56"
},
"rated": "5",
"title": "Spider Man 3",
"year": 2009
}
このようなリクエストを送ると
@app.route('/movies-embed/', methods=["POST"])
def add_movie_embed():
# Created Imdb object
imdb = Imdb(imdb_id="12340mov", rating=4.2, votes=7.9)
body = request.get_json()
# Add object to movie and save
movie = Movie(imdb=imdb, **body).save()
return jsonify(movie), 201
これは、新しく追加されたドキュメントを、埋め込まれたドキュメントとともに返します。
$ curl -X POST -H "Content-Type: application/json"\
-d '{"title": "Batman", "year": 2016, "rated": "yes"}'\
localhost:5000/movies-embed/
動的なドキュメントの作成
A
ドキュメントを更新する
文書を更新するには、データベースから永続文書を取り出してそのフィールドを更新し、メモリ上の変更されたオブジェクトに対して update()
メソッドを呼び出します。
{
"_id": {
"$oid": "601096176cc65fa421dd905d"
},
"cast": [],
"imdb": {
"imdb_id": "12340mov",
"rating": 4.2,
"votes": 7
},
"rated": "yes",
"title": "Batman",
"year": 2016
}
更新リクエストを送信してみましょう。
@app.route('/director/', methods=['POST'])
def add_dir():
body = request.get_json()
director = Director(**body).save()
return jsonify(director), 201
これは更新されたドキュメントの ID を返します。
@app.route('/director/', methods=['POST'])
def add_dir():
body = request.get_json()
director = Director()
director.name = body.get("name")
director.age = body.get("age")
director.save()
return jsonify(director), 201
update()` メソッドを使うと、一度にたくさんの文書を更新することもできます。何らかの条件で更新したい文書をデータベースに問い合わせ、その結果得られた Queryset に対して update メソッドを呼び出すだけです。
@app.route('/director/', methods=['POST'])
def add_dir():
body = request.get_json()
director = Director()
setattr(director, "name", body.get("name"))
setattr(director, "age", body.get("age"))
director.save()
return jsonify(director), 201
では、更新リクエストを送信してみましょう。
$ curl -X POST -H "Content-Type: application/json"\
-d '{"name": "James Cameron", "age": 57}'\
localhost:5000/director/
更新されたドキュメントのIDのリストが返されます。
{
"_id": {
"$oid": "6029111e184c2ceefe175dfe"
},
"age": 57,
"name": "James Cameron"
}
ドキュメントの削除
update()メソッドと同様に、
delete()メソッドは
id` フィールドを基にしてオブジェクトを削除します。
@app.route('/movies/<id', methods=['PUT'])
def update_movie(id):
body = request.get_json()
movie = Movie.objects.get_or_404(id=id)
movie.update(**body)
return jsonify(str(movie.id)), 200
もちろん、指定した ID のオブジェクトがデータベースに存在するという保証はないので、 delete()
を呼び出す前に get_or_404()
メソッドを使用してオブジェクトを取得します。
それでは、削除リクエストを送信してみましょう。
$ curl -X PUT -H "Content-Type: application/json"\
-d '{"year": 2016}'\
localhost:5000/movies/600eb609b076cdbc347e2b9a/
この結果は
"600eb609b076cdbc347e2b9a"
一度にたくさんの文書を削除することもできます。その場合は、削除したい文書をデータベースに問い合わせ、その結果得られた Queryset に対して delete()
メソッドを呼び出すことになるでしょう。
たとえば、ある年に作られた映画をすべて削除するには、次のようにします。
@app.route('/movies_many/<title', methods=['PUT'])
def update_movie_many(title):
body = request.get_json()
movies = Movie.objects(year=year)
movies.update(**body)
return jsonify([str(movie.id) for movie in movies]), 200
2009`年のムービーエントリーをすべて削除するために、削除リクエストを送信してみましょう。
$ curl -X PUT -H "Content-Type: application/json"\
-d '{"year": 2016}'\
localhost:5000/movies_many/2010/
この結果は
[
"60123af478a2c347ab08c32b",
"60123b0989398f6965f859ab",
"60123bfe2a91e52ba5434630",
"602907f3f3918e990ba24f13",
"602919f67e80d573ad3f15e4"
]
ファイルを操作する
ファイルの作成と保存
MongoEngine では、MongoDB の GridFS と連携してファイルを保存したり取得したりするのがとても簡単です。MongoEngine は FileField()
を使ってこれを実現しています。
それでは、MongoEngine を使って MongoDB GridFS にファイルをアップロードする方法を見てみましょう。
@app.route('/movies/<id', methods=['DELETE'])
def delete_movie(id):
movie = Movie.objects.get_or_404(id=id)
movie.delete()
return jsonify(str(movie.id)), 200
上のブロックを一行ずつ見ていきましょう。
-
- まず
request.files
にあるfile
というキーから画像を取得します。
- まず
-
- 次に、
Movie
オブジェクトを作成します。
- 次に、
- 他のフィールドとは異なり、通常の代入演算子を使用して
FileField()
に値を代入することはできません。その代わりに、put()
メソッドを使用して画像を送信することにします。put()` メソッドは、アップロードするファイル(これは file-like オブジェクトかバイトストリームでなければなりません)、ファイル名、およびオプションのメタデータを引数として受け取ります。 -
- ファイルを保存するために、通常通りムービーオブジェクトの
save()
メソッドを呼び出します。
- ファイルを保存するために、通常通りムービーオブジェクトの
-
- 画像を参照する ID を持つ
movie
オブジェクトを返します。
- 画像を参照する ID を持つ
$ curl -X DELETE -H "Content-Type: application/json"\
localhost:5000/movies/600eb609b076cdbc347e2b9a/
JSON レスポンスからわかるように、ファイルは実際には別の MongoDB ドキュメントとして保存されており、我々はそれに対するデータベース参照を持っているだけです。
ファイルの取得
ファイルを FileField()
に put()
したら、そのフィールドを含むオブジェクトを取得して read()
してメモリに戻すことができます。それでは、MongoDB ドキュメントからファイルを取得する方法を見ていきましょう。
"600eb609b076cdbc347e2b9a"
では、どのような処理をするのかをセグメントで見てみましょう。
- 画像を含むムービードキュメントを取得しました。
-
- イメージをバイト列として
image
変数に保存し、ファイル名とコンテントタイプを取得してfilename
とcontent_type
変数に保存します。
- イメージをバイト列として
-
- Flask の
send_file()
ヘルパーメソッドを使用して、ファイルをユーザに送信しようとしましたが、画像はbytes
オブジェクトなので、AttributeError: 'bytes' object has no attribute 'read'
が発生します。send_file()
は bytes ではなく、 file-like オブジェクトを期待しているので。
- Flask の
-
- この問題を解決するために、
io
モジュールのBytesIO()
クラスを使って、bytes オブジェクトをデコードしてsend_file()
が送信できるような file-like オブジェクトに戻します。
- この問題を解決するために、
ファイルの削除
ファイルを含むドキュメントを削除しても、ファイルは別オブジェクトとして格納されているため、GridFSから削除されることはありません。
ドキュメントとそれに付随するファイルを削除するには、ドキュメントを削除する前に、まずファイルを削除する必要があります。
FileField()
は delete()
メソッドも提供しており、これを利用することでオブジェクト自体の削除を行う前に、単純にデータベースとファイルシステムからファイルを削除することができます。
@app.route('/movies/delete-by-year/<year/', methods=['DELETE'])
def delete_movie_by_year(year):
movies = Movie.objects(year=year)
movies.delete()
return jsonify([str(movie.id) for movie in movies]), 200
結論
MongoEngine は、Python アプリケーションから MongoDB を操作するための比較的シンプルで機能豊富な Pythonic インターフェイスを提供します。そして Flask-MongoEngine は、Flask アプリケーションに MongoDB を簡単に統合できるようにします。
このガイドでは、MongoEngine とその Flask 拡張の機能のいくつかを見てきました。このガイドでは、MongoEngine とその Flask 拡張の機能のいくつかを見てきました。簡単な CRUD API を作成し、MongoDB GridFS を使って MongoEngine を使ったファイルの保存、取得、削除を行いました。
</div