かまたま日記3

プログラミングメイン、たまに日常

MongoDBの位置演算子にハマった件

最近MongoDBを使ったちょっとした社内用のシステムを作っているのですが、そこでハマったポイントです。
※ 現時点で自分はMongoDB素人なので、なぜ以下の現象が起こるかは把握しておりません。ツッコミお待ちしております。

やりたいこと

配列を持ったレコードがあって、特定の条件を満たした配列の要素だけ値を更新したい

起こった現象

以下のようなレコードがあるとします

{
  "_id": "kamatama41",
  "logins": [
    {"domain:" "hoge.com", "day": "19", "count": 3 },
    {"domain:" "fuga.com", "day": "19", "count": 2 },
    {"domain:" "hoge.com", "day": "20", "count": 1 }
  ]
}

このレコードの「domain="hoge.com", day="20"」の要素のcountをインクリメントしたいときに、位置演算子を使って以下のクエリを投げると、結果なぜか"fuga.com"の要素が更新されてしまいます。

> db.sample.update(
... {
...   "_id": "kamatama41",
...   "logins.domain": "hoge.com",
...   "logins.day": "20"
... },
... {
...   $inc:{"logins.$.count": 1}
... });
>
> db.sample.find().forEach(printjson);
{
	"_id" : "kamatama41",
	"logins" : [
		{
			"domain" : "hoge.com",
			"day" : "19",
			"count" : 3
		},
		{
			"domain" : "fuga.com",
			"day" : "20",
			"count" : 3
		},
		{
			"domain" : "hoge.com",
			"day" : "20",
			"count" : 1
		}
	]
}

いろいろ試行錯誤の結果、条件の指定の順番を変えてみたら、今度は「domain="hoge.com",day="19"」の方が更新されてしまいました。

> db.sample.update(
... {
...   "_id": "kamatama41",
...   "logins.day": "20",
...   "logins.domain": "hoge.com"
... },
... {
...   $inc:{"logins.$.count": 1}
... });
>
> db.sample.find().forEach(printjson);
{
	"_id" : "kamatama41",
	"logins" : [
		{
			"domain" : "hoge.com",
			"day" : "19",
			"count" : 4
		},
		{
			"domain" : "fuga.com",
			"day" : "20",
			"count" : 2
		},
		{
			"domain" : "hoge.com",
			"day" : "20",
			"count" : 1
		}
	]
}

解決策

以下のようなデータ構造にして、クエリをちょっと変えたら自分が思った通りの要素が更新されました。

> db.sample.insert(
... {
...   "_id": "kamatama41",
...   "logins": [
...     { "key": {"domain": "hoge.com", "day": "19"}, "count": 3 },
... { "key": {"domain": "fuga.com", "day": "20"}, "count": 2 },
... { "key": {"domain": "hoge.com", "day": "20"}, "count": 1 }
...   ]
... });
>
> db.sample.update(
... {
...   "_id": "kamatama41",
...   "logins.key": { "domain": "hoge.com", "day": "20"}
... },
... {
...   $inc:{"logins.$.count": 1}
... });
>
> db.sample.find().forEach(printjson);
{
	"_id" : "kamatama41",
	"logins" : [
		{
			"key" : {
				"domain" : "hoge.com",
				"day" : "19"
			},
			"count" : 3
		},
		{
			"key" : {
				"domain" : "fuga.com",
				"day" : "20"
			},
			"count" : 2
		},
		{
			"key" : {
				"domain" : "hoge.com",
				"day" : "20"
			},
			"count" : 2
		}
	]
}

ちなみにlogins.keyのjsonのキーを前後逆にしたら更新がかかりませんでした。
うーん、MongoDB(とJSON)良く分からん…

> db.sample.update(
... {
...   "_id": "kamatama41",
...   "logins.key": { "day": "20", "domain": "hoge.com"}
... },
... {
...   $inc:{"logins.$.count": 1}
... });
>
> db.sample.find().forEach(printjson);
{
	"_id" : "kamatama41",
	"logins" : [
		{
			"key" : {
				"domain" : "hoge.com",
				"day" : "19"
			},
			"count" : 3
		},
		{
			"key" : {
				"domain" : "fuga.com",
				"day" : "20"
			},
			"count" : 2
		},
		{
			"key" : {
				"domain" : "hoge.com",
				"day" : "20"
			},
			"count" : 1
		}
	]
}