MongoDBでJOINっぽいことをしてみる

# はじめに

3.2からaggregation Stageに [$lookup](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/)が追加され、SQLのJOINっぽいことができるようになったので、試してみようと思います。

# 準備
部署IDを項目に持つ社員コレクションと、部署コレクションを用意します。
## サンプルデータ
**employee コレクション**

```
/* 1 */
{
    "_id" : ObjectId("588dea189433b6511a29fcee"),
    "id" : "000001",
    "name" : "Tanaka",
    "sectionId" : "SEC-001",
    "rank" : "SE-1"
}

/* 2 */
{
    "_id" : ObjectId("588dea419433b6511a29fcef"),
    "id" : "000002",
    "name" : "Suzuki",
    "sectionId" : "SEC-001",
    "rank" : "SE-1"
}

/* 3 */
{
    "_id" : ObjectId("588dea502e5af7712f36b140"),
    "id" : "000003",
    "name" : "Sato",
    "sectionId" : "SEC-002",
    "rank" : "SE-4"
}

/* 4 */
{
    "_id" : ObjectId("588dea5e2e5af7712f36b14e"),
    "id" : "000004",
    "name" : "Tamura",
    "sectionId" : "SEC-003",
    "rank" : "GM-1"
}
```

**section コレクション**

```
/* 1 */
{
    "_id" : ObjectId("588deb0c2e5af7712f36b1a1"),
    "sectionId" : "SEC-001",
    "sectionName" : "第一開発部"
}

/* 2 */
{
    "_id" : ObjectId("588deb239433b6511a29fcf0"),
    "sectionId" : "SEC-002",
    "sectionName" : "第二開発部"
}

/* 3 */
{
    "_id" : ObjectId("588deb5a9433b6511a29fcf1"),
    "sectionId" : "SEC-003",
    "sectionName" : "事務"
}
```

# $lookupする

employeeコレクションを基準に、sectionコレクションを$lookupします。

```
db.employee.aggregate([
    {$lookup:
        {
            from:"section",
            localField:"sectionId",
            foreignField:"sectionId",
            as:"sectionInfos"
        }
    }
])
```

* from: 参照するコレクション
* localField: 結合に使用する、基準コレクション側の項目名
* foreignField: 結合に使用する、参照コレクション側の項目名
* as: 別名

この例では、localFieldとforeignFieldの項目名が同じものになっていますが、同じである必要はありません。

結果は以下の通り。

```
/* 1 */
{
    "_id" : ObjectId("588dea189433b6511a29fcee"),
    "id" : "000001",
    "name" : "Tanaka",
    "sectionId" : "SEC-001",
    "rank" : "SE-1",
    "sectionInfos" : [ 
        {
            "_id" : ObjectId("588deb0c2e5af7712f36b1a1"),
            "sectionId" : "SEC-001",
            "sectionName" : "第一開発部"
        }
    ]
}

/* 2 */
{
    "_id" : ObjectId("588dea419433b6511a29fcef"),
    "id" : "000002",
    "name" : "Suzuki",
    "sectionId" : "SEC-001",
    "rank" : "SE-1",
    "sectionInfos" : [ 
        {
            "_id" : ObjectId("588deb0c2e5af7712f36b1a1"),
            "sectionId" : "SEC-001",
            "sectionName" : "第一開発部"
        }
    ]
}

/* 3 */
{
    "_id" : ObjectId("588dea502e5af7712f36b140"),
    "id" : "000003",
    "name" : "Sato",
    "sectionId" : "SEC-002",
    "rank" : "SE-4",
    "sectionInfos" : [ 
        {
            "_id" : ObjectId("588deb239433b6511a29fcf0"),
            "sectionId" : "SEC-002",
            "sectionName" : "第二開発部"
        }
    ]
}

/* 4 */
{
    "_id" : ObjectId("588dea5e2e5af7712f36b14e"),
    "id" : "000004",
    "name" : "Tamura",
    "sectionId" : "SEC-003",
    "rank" : "GM-1",
    "sectionInfos" : [ 
        {
            "_id" : ObjectId("588deb5a9433b6511a29fcf1"),
            "sectionId" : "SEC-003",
            "sectionName" : "事務"
        }
    ]
}
```

\$lookupした場所が、別名で配列になって返ってきました。

# 配列を取り出す

今回のサンプルでは、一人の社員は一つの部署にしか所属しないので、配列のままだと不格好です。というわけで、配列部を\$unwindします。
あと、4レコードは多いので、\$matchでTanakaさんだけに絞り込みます。

```
db.employee.aggregate([
    {$match:{id:"000001"}},
    {$lookup:
        {
            from:"section",
            localField:"sectionId",
            foreignField:"sectionId",
            as:"sectionInfos"
        }
    },
    {$unwind:"$sectionInfos"}
])
```

結果は次の通り。

```
/* 1 */
{
    "_id" : ObjectId("588dea189433b6511a29fcee"),
    "id" : "000001",
    "name" : "Tanaka",
    "sectionId" : "SEC-001",
    "rank" : "SE-1",
    "sectionInfos" : {
        "_id" : ObjectId("588deb0c2e5af7712f36b1a1"),
        "sectionId" : "SEC-001",
        "sectionName" : "第一開発部"
    }
}
```

# 必要な項目だけを取得する

最後に $projectを使用して必要な項目だけを取り出します。

```
db.employee.aggregate([
    {$match:{id:"000001"}},
    {$lookup:
        {
            from:"section",
            localField:"sectionId",
            foreignField:"sectionId",
            as:"sectionInfos"
        }
    },
    {$unwind:"$sectionInfos"},
    {$project:
        {
            "id":"$id",
            "name":"$name",
            "sectionId":"$sectionId",
            "sectionName":"$sectionInfos.sectionName",
            "rank":"$rank"
        }
    }
])
```
WHEREを書いて、JOINを書いて、SELECTを書くみたいなイメージになりました。
*※ この順番で記述しなければならないわけではないので、目的に応じて最適な順序で記述するようにしてください。*

結果は次の通り

```
/* 1 */
{
    "_id" : ObjectId("588dea189433b6511a29fcee"),
    "id" : "000001",
    "name" : "Tanaka",
    "sectionId" : "SEC-001",
    "sectionName" : "第一開発部",
    "rank" : "SE-1"
}
```

例示はしませんが、\$lookupでsectionからもってきた項目を使って、さらに別のコレクションを\$lookupするなんてことも可能です。

# おわりに
これでデータを取得する面からは、MongoDBでも正規化したデータ構造を利用することができますね。

ただ、MongoDBはトランザクションをサポートしていませんので、正規化したコレクションを完全にCRUD操作するにはアプリケーション側で、[2相コミット](https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/)などの機構を構築するなどの工夫が必要なようです。



誤りなどありましたら、コメントにご指摘をいただけますと幸いです。