PyMongoの基本操作

初めましての方は初めまして．そうでない方はいつも読んでいただきありがとうございます．

研究で MongoDB を利用する事になり，その操作のために PyMongo を利用したのですが，今回の記事ではその基本操作を自分のメモ的な感じでまとめたいと思います．

|項目|バージョン|説明|
|:-:|:-:|--|
|`Python`|`3.10.12`|プログラミング言語|
|`PyMongo`|`4.6.1`|PythonでMongoDBを操作するためのライブラリ|
|`MongoDB`|`7.0.5`|データベース|
|その他|-|私の環境ではDockerを利用していますが，他の環境でも同じ操作になると思います．|

:::note
本記事は[PyMongo 4.6.1 Documentation](https://pymongo.readthedocs.io/en/stable/#)を参考にして作成したものです．言うまでもなく[公式ドキュメント](https://pymongo.readthedocs.io/en/stable/#)の方が詳しいので，詳細が気になる方はそちらを参照されて下さい．（英語）
とりあえず動かせれば良いという方や，英語のドキュメントに抵抗のある方，そんな方に向けた記事となります．
:::

# MongoDBとのコネクション
```python: connect_to_mongo
from pymongo import MongoClient

with MongoClient(
    host='host_name',
    port=27017,     # port番号を変更している場合はそれに合わせる
    username='root',     # 今回は研究のみで用い，外部からのアクセスは無いため，rootでログイン
    password='root_password') as client:
    
    # データベースの選択
    database = client['database_name'] 

    # コレクションの選択
    collection = database['collection_name']
```

> 参考
> - [mongo_client – Tools for connecting to MongoDB](https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#module-pymongo.mongo_client)
> - [Getting a Database](https://pymongo.readthedocs.io/en/stable/tutorial.html#getting-a-database)
> - [Getting a Collection](https://pymongo.readthedocs.io/en/stable/tutorial.html#getting-a-collection)


# データの追加
- 1件のデータを追加する場合
    ```python: insert_one
    collection.insert_one(*data)
    ```
- 複数件のデータを追加する場合
    ```python: insert_many
    collection.insert_many(*data)
    ```
> 参考
> - [insert_one](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.insert_one)
> - [insert_many](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.insert_many)


# データの取得
- 1件のデータを取得する場合
    ```python: find_one
    collection.find_one()
    ```
- 複数件のデータを取得する場合
    ```python: find
    collection.find()

    # 取得したデータの確認
    for item in collection.find():
        print(item)
    ```

> 参考
> - [find_one](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one)
> - [find](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find)

# その他
本章では，先述の基本操作以外で（個人的に）よく使うが，調べても（感覚として）あまり検索にヒットしなかったものをまとめておきます．

## 条件検索
- 例1) 条件に一致するデータを取得
    ```python: find_with_query
    query = {       # 'id'が12345のデータを取得
        'id': 12345
    }
    
    response = collection.find(filter=query)
    ```
- 例2) 条件に当てはまるデータを取得
    ```python: find_with_query
    query = {     # 'count'が20以上のデータを取得
        'count': {'$gte': 20}
    }
    
    response = collection.find(filter=query)
    ```
- 例3) 複数の条件を組み合わせたデータの取得
    ```python: find_by_multiple_query
    query = {     # 'id'がtarget_idsに含まれ，'count'が25以上30以下のデータを取得
        'id': {'$in': target_ids}, 
        'count': {'$gte': 25, '$lte': 30}
    }

    response = collection.find(filter=query)
    ```

演算子は以下のものが利用できます．

|演算子|記号|説明|
|:-:|:-:|---|
|$eq|`==`|指定した値と等しい値を照合する．|
|$gt|`>`|指定した値より大きい値を照合する．|
|$gte|`>=`|指定した値以上の値を照合する．|
|$in|`in`|配列で与えられた値のいずれかと一致するものを照合する．|
|$lt|`<`|指定した値より小さい値を照合する．|
|$lte|`<=`|指定した値以下の値を照合する。|
|$ne|`!=`|指定した値と等しくないすべての値を照合する．|
|$nin|`not in`|配列で指定された値のどれにも一致しないものを照合する．|

:::note warn
上表は[公式ドキュメント](https://www.mongodb.com/docs/manual/reference/operator/query/#comparison)を翻訳したものです．最新の情報は[そちら](https://www.mongodb.com/docs/manual/reference/operator/query/#comparison)を参照してください．
ちなみに，翻訳にはDeepLの助けを借りています．
:::

> 参考
> - [find](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find)
> - [Comparison](https://www.mongodb.com/docs/manual/reference/operator/query/#comparison)

## 取得する要素の選択
```python: projection
projection = {     # 'id'と'key2'のみ取得
    'id': True,
    'key1': False,
    'key2': True,
    'key3': False
}

# 表示するもののみを選択することも可能 
# projection = {     # 'id'と'key2'のみ取得 
#     'id': True,
#     'key2': True
# }

# ただし，Falseも明示的に指定する場合は全てのキーについて記述する必要あり
# 以下のような指定ではエラーが発生する
# projection = {     # 'id'と'key2'のみ取得
#     'id': True,
#     'key2': True,
#     'key3': False
# }

collection.find(projection=projection)
```

:::note 
使い方を調べる中で部分的に`False`を指定すること（コードブロック内の注意書きのような指定方法）も可能であるという記事も見かけましたが，少なくとも既述の私の環境では確認できませんでした．また，公式ドキュメント内でも

> a list of field names that should be returned in the result set or a dict specifying the fields to include or exclude. If projection is a list “_id” will always be returned. Use a dict to exclude fields from the result (e.g. projection={‘_id’: False}).

というように述べられており，部分的に指定できるという点は疑わしいと考えています．（指定できるという方がいらっしゃれば，環境を教えて頂きたいです．）
:::

> 参考
> - [find](https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find)

## 取得する件数の指定
```python: limitation
# 100件を上限としてデータを取得
response = collection.find().limit(100)
```

> 参考
> - [limit](https://pymongo.readthedocs.io/en/stable/api/pymongo/cursor.html#pymongo.cursor.Cursor.limit)

## ユニークな値を取得
```python: unique
# 指定したkeyのユニークなvalueを取得
response = collection.find().distinct(key)
```

> 参考
> - [distinct](https://pymongo.readthedocs.io/en/stable/api/pymongo/cursor.html#pymongo.cursor.Cursor.distinct)


# 最後に
ここまでお読みいただいてありがとうございました．

MongoDBを使用し始めて1週間も経たずに書いている記事で，私自身まだまだ学ぶべきことは多いと感じています．同じような方の助けになれば幸いです．

MongoDBを使いこなしている方は本記事の内容に誤りやもっとこうすべきという部分があれば，是非ご教示頂けますと幸いです．


# おまけ
PyMongoとは若干外れますが，WebAPIでJsonファイル形式で取得したデータを格納するときに作成した関数を貼っておきます．
私の知識不足なのかもしれませんが，入れ子構造をもつJson形式のデータを入れ子構造を持たないJson形式のデータに変換する（？）ライブラリが見当たらず，そのままではMongoDBに格納できないので自作した関数です．（もしライブラリをご存知の方がいらっしゃればご教示頂きたいです．）

```python: flatten_json
# 以下のような変換をおこなう
{
    `id`: 12345,
    `key1`: value_1,
    `key2`: {
        'sub_key1': sub_value_1,
        'sub_key2': [
            {'sub_sub_key1': sub_sub_value1},
            {'sub_sub_key2': sub_sub_value2}
        ],
        'sub_key3': sub_value_3
    }
}

==> {
    `id`: 12345,
    `key1`: value_1,
    `key2_sub_key1`: sub_value_1,
    `key2_sub_key2_sub_sub_key1`:sub_sub_value1,
    `key2_sub_key2_sub_sub_key2`:sub_sub_value2,
    `key2_sub_key3`: sub_value_3
}


def flatten_json(data, parent_key: str='', sep: str='_'):
    '''
    Jsonデータをフラットにする関数

    Args:
        data: Jsonデータ
        parent_key (str): 上位のキー
        sep (str): キーを分割する記号

    Returns:
        dict: 入れ子構造を持たない辞書型データ
    '''
    items = {}
    if isinstance(data, dict):
        for key, value in data.items():
            new_key = parent_key + sep + key if parent_key else key
            items.update(flatten_json(value, new_key, sep=sep))
    elif isinstance(data, list):
        for i, value in enumerate(data):
            new_key = parent_key + sep
            items.update(flatten_json(value, new_key, sep=sep))
    else:
        items[parent_key] = data
    return items
```

:::note alert
上記のコードは動作を保証するものではありません．コードを良く理解した上で，ご自身の責任で利用されてください．
:::