MongoDB 데이터간 JOIN 방법($lookup)

MongoDB 가 하나의 컬렉션안에 다양한 계층과 릴레이션 관계를 JSON 으로 구조화 할 수 있긴 하지만, 결국에는 collection 간에 데이터를 조인(JOIN) 해야 하는 니즈는 생기기 마련이다. aggregate 함수에서는 $lookup 기능을 이용하여 컬렉션간에 데이터를 JOIN 할 수 있다.

$lookup 사용법

샘플데이터를 하나 준비했다. 학원 수납정보인 sampleData 컬렉션과 회원정보 member 컬렉션을 조인해보기로 한다. 그 중 sampleData 컬렉션의 memberId 필드는 회원정보 member 컬렉션에 있는 _id 필드이다.

mydb> db.sampleData.find()

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000
  }
]

member 컬렉션에서 _id 값은 ObjectId 타입이라서 아래와 같이 조회해야 한다.

db.member.find({ _id : ObjectId('655808defd437954470f9d7d')  })

그렇다보니, member 컬렉션과 조인하려면 memberId 필드를 미리 ObjectId 타입으로 변경해놓아야 한다. aggregate 함수내에서는 $toObjectId 로 변환한다.

db.sampleData.aggregate([
    {
        $addFields : {
            memberObjectId : {
                $toObjectId : "$memberId"
            }
        }
    }
])

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000,
    memberObjectId: ObjectId("655808defd437954470f9d7d")
  }
]

이제 준비가 끝났으니 $lookup 으로 조인만 하면 된다.

db.sampleData.aggregate([
    {
        $addFields : {
            memberObjectId : {
                $toObjectId : "$memberId"
            }
        }
    },
    {
        $lookup : {
            from : "member",
            localField : "memberObjectId",
            foreignField : "_id",
            as: "member_info"
        }
    }
])

출력결과를 보면 member_info 이름으로 조인된걸 볼 수 있다.

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000,
    memberObjectId: ObjectId("655808defd437954470f9d7d"),
    member_info: [
      {
        _id: ObjectId("655808defd437954470f9d7d"),
        name: '홍길동',
        gender: 'M',
        membershipDate: '2023-12-02',
        delYn: false
      }
    ]
  }
]

여기가 완성이면 좋겠지만, $project로 필요한 데이터만 출력해보려 한다.

db.sampleData.aggregate([
    {
        $addFields : {
            memberObjectId : {
                $toObjectId : "$memberId"
            }
        }
    },
    {
        $lookup : {
            from : "member",
            localField : "memberObjectId",
            foreignField : "_id",
            as: "member_info"
        }
    },
    {
        $project : {
            memberId : 1,
            paymentDate : 1,
            amount : 1,
            "member_info.name" : 1,
            "member_info.gender" : 1
        }
    }
])

그런데 결과를 보니, member_info 구조가 그대로 유지되어 있을 뿐아니라, array 형식도 유지되어 있다.

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000,
    member_info: [ { name: '홍길동', gender: 'M' } ]
  }
]

$lookup 으로 조인된 데이터는 [ ] 로 묶여있다. array로 연결되어 있기 때문에, $arrayElemAt 기능으로 꺼내어 필요한 필드만 가져와야 한다. 어짜피 이 샘플데이터들은 1:1로 조인되는 구조라 이렇게 한 것이고, 다건인 경우 케이스에 맞춰 정제해야 한다.

db.sampleData.aggregate([
    {
        $addFields : {
            memberObjectId : {
                $toObjectId : "$memberId"
            }
        }
    },
    {
        $lookup : {
            from : "member",
            localField : "memberObjectId",
            foreignField : "_id",
            as: "member_info"
        }
    },
    {
        $addFields: {
            name : {
                $arrayElemAt : [ "$member_info.name", 0 ]
            },
            gender : {
                $arrayElemAt : [ "$member_info.gender", 0 ]
            }
        }
    }
])

name, gender 가 밖으로 추가된게 보인다.

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000,
    memberObjectId: ObjectId("655808defd437954470f9d7d"),
    member_info: [
      {
        _id: ObjectId("655808defd437954470f9d7d"),
        name: '홍길동',
        gender: 'M',
        membershipDate: '2023-12-02',
        delYn: false
      }
    ],
    name: '홍길동',
    gender: 'M'
  }
]

마지막으로 정말 출력할 필드만 $project 로 정의해주면 된다.

db.sampleData.aggregate([
    {
        $addFields : {
            memberObjectId : {
                $toObjectId : "$memberId"
            }
        }
    },
    {
        $lookup : {
            from : "member",
            localField : "memberObjectId",
            foreignField : "_id",
            as: "member_info"
        }
    },
    {
        $addFields: {
            name : {
                $arrayElemAt : [ "$member_info.name", 0 ]
            },
            gender : {
                $arrayElemAt : [ "$member_info.gender", 0 ]
            }
        }
    },
    {
        $project : {
            memberId : 1,
            paymentDate : 1,
            amount : 1,
            name : 1,
            gender : 1
        }
    }
])

// 출력결과
[
  {
    _id: ObjectId("65c8df8b20d712293ea1d0f1"),
    memberId: '655808defd437954470f9d7d',
    paymentDate: '2024-02-09',
    amount: 90000,
    name: '홍길동',
    gender: 'M'
  }
]

더 보면 좋을 글들