CLOVER🍀

That was when it all began.

PyMongoを使って、PythonからMongoDBにアクセスしてみる

これは、なにをしたくて書いたもの?

MongoDBの練習がてらに、PythonからMongoDBにアクセスするプログラムを書いてみようかなと。

MongoDBのPythonドライバー

MongoDBのPythonドライバーは、MongoDBのサイトを見るとPyMongoとMotorがあるようです。

MongoDB Python Drivers — MongoDB Ecosystem

PyMongo — MongoDB Ecosystem

Motor (Async Driver) — MongoDB Ecosystem

PyMongoが推奨のドライバーで、MotorはTornadeやasyncioと互換性のある非同期ドライバーのようです。

MongoDB本体のバージョンとの互換性を確認してみると、Motorは途中で止まっているようにも思います。

PyMongo / Compatibility

Motor(Async Driver) / Compatibility

この意味でも、PyMongoを選択したよさそうですね。

PyMongoの、ドキュメントおよびAPIリファレンスはこちらです。

PyMongo 3.9.0 Documentation — PyMongo 3.9.0 documentation

API Documentation — PyMongo 3.9.0 documentation

では、使っていってみましょう。

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.6.9


$ pip3 -V
pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)

また、MongoDBのバージョンは4.2.3とし、MongoDBが動作しているサーバーのIPアドレスは172.17.0.2とします。

インストール

ドキュメントに沿って、PyMongoをインストールします。

Installing / Upgrading — PyMongo 3.9.0 documentation

Python 2.7、3.4以上に対応しているようですね。

PyMongo supports CPython 2.7, 3.4+, PyPy, and PyPy3.5+

pipでインストール。

$ pip3 install pymongo

今回は、3.10.1がインストールされました。

$ pip3 freeze | grep pymongo
pymongo==3.10.1

が、この時点のドキュメントの「current」は、3.9.0なんですよね。これは、どういうことでしょう。まあ、今回は気にせずいきますか…。
このエントリが参照しているドキュメントへのリンクは、3.9.0で固定してあります。

あと、確認はテストコードで行おうと思うので、pytestもインストールしておきます。

$ pip3 install pytest
$ pip3 freeze | grep pytest
pytest==5.4.1

ディレクトリと__init__.pyを作成して、準備完了。

$ mkdir tests && touch tests/__init__.py

テストコードの雛形

テストコードの雛形は、こちら。
tests/test_pymongo.py

import re

from pymongo import MongoClient
import pymongo

# ここに、テストを書く!

基本的には、PyMongoのチュートリアルを見つつ、必要に応じてAPIリファレンスを参照して進めていくのかな、と思います。

Tutorial — PyMongo 3.9.0 documentation

API Documentation — PyMongo 3.9.0 documentation

PyMongoからMongoDBに接続する

PyMongoからMongoDBに接続する方法は、こちら。

Making a Connection with MongoClient

接続先となるサーバーの記載方法は、2通りあるようですね。また、MongoClientはコンテキストマネージャープロトコルを実装して
いるので、with構文も使えます。

こんな感じですね。

def test_connect_mongo():
    client = MongoClient("172.17.0.2", 27017)

    assert client is not None

    client.close()


def test_connect_mongo_with():
    with MongoClient("mongodb://172.17.0.2:27017") as client:
        assert client is not None

使わなくなった接続は、closeしておきましょう。

ちなみに、MongoClientはコネクションプールの役割も持っています。

How does connection pooling work in PyMongo?

スレッドセーフであり、プロセスセーフではありません、と。

Is PyMongo thread-safe?

Is PyMongo fork-safe?

続いて、データベースとコレクションの取得。

Getting a Database

Getting a Collection

今回は書籍をお題に、データベースとコレクションを作成。

def test_get_database_and_collection():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            assert book_collection is not None

ドキュメント1件の登録、1件の取得

では、ドキュメントを登録してみます。

まずは1件登録して、1件取得。

Inserting a Document

Getting a Single Document With find_one()

こんな感じで、コレクションに対してinsert_oneでドキュメントを1件登録、find_oneで指定のドキュメントを1件取得できます。

def test_document_insert_and_find_one():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            book = {"isbn": "978-4873117386", "title": "入門 Python 3", "price": 4070}

            book_collection.insert_one(book)

            assert book_collection.find_one({"isbn": "978-4873117386"})["title"] == book["title"]
            assert book_collection.count_documents({}) == 1

            book_collection.delete_many({})
            assert book_collection.count_documents({}) == 0

テストコードの都合上、カウントと全件取得も行っていますが。

Counting

delete_many

複数件のドキュメントの登録、取得

続いて、複数ドキュメントの登録と、複数ドキュメントの検索を行ってみましょう。

Bulk Inserts

Querying for More Than One Document

こんな感じで。

def test_document_insert_and_find_many():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            books = [
                {"isbn": "978-4873117386", "title": "入門 Python 3", "price": 4070},
                {"isbn": "978-4048930611", "title": "エキスパートPythonプログラミング改訂2版", "price": 3960},
                {"isbn": "978-4297111113", "title": "Python実践入門", "price": 3278}
            ]

            book_collection.insert_many(books)
            assert book_collection.count_documents({}) == 3

            found_all_books = [book for book in book_collection.find({}, sort = [("price", pymongo.DESCENDING)])]
            assert found_all_books[0]["title"] == "入門 Python 3"
            assert len(found_all_books) == 3

            found_books = [book for book in book_collection.find({"title": re.compile("Python"), "price": {"$gt": 3500}}, sort = [("price", pymongo.ASCENDING)])]
            assert found_books[0]["title"] == "エキスパートPythonプログラミング改訂2版"
            assert len(found_books) == 2

            book_collection.delete_many({})

            assert book_collection.count_documents({}) == 0

ドキュメントを複数件登録するには、コレクションに対してinsert_manyを呼び出せばOKです。引数は、登録したいドキュメントの
リストです。

検索はfindで行うのですが、戻り値はCursorになるので今回は内包表記でリストに変換しています。

            found_all_books = [book for book in book_collection.find({}, sort = [("price", pymongo.DESCENDING)])]

あと、価格の降順ソートにしておきました。

また、もうひとつ検索のパターンとしては、AND条件で正規表現や比較演算子を使うものも。

            found_books = [book for book in book_collection.find({"title": re.compile("Python"), "price": {"$gt": 3500}}, sort = [("price", pymongo.ASCENDING)])]

正規表現で検索を行う場合は、Python正規表現オブジェクトを渡せばOKです。こちらのソートは、価格の昇順にしておきました。

とりあえず、こんなところでしょうか。

まとめ

MongoDBのPythonドライバー、PyMongoからMongoDBにアクセスして、初歩的な操作をざっと試してみました。

ディクショナリーをベースにして使えるので、簡単に使えてよいのではないでしょうか。

ちょっとした時に利用していくんだろうなーと思います。