bsonのdump(文字列表現)

人間が確認するためにbsonを文字列にdumpしたいことがある。

bsonはjsonではない

bsonはjsonではないのでjson.dumpsは使えない。

import json
import bson
from collections import ChainMap


person = ChainMap({"name": "foo"}, ChainMap({"age": 20}, {"_id": bson.ObjectId()}))

try:
    print(json.dumps(person))
except Exception as e:
    print("hmm", e)

エラーになる。

hmm ChainMap({'name': 'foo'}, ChainMap({'age': 20}, {'_id': ObjectId('5950d8f10ccee07b8eb563dc')})) is not JSON serializable

json.dumpsに小細工すると見た目が悪い

json.dumpsにdefaultなどを渡して小細工をすると文字列化はできるのだけれど。例えばdictではなくChainMapやOrderedDictみたいなMappingの型の値の場合に見た目が良くない。

print(json.dumps(person, indent=2, ensure_ascii=False, default=str))

これはよくない。

"ChainMap({'name': 'foo'}, ChainMap({'age': 20}, {'_id': ObjectId('5950d9560ccee07c0ec0d91f')}))"

もう少し真面目に書くとマシにはなる。

def default(d):
    if hasattr(d, "keys"):
        return dict(d)
    else:
        return str(d)


print(json.dumps(person, indent=2, ensure_ascii=False, default=default))

マシにはなる。

{
  "_id": "5950d9560ccee07c0ec0d91f",
  "name": "foo",
  "age": 20
}

bson.json_utilが便利かもしれない

bson.json_utilが便利かもしれない。

import bson.json_util as u

print(u.dumps(person, indent=2, ensure_ascii=False))

dumpsに対応するloadsもあったりはするのでこちらのほうが良いのかもしれない(とは言え、こちらの形式もMongoBoosterなどでおなじみの表現じゃないところがちょっと困る(もう少しbsonにencodingした時の状態に近い感じ))。

{
  "name": "foo",
  "_id": {
    "$oid": "5950d99a0ccee07c50d97fd2"
  },
  "age": 20
}

1つのquery objectに紐づく複数のqueryの管理について

(途中:wip)

はじめに

pymongoを直で使うのがつらいと感じる事がある。というよりmongodb一般でつらいと感じることがある。主にjoin。 joinを手軽に書けるwrapper的なものがほしいと感じる事がある

joinが書けるということ

joinが書けるということは1つのquery objectが複数のcollectionをresourceにデータを取得する事ができるようになる必要がある。 つまりQueryをobjectで表した時に複数のconnection(ここで言っているconnectionは実際のmongodbのconnectionではなくあくまで仮想的なもの)を抱えるということになる。

ポイントは2つ

  • どれとどれと結びつけるか判定するために、collection名(テーブル名)を指定できる必要がある。
  • どのようにjoinするかを決められる必要がある(left joinかもしれないしright joinかもしれない)

実際の実装について考えてみると、joinは仮想的なもので、対応はprefetchないしはlazy fetch(ただしbatch query的なもの)という形になる。この時relationの関係(one to many, many to one, one to one, many to many)で少しだけ対応が必要になりそうな気がする。

(joinの実装なのでoptimizerがやってくれることを自分で明示的に実装しないとダメ)

実例

gist

以下の様な構造のcollectionがあるとする、全部embedded documentではなく別々のcollectionに入っている。

// group
{
  id: <id>
  name: <string>
}

// user
{
  id: <id>
  gruop_id: <id> :: rel<group>
  name: <string>
}

// skill (ここはmany to manyになる場合もあるかも?)
{
  id: <id>
  user_id: <id> :: rel<user>
  name: <string>
}
  • groupとuserは1:N
  • skillとuserは1:N

ここでuserをmainのqueryとして考えた時にskillとgroupもjoinして取得したい場合について考える。

users = list(db.users.find({}))
skills = db.skills.find({"user_id": {"$in": list({u["_id"] for u in users})}})
skill_map = defaultdict(list)
for s in skills:
    skill_map[s["user_id"]].append(s)

groups = db.groups.find({"_id": {"$in": list({u["group_id"] for u in users})}})
group_map = {s["_id"]: s for s in groups}

tris = []
for u in users:
    tris.append(dict(user=u, group=group_map[u["group_id"]], skills=skill_map[u["_id"]

つらい。 (TODO: 詳しい説明と例)

もう1つの関門はpagination

pagination的な処理も問題になってくる、通常、pymongoのcursorはiterateするときに、テキトウなサイズでchunkを取得するみたいな実装になっている。 この部分をjoinしたものに対しても上手く動かそうとするとちょっと手間がかかる。あとたしかbatch_size()みたいなメソッドでchunkのサイズを変更できる。このあたりのサポートをどうするかという悩みもある。

(もう少し具体的な話)

(完全に枝葉だけれど)pymongoの場合には、batch_sizeを指定しない場合に、作成したbufferのサイズでchunkを作っているっぽい(正確には、初回で100件位取ってから、次のfetchからbuffer size。呼ばれるのは初回はQueryその後はGetMore)。 batch_sizeを指定した場合には、N件毎にfetchみたいな形になる(単にgetMoreを呼ぶ時にlimitを指定しているだけ)。

class Cursor(object):
    def __iter__(self):
        return self

    def next(self):
        """Advance the cursor."""
        if self.__empty:
            raise StopIteration
        _db = self.__collection.database
        if len(self.__data) or self._refresh():  # このrefreshでgetMoreが呼ばれる
            if self.__manipulate:
                return _db._fix_outgoing(self.__data.popleft(),
                                         self.__collection)
            else:
                return self.__data.popleft()
        else:
            raise StopIteration

あとメンテするのがだるい

あとメンテするのがだるい。とりわけ個別のライブラリやデータベースにハードコードしたライブラリをメンテするのはだるい。 とは言えすぐpandas(+なかったらwrapper書く)というのも結構つらい気がする。