MongoDBでJOINができないというデマをやめろ

MongoDBの紹介記事、あるいはRDBと比較する記事なんかに、MongoDBのデメリットとして「RDBと違いJOINができない」と書いてあるのをよく見る。MongoDBにJOINが導入される以前の古い記事だったら良いのだが、とんでもないことに最近書かれた記事にもこんなことが堂々と書いてある。2015年にリリースされたバージョン3.2からJOINできるのだが、それ以降の記事でも当たり前のように書かれている。 「MongoDBではJOINができないため、リレーション先のドキュメントを自身のオブジェクトにも持たなければいけません」とかね。

どうせMongoDBを使ったこともない人間が又聞きで広めているのだろうが、こんな大嘘がばら撒かれているようでは技術選定に支障が出るだろう。私もMongoDBのプロフェッショナルというわけでないが、曲がりなりにも業務で1年間使ってきた程度の経験と知見はある。ということで、MongoDBでもJOINができるということをここで力説したい。

MongoDBにおけるJOIN

MongoDBでJOINを行うには $lookup オペレータを使えば良い。例えば、companyとuserが一対一で紐づく簡単なリレーションを考える。コレクションはuserコレクション、companyコレクションの2つで、それぞれ以下のようにデータが入っているとする。(Atlasのドキュメントのコピーボタンからコピーしてきたが、なんかnumberIntとか表示されてて見にくいね)

// userコレクション
{"_id":{"$numberInt":"1"},"companyId":{"$numberInt":"1"},"name":"田中太郎"}
// companyコレクション
{"_id":{"$numberInt":"1"},"name":"hoge会社"}

ここで、userのcompanyId、companyの_idを紐付けてJOINしたい。そんな場合はuserコレクションへのaggregationで以下のように書くことができる。

{$lookup:
  {
    from: "company",
    localField: "companyId",
    foreignField: "_id",
    as: "joinedCompany",
  }
}

実行結果.

from でJOINするコレクション名を指定、 localField に外部キー、 foreignField に紐付ける先のキーを指定、as でJOINしたオブジェクトのフィールド名を指定する。めちゃくちゃシンプルな構文だ。ただ、joinedCompanyの型がArrayになっているのが気になる方もいるかもしれない。

$unwind

$lookup したフィールドの型はArrayになる。一対多ならそのままでもいいのだが、一対一のリレーションでArrayにする必要はないので、これはどうにかしたい。

その場合はさらにaggregation stageに $unwind オペレータを積めば良い。

{$unwind:
  {
    path: "$joinedCompany",
  }
}

今度は joinedArray の型がObjectになっているのが分かる。 $unwind は配列を展開するためのオペレータだ。ちなみに、 $lookup はSQLのLEFT OUTER JOINに等しい操作なため、紐付け先が存在しないuserも取れてしまう。そんな場合は preserveNullAndEmptyArrays をfalseに設定すれば良い。

{$unwind:
  {
    path: "$joinedCompany",
    preserveNullAndEmptyArrays: false
  }
}

今回は一対一の場合を紹介したが、例えばuserが複数の会社に所属するとして companyIds のようなフィールドを持つことになったとしても、上記のクエリの companyId の部分をそのまま置き換えるだけでJOINできる。それくらいMongoDBは賢い。

まとめ

  • MongoDBでもJOINは使える
  • だから正規化も問題なくできる
  • INNER JOINでもOUTER JOINでもできる
  • MongoDBでJOINができないと言う人間はにわか認定して良い

最後のは冗談です。優しく指摘してあげてください。

以上!