Menu

[java] MongoTemplate 을 이용한 Mongodb join 예

mongodb를 사용하는 spring boot 기반 프로젝트 진행 중 join 에 대해서 rdb 와 달라 정리하고자 한다.
mongodb 의 data는 다음과 같다.

Data

[users] collection

{
  _id: ObjectId("612da21a9a29cfeea9a7a74d"),
  tntId: ObjectId("61276d4c7790a01da4bac0fd"),
  name: "아무개1",
  groupId: ObjectId("612d79f11e9f447e0e046ec8"),
  ...
}

[group] collection

{
    _id: ObjectId("612d79f11e9f447e0e046ec4"),
    tntId: ObjectId("61276d4c7790a01da4bac0fd"),
    groupName: "임시그룹1"
}

[route] collection


    _id: ObjectId("612dc12ea45efb224a986ce5"),
    tntId: ObjectId("61276d4c7790a01da4bac0fd"), 
    target: { 
        route "test", 
        list: \["612da21a9a29cfeea9a7a74d"\] // users._id 값들이 String 타입으로 추가됨. 
    } 
    ... 
}

위에서 보는 것처럼 users, group, target 이라는 3개의 collection 이 있고 targettarget.list 에는 users._id 값이 String 타입으로 추가되는 구조이다.

target.list 를 기준으로 users._id, users.name, group._id, group.groupName 을 가져오는 mongotemplate을 사용한 java 소스는 다음과 같다.

MongoTemplate 을 사용한 java 소스

public List getTargetViewList(String _id) {
        // mongodb routes 와 group 을 join
        // sql => select * from (select u.*, g.* from users u left outer join group g on u.groupId = g._id) agg_groups
        LookupOperation lookupOperation = LookupOperation.newLookup()
                        .from("group")
                        .localField("groupId")
                        .foreignField("_id")
                        .as("agg_groups");

        // agg_groups 가 list 로 나와서 object 로 변환
        //  (user 의 group 은 1:1 매핑)
        // {
        //   "_id": xxx,
        //   "name": xxx,
        //   "agg_groups: [ { ... } ] // => "agg_group": { ... }
        // }
        ProjectionOperation projectionOperation =
                Aggregation.project("agg_groups")
                        .andInclude("_id")
                        .andInclude("name")
                        .and(ArrayOperators.ArrayElemAt.arrayOf("agg_groups").elementAt(0)).as("agg_group")
                ;

        // user 와 group을 합친 결과에서
        // target list 에 있는 user._id 와 매치. 즉, in 절 추가
        // sql => select * from ( agg_group ) where id in ( target_list )        
        List targetUserIdList = mongoTemplate.findById(_id, Target.class, "target").getList();
        MatchOperation matchOperation = Aggregation.match(Criteria.where("_id").in(targetUserIdList));
 
        // 위 형태에서 필요한 정보를 한 object 로 변경
        // {
        //    "user_id": xxx,Routeroute
        //    "user_name": xxx,
        //    "group_id": xxx,
        //    "group_name: xxx
        // }
        ProjectionOperation convertAggGroupsOperation =
                Aggregation.project()
                        .and("_id").as("user_id")
                        .andExclude("_id")  // _id 와 user_id 가 중복되므로 _id 는 제거
                        .and("name").as("user_name")
                        .and("agg_group._id").as("group_id")
                        .and("agg_group.groupName").as("group_name")
                ;

        // 조건을 순서대로 대입하여 mongodb 쿼리 생성
        Aggregation aggregation = Aggregation.newAggregation(
                lookupOperation,
                projectionOperation,
                matchOperation,
                convertAggGroupsOperation
        );
        
        // 기준이 되는 users 를 여기다가 넣기
        return mongoTemplate.aggregate(aggregation, "users", HashMap.class).getMappedResults();
    }

위 소스 중 Route.classroute collection 을 담기 위한 entity class 이다.

@Document("route")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Schema  // swagger
class Route{
    @Id
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    @Schema(description = "Object ID", required = true)
    private ObjectId _id;
    
    @Schema(description = "소속 테넌트 ID", required = true)
    private ObjectId tntId;
    
    private Target target;    
}

@Getter
@Setter
@Schema  // swagger
class Target {
    private String kind;
    private List list;
}

이렇게 원하는 결과를 만들어 냈다.
(이 글을 보는 분들 중 제 글에 잘못되거나 태클 꺼리가 보이실 경우 생각하시는 바가 맞을겁니다...)

출처