PyMongoを使ってMongoDBとPythonを連携させる

この記事では、Pythonの観点からデータストアとしてのMongoDBに飛び込んでいきます。そのために、簡単なスクリプトを書いて、何ができるのか、そこからどんな利益が得られるのかを紹介します。

Webアプリケーションは、他の多くのソフトウェアアプリケーションと同様に、データを動力源としています。このデータの整理と保存は、私たちが自由に使える様々なアプリケーションとどのように相互作用するかを決定するため、重要です。また、扱うデータの種類によって、このプロセスの進め方に影響を与えることがあります。

データベースは、データの整理と保存を可能にすると同時に、情報の保存、アクセス、セキュリティの確保をコントロールすることができます。

NoSQLデータベース

データベースには、大きく分けてリレーショナルデータベースと非リレーショナルデータベースの2種類があります。

リレーショナル・データベースは、データベース内の別のデータに関連してデータを保存、アクセス、操作することができるデータベースである。データは、行と列を持つ組織化されたテーブルに格納され、テーブル間の情報を結びつけるリレーションシップがあります。このようなデータベースを扱うには、構造化照会言語(Structured Query Language、SQL)を使用し、MySQLやPostgreSQLなどがその例である。

非リレーショナルデータベースは、リレーショナルデータベースのような関係でも表形式でもなく、データを保存する。SQLを使わないので、NoSQLデータベースとも呼ばれる。

さらにNoSQLデータベースは、キーバリューストア、グラフストア、カラムストア、ドキュメントストアに分けることができ、MongoDBはこれに該当する。

MongoDBとその利用シーンについて

MongoDBはドキュメントストアであり、非リレーショナルデータベースである。データをドキュメントで構成されるコレクションに格納することができます。

MongoDB では、ドキュメントとは BSON (Binary-JSON) と呼ばれる JSON ライクなバイナリシリアライズ形式のことで、最大サイズは 16 メガバイトに制限されています。このサイズ制限は、転送時にメモリと帯域幅を効率的に使用するために設けられている。

MongoDBでは、この制限を超えるサイズのファイルを保存する必要がある場合に備えて、GridFSという仕様も提供しています。

ドキュメントは、通常のJSONデータと同じように、フィールドと値のペアで構成されています。しかし、このBSONフォーマットは、Date型やBinary Data型のような、より多くのデータ型を含むこともできます。BSONは、軽量でトラバースが容易であり、BSONとの間でデータをエンコードおよびデコードする際に効率的であるように設計されています。

NoSQLデータストアであるMongoDBは、リレーショナルデータベースよりも非リレーショナルデータベースを使うことで得られる利点を享受することができます。データをシャーディングやパーティショニングして複数のマシンに配置することで、効率的に水平方向に拡張できるため、高いスケーラビリティを実現できるのがメリットです。

また、MongoDBでは、構造化、半構造化、非構造化データを大量に保存しても、データ間のリレーションシップを維持する必要がない。オープンソースであるため、MongoDBの導入コストは、メンテナンスと専門知識だけに抑えられています。

他のソリューションと同様、MongoDBの使用にはデメリットがあります。まず、保存されたデータ間のリレーションシップを維持できないことだ。このため、一貫性を確保するACIDトランザクションを実行することが難しい。

ACIDトランザクションをサポートしようとすると、複雑さが増してしまう。MongoDBは他のNoSQLデータストアと同様、リレーショナルデータベースほど成熟しておらず、このため専門家を見つけるのが難しい場合がある。

MongoDBの非リレーショナルな性質は、リレーショナルなものよりも特定の状況でのデータ保存に理想的です。たとえば、MongoDBがリレーショナルデータベースより適しているシナリオは、データ形式が柔軟で関係がない場合です。

柔軟なデータ、非リレーショナルなデータであれば、リレーショナルデータベースとは異なり、データを保存する際にACID特性を維持する必要はないのです。また、MongoDBでは、データを新しいノードに簡単にスケールすることができます。

しかし、MongoDBの利点は、データがリレーショナルなものである場合には理想的ではありません。たとえば、顧客のレコードとその注文を保存している場合です。

このような場合、データ間の関係を維持するためにリレーショナルデータベースが必要になり、これは重要なことです。また、ACIDに準拠する必要がある場合も、MongoDBの使用は適さない。

Mongo シェルで MongoDB と対話する

MongoDB を使うには MongoDB サーバーをインストールする必要があります。これは公式ホームページからダウンロードできます。このデモでは無料の Community Server を使用します。

MongoDB サーバーには Mongo シェルが付属しており、これを使うとターミナルからサーバーにアクセスできます。

このシェルを起動するには、ターミナルで mongo と入力します。すると、MongoDB と Mongo Shell のバージョン、そしてサーバーの URL など、MongoDB サーバーのセットアップに関する情報が表示されます。

たとえば、このサーバーは次のような環境で動いています。

mongodb://127.0.0.1:27017


MongoDBでは、ドキュメントを含むコレクションを保持するためにデータベースが使用されます。Mongo シェルでは、useコマンドを使って新しいデータベースを作成したり、既存のデータベースに切り替えたりすることができます。

> use SeriesDB


これ以降に実行する操作はすべて SeriesDB データベースに反映されます。このデータベースには、リレーショナルデータベースのテーブルに相当するコレクションを格納します。

例えば、このチュートリアルの目的のために、いくつかのシリーズをデータベースに追加してみましょう。

> db.series.insertMany([
... { name: "Game of Thrones", year: 2012},
... { name: "House of Cards", year: 2013 },
... { name: "Suits", year: 2011}
... ])


と迎えています。

{
    "acknowledged" : true,
    "insertedIds" : [
        ObjectId("5e300724c013a3b1a742c3b9"),
        ObjectId("5e300724c013a3b1a742c3ba"),
        ObjectId("5e300724c013a3b1a742c3bb")
    ]
}


シリーズコレクションに格納されているすべてのドキュメントを取得するために、db.inventory.find({})を使用します。このSQLはSELECT * FROM seriesと同等です。空のクエリ (つまり{}`) を渡すと、すべてのドキュメントが返されます。

> db.series.find({})


{ "_id" : ObjectId("5e3006258c33209a674d1d1e"), "name" : "The Blacklist", "year" : 2013 }
{ "_id" : ObjectId("5e300724c013a3b1a742c3b9"), "name" : "Game of Thrones", "year" : 2012 }
{ "_id" : ObjectId("5e300724c013a3b1a742c3ba"), "name" : "House of Cards", "year" : 2013 }
{ "_id" : ObjectId("5e300724c013a3b1a742c3bb"), "name" : "Suits", "year" : 2011 }


例えば、2013年に放映されたテレビシリーズをすべて返すには、等号条件を使用します。

> db.series.find({ year: 2013 })
{ "_id" : ObjectId("5e3006258c33209a674d1d1e"), "name" : "The Blacklist", "year" : 2013 }
{ "_id" : ObjectId("5e300724c013a3b1a742c3ba"), "name" : "House of Cards", "year" : 2013 }


SQL では SELECT * FROM series WHERE year=2013 となります。

MongoDB では db.collection.UpdateOne() を使って個々のドキュメントを更新したり、 db.collection.UpdateMany() を使って一括で更新したりすることもできます。例えば、Suits のリリース年を更新するには、次のようにします。

> db.series.updateOne(
{ name: "Suits" },
{
    $set: { year: 2010 }
}
)
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }


最後に、ドキュメントを削除するために、Mongo シェルは db.collection.deleteOne()db.collection.deleteMany() という関数を提供しています。

たとえば、2012に始まったシリーズをすべて削除するには、次のように実行します。

> db.series.deleteMany({ year: 2012 })
{ "acknowledged" : true, "deletedCount" : 2 }


MongoDB の CRUD 操作に関する詳しい情報は、オンラインリファレンスにあります。例や条件付きの操作、アトミック性、SQL の概念と MongoDB の概念や用語の対応付けなどです。

PythonとMongoDBの連携

MongoDB は Python や JavaScript、Java、Go、C# などさまざまなプログラミング言語を使って MongoDB データストアを操作するためのドライバーとツールを提供しています。

PyMongo は Python 用の公式 MongoDB ドライバで、今回はこれを使って SeriesDB データベースに保存されているデータを操作する簡単なスクリプトを作成します。

Python 3.6+とVirtualenvがインストールされているマシンで、アプリケーション用の仮想環境を作成し、pipでPyMongoをインストールしましょう。

$ virtualenv --python=python3 env --no-site-packages
$ source env/bin/activate
$ pip install pymongo


PyMongo` を使って、MongoDB のデータベースに対して様々な操作を行うための簡単なスクリプトを書いてみましょう。

MongoDBへの接続

まずは mongo_db_script.pypymongo をインポートし、ローカルで動作している MongoDB のインスタンスに接続するクライアントを作成します。

import pymongo


# Create the client
client = MongoClient('localhost', 27017)


# Connect to our database
db = client['SeriesDB']


# Fetch our series collection
series_collection = db['series']


ここまでで、MongoDB サーバーに接続するクライアントを作成し、それを使って ‘SeriesDB’ データベースを取得することができました。そして ‘series’ コレクションを取得してオブジェクトに格納します。

ドキュメントの作成

このスクリプトをより便利にするために、PyMongoをラップする関数を書いて、簡単にデータを操作できるようにします。Pythonの辞書を使ってドキュメントを表現し、その辞書を関数に渡していきます。まず、’series’コレクションにデータを挿入する関数を作ってみましょう。

# Imports truncated for brevity


def insert_document(collection, data):
    """ Function to insert a document into a collection and
    return the document's id.
    """
    return collection.insert_one(data).inserted_id


この関数はコレクションとデータの辞書を受け取り、提供されたコレクションにデータを挿入します。この関数は識別子を返すので、それを使ってデータベースから個々のオブジェクトを正確に取得できます。

また、MongoDB はデータを作成するときに、ドキュメントに _id キーがない場合はそれを追加することにも注意しましょう。

それでは、この関数で番組を追加してみましょう。

new_show = {
    "name": "FRIENDS",
    "year": 1994
}
print(insert_document(series_collection, new_show))


出力は次のとおりです。

5e4465cfdcbbdc68a6df233f


スクリプトを実行すると、新しいショーの _id がターミナルに表示され、後でこの識別子を使ってショーを取得することができます。

この識別子を使用して、後でショーを取得することができます。辞書にあるように、自動的に割り当てられるのではなく、 _id 値を指定することができます。

new_show = {
    "_id": "1",
    "name": "FRIENDS",
    "year": 1994
}


そして、もし既存の _id を持つドキュメントを保存しようとすると、次のようなエラーが表示されるでしょう。

DuplicateKeyError: E11000 duplicate key error index: SeriesDB.series.$id dup key: { : 1}


ドキュメントの取得

データベースからドキュメントを取得するには、find_document() を使用します。この関数は、フィルタリングしたい要素を含む辞書と、ドキュメントが一つなのか複数なのかを指定するためのオプションの引数を受け取ります。

# Imports and previous code truncated for brevity


def find_document(collection, elements, multiple=False):
    """ Function to retrieve single or multiple documents from a provided
    Collection using a dictionary containing a document's elements.
    """
    if multiple:
        results = collection.find(elements)
        return [r for r in results]
    else:
        return collection.find_one(elements)


では、この関数を使ってドキュメントを検索してみましょう。

result = find_document(series_collection, {'name': 'FRIENDS'})
print(result)


この関数を実行するときに multiple パラメータを指定しなかったので、結果は1つの文書になりました。

{'_id': ObjectId('5e3031440597a8b07d2f4111'), 'name': 'FRIENDS', 'year': 1994}


multipleパラメータを指定すると、コレクション内のname属性がFRIENDS` に設定されているすべてのドキュメントのリストが出力されます。

ドキュメントを更新する

次の関数 update_document() は、特定のひとつのドキュメントを更新するために使用されます。ドキュメントを探すときには、そのドキュメントの _id と、そのドキュメントが属しているコレクションを使用します。

# Imports and previous code truncated for brevity


def update_document(collection, query_elements, new_values):
    """ Function to update a single document in a collection.
    """
    collection.update_one(query_elements, {'$set': new_values})


では、ドキュメントを挿入してみましょう。

new_show = {
    "name": "FRIENDS",
    "year": 1995
}
id_ = insert_document(series_collection, new_show)


ドキュメントを追加したときに返された _id を使って、ドキュメントを更新してみましょう。

update_document(series_collection, {'_id': id_}, {'name': 'F.R.I.E.N.D.S'})


そして最後に、新しい値が適所に置かれたことを確認するためにそれを取得し、その結果を表示してみましょう。

result = find_document(series_collection, {'_id': id_})
print(result)


スクリプトを実行すると、ドキュメントが更新されたことがわかります。

{'_id': ObjectId('5e30378e96729abc101e3997'), 'name': 'F.R.I.E.N.D.S', 'year': 1995}


ドキュメントの削除

最後に、ドキュメントを削除する関数を書いてみましょう。

# Imports and previous code truncated for brevity


def delete_document(collection, query):
    """ Function to delete a single document from a collection.
    """
    collection.delete_one(query)


ここでは delete_one メソッドを使用しているので、クエリが複数の文書にマッチしても、1回の呼び出しで1つの文書しか削除することができません。

では、この関数を使ってエントリーを削除してみましょう。

delete_document(series_collection, {'_id': id_})


同じドキュメントを検索してみると

result = find_document(series_collection, {'_id': id_})
print(result)


期待通りの結果が返ってきます。

None


次のステップ

ここまでで、Python スクリプトから MongoDB サーバーにアクセスするための PyMongo のメソッドをいくつか紹介してきました。しかし、このモジュールで利用できるすべてのメソッドを利用したわけではありません。

利用可能なすべてのメソッドは PyMongo の公式ドキュメントにあり、サブモジュールごとに分類されています。

今回は、MongoDBデータベースに対して初歩的なCRUD機能を実行するシンプルなスクリプトを書きました。もっと複雑なコードベースや、例えば Flask/Django アプリケーションにこの関数をインポートすることもできますが、これらのフレームワークにはすでに同じ結果を得るためのライブラリがあります。これらのライブラリによって、より簡単に、より便利に、そしてより安全に MongoDB に接続できるようになります。

例えば Django では Django MongoDB Engine や Djongo といったライブラリが使えますし、 Flask では Flask-PyMongo があり、 Flask と PyMongo の橋渡しをして MongoDB データベースへのシームレスな接続を手助けしてくれます。

結論

MongoDBはドキュメントストアで、非リレーショナルデータベース(NoSQL)のカテゴリに分類されます。リレーショナルデータベースと比較して、ある種の利点がある一方、いくつかの欠点もあります。

すべての状況に適しているわけではありませんが、MongoDBを使ってデータを保存し、 PyMongo などのライブラリを使ってPythonアプリケーションからデータを操作することで、最適な状況でMongoDBのパワーを活用することができます。

したがって、MongoDBをデータの保存に使うかどうかを決める前に、自分たちの要求をよく吟味することが必要です。

この記事で書いたスクリプトは GitHub で見ることができます。

タイトルとURLをコピーしました