Goのmgoで$lookupと$projectを使って結合したデータを取得する

mongoDBでの結合とフィルター

mongoDBはドキュメント型のNoSQLですが、左外部結合ができます。 で、左外部結合したうえでフィルターにかけ、要求されたデータを返すことができます。

前半の説明は以下のページを参考にしています。 Mongodb Join on _id field from String to ObjectId MongoPlayground

$lookup

$lookupにより、join先の指定したフィールドのドキュメントから要求されたドキュメントを配列にし、join元の指定されたドキュメントに組み込みます。

{
   $lookup:
     {
       from: <join先のコレクション>,
       localField: <join元のフィールド>,
       foreignField: <join先のフィールド>,
       as: <join元に組み込まれることになる配列フィールド>
     }
}

mongoDB Documentation $lookup (aggregation)

$lookupのようなAggregationPipelineStagesはdocument.aggregate()に指定して実行します。 AggregationPipeline

db.document.aggregate([{ここで指定する}])

すなわち、

db.original_collection.aggregate([
  {
    "$lookup": {
      "from": "another_collection",
      "localField": "_id",
      "foreignField": "original_collection_id",
      "as": "another_collections"
    }
  }
])

このようにして使うことができます。

ですが、このままでは結合されたデータを取得することはできません。 取得するためにはもう一つ、$projectというステージを追加する必要があります。

$project

$projectでは、取得するドキュメントに対して設定を割り当てることができます。

{ $project: { <設定> } }

設定の中では、どのフィールドの値を表示するかとか、特定のフィールドの値を変形させるなどして新しいフィールドに置き換えるとかなどを行えます。

ここでは、idとjoin元に紐づくjoin先のデータをまとめた配列を指定します。 この時、idはobjectIdなので、この中身だけを引っ張り出してきたstringに変換します。

したがって、$projectの設定は以下のようになります。

{ "$project": {"_id": { "$toString": "$_id" }} }

$toString (aggregation)について

ここで$lookup$projectを組み合わせて、以下のクエリーを作成します。

db.original_collection.aggregate([
  { "$project": {"_id": { "$toString": "$_id" }} },
  {
    "$lookup": {
      "from": "another_collection",
      "localField": "_id",
      "foreignField": "original_collection_id",
      "as": "another_collections"
    }
  }
])

これを実行すると、おそらく以下のようなレスポンスが返ってくるでしょう(DBの中身が正しければ)。

{
  "_id" : "5caae079e1382335f04c5846",
  "another_collections" : [
    {
      "_id" : ObjectId("5caae1cce1382337367b5316"),
      "name" : "anotherCollection",
      "original_collection_id" : "5caae079e1382335f04c5846"
    }
  ]
}

これをmgoでやりたい

上に述べたのは、mongoのDBに入って直接いじるやり方です。

Goのmgoでやるときは下のようにしてやります。 mgoの使い方とか細かい部分は端折って、typeの定義の部分と、dbでfindする部分だけ書きます。

// typeの定義
type SubCollection struct { // join先
  Id bson.ObjectId `bson:"_id"`
  Name string `bson:"name"`
  OriginalCollectionId string `bson:"original_collection_id"`
}
type OriginalCollection struct { // join元
  Id bson.ObjectId `bson:"_id"`
  Name string `bson:"name"`
  SubCollections []SubCollection
}

// 結果を取得する
var AllOriginalCollections []OriginalCollection

query := []bson.M{
  {
    "$project": bson.M{
      "_id": bson.M{"$toString": "$_id"},
    },
  },
  {
    "$lookup": bson.M{
      "from": "SubCollections",
      "localField": "_id",
      "foreignField": "original_collection_id",
      "as": "original_collections",
    },
  },
}

// コレクションdatabasesを指定し、AggregationPipelineを使うためのPipe関数をqueryを引数にして呼び出す
pipe := db.C("databases").Pipe(query)
pipe.All(&AllOriginalCollections) // APの設定を踏まえたクエリー(All:全取得)の実行
fmt.Println(AllOriginalCollections) // 結果表示

構造体であらかじめ$lookupのasで指定するフィールドを設定しておかないといけない点と、AggregationPipelineを利用するためにPipe関数を使っている点以外は基本上で説明した通りです。

たぶん以下のような結果が返ってくると思います。

[
  {
    ObjectIdHex("356361616530373965313338323333356630346335383436")
    sampleOriginalCollection
    [
      {
        ObjectIdHex("5caae1cce1382337367b5316")
        sampleSubCollection
        5caae079e1382335f04c5846
      }
    ]
  }
]

ただ、この時ObjectIdHexがなぜかよくわからない数字の文字列になります。 原因はわかりませんが、以下のように$projectに結合用にObjectIdをstringに変換したidのカラムを作って、もともとのObjectIdのカラムはそのまま出力すれば、問題は解決されます。

// queryの部分だけ
query := []bson.M{
  {
    "$project": bson.M{
      "_id": 1,
      "objectIdToString": bson.M{"$toString": "$_id"}
    },
  },
  {
    "$lookup": bson.M{
      "from": "SubCollections",
      "localField": "objectIdToString",
      "foreignField": "original_collection_id",
      "as": "original_collections",
    },
  },
}

objectIdHexが変形せずに出力されるようになります。

[
  {
    ObjectIdHex("5caae079e1382335f04c5846")
    sampleOriginalCollection
    [
      {
        ObjectIdHex("5caae1cce1382337367b5316")
        sampleSubCollection
        5caae079e1382335f04c5846
      }
    ]
  }
]