Mongo DB Aggregation Pipeline - SpringBoot에서 사용하기

SpringBoot에서 MongoDB 간단설정하기

Maven

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
  • Spring-boot-starter-data-mongodb Dependency로 간단하게 mongoldb 관련 모든 라이브러리를 로드 할 수 있습니다.

application.yml

1
2
3
4
5
6
spring:
data:
mongodb:
uri: mongodb://127.0.0.1:27017/employee-test
username: myUser
password: myUserIsCarrey
  • SpringBoot에서는 단순하게 application.yml에 connection-uri정보만 있으면 Spring Boot 서버 시작 시
    MongoDB와 연결할 수 있습니다.

Mongo DB Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {

@Value("${spring.data.mongodb.uri}")
private String url;
@Value("${spring.data.mongodb.username}")
private String username;
@Value("${spring.data.mongodb.password}")
private String password;

@Override
public MongoClient mongoClient() {
MongoClientURI mongoClientURI = new MongoClientURI(this.url);
MongoCredential mongoCredential =
MongoCredential.createCredential(this.username, mongoClientURI.getDatabase(),this.password.toCharArray());
ServerAddress serverAddress = new ServerAddress(mongoClientURI.getHosts().get(0));
MongoClientOptions options = MongoClientOptions.builder().build();
return new MongoClient(serverAddress, mongoCredential, options);
}

@Override
protected String getDatabaseName() {
return new MongoClientURI(this.url).getDatabase();
}
}

  • MongoDB Client 객체를 생성하는 코드를 추가하였습니다.
  • credential을 사용하는 경우 저런식으로 credential 객체를 만들어줘야 합니다.
  • 그렇지 않으면 Command aggregate failed: not authorized on DB to execute command 에러가 발생하며 Mongo DB 기능을 이용할 수 없습니다.

예제로 풀어보는 Mongo DB Aggregation pipeline

예제로 등록한 employee collection은 오라클 DB의 예제 DB인 employee 테이블을 차용하였습니다.

문제 1.

20 번 및 30 번 부서에서 근무하는 모든 사원들의 ENAME 집합과 부서번호를 출력하라

Mongo DB Aggregation pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
db.employee.aggregate(
[
{
$match : {
deptId : {
$in: [20, 30]
}
}
},
{
$group : {
_id: "$deptId",
enames: {
$addToSet: "$ename"
}
}
},
{
$project : {
_id : 0,
deptId : "$_id",
enames : 1
}
}
]
);

Spring Data MongoDB를 이용한 Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Autowired
private MongoTemplate mongoTemplate;

/**
* 20 번 및 30 번 부서에서 근무하는 모든 사원들의 ENAME 집합과 부서번호를 출력하라
* @param deptNos
* @return
*/
public List<UserListByDeptNo> getUserListByDeptNo(List<Integer> deptNos) {

//match
Criteria criteria = new Criteria().where("deptId").in(deptNos);
MatchOperation matchOperation = Aggregation.match(criteria);

//group
GroupOperation groupOperation = Aggregation.group("deptId")
.addToSet("ename").as("enames");

//projection
ProjectionOperation projectionOperation = Aggregation.project("enames")
.and(previousOperation()).as("deptId");

//aggrgation
AggregationResults<UserListByDeptNo> aggregate =
this.mongoTemplate.aggregate(newAggregation(matchOperation, groupOperation, projectionOperation),
Employee.class,
UserListByDeptNo.class);

return aggregate.getMappedResults();
}

문제2

부서별 연봉합계 순위를 랭킹하여라

Mongo DB Aggregation pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
db.employee.aggregate(
[
{
"$group" : {
"_id" : { "deptId" : "$deptId" , "deptName" : "$deptName"},
"totalSal" : { "$sum" : "$sal"}
}
} ,
{ "$sort" : { "totalSal" : -1}} ,
{
"$project" : {
"deptId" : "$_id.deptId" ,
"deptName" : "$_id.deptName" ,
"totalSal" : 1
}
}
]
)

Spring Data MongoDB를 이용한 Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public List<SalRankByDept> getSalRankByDepts() {

//Group
GroupOperation groupOperation = Aggregation.group("deptId", "deptName")
.sum("sal").as("totalSal");
//Sort
SortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "totalSal");

//Projection
ProjectionOperation projectionOperation = Aggregation.project("deptId", "deptName","totalSal");

//aggrgation
AggregationResults<SalRankByDept> aggregate =
this.mongoTemplate.aggregate(newAggregation(groupOperation, sortOperation, projectionOperation),
Employee.class,
SalRankByDept.class);

return aggregate.getMappedResults();
}

삽질 끝판왕 도전기

위의 예제 코드 2개를 보면 특이한 점이 한가지 있습니다.
바로 Projection Operation 부분이 살짝 다른데요.

1
2
 //projection
ProjectionOperation projectionOperation = Aggregation.project("enames").and(previousOperation()).as("deptId");
1
2
//Projection
ProjectionOperation projectionOperation = Aggregation.project("deptId", "deptName","totalSal");

위의 코드에서는 grouping key에 대해 조회 할 때

.and(previousOperation()).as("deptId") 라고 and 메서드에 previousOperation() 라는 메서드를 적어주었습니다.
두번째 코드에서는 그냥 grouping Key를 projection field에 나열하였습니다.

무슨 차이일까요?

  • 첫번째 코드는 grouping key가 1개이다.
  • 두번째 코드는 grouping key가 2개이다.

위와 같은 차이가 있습니다.

만약에 1번 코드를 2번처럼 사용한다면

1
ProjectionOperation projectionOperation = Aggregation.project("deptId", "enames");

이와 같이 사용할 수 있을 것입니다.
위의 코드로 수정하고 프로그램을 실행해 보면…

1
Caused by: java.lang.IllegalArgumentException: Parameter deptId must not be null!

라는 메세지가 나오면서 런타임 예외를 던집니다.
뭐 때문에 나는 발생하는 예외 일까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public Document toDocument(AggregationOperationContext context) {
Document operationObject = new Document();
if (this.idFields.exposesNoNonSyntheticFields()) {
operationObject.put("_id", (Object)null);
} else if (this.idFields.exposesSingleNonSyntheticFieldOnly()) {
FieldReference reference = context.getReference((Field)this.idFields.iterator().next());
operationObject.put("_id", reference.toString());
} else {
Document inner = new Document();
Iterator var4 = this.idFields.iterator();

while(var4.hasNext()) {
ExposedField field = (ExposedField)var4.next();
FieldReference reference = context.getReference(field);
inner.put(field.getName(), reference.toString());
}

operationObject.put("_id", inner);
}

Iterator var8 = this.operations.iterator();

while(var8.hasNext()) {
GroupOperation.Operation operation = (GroupOperation.Operation)var8.next();
operationObject.putAll(operation.toDocument(context));
}

return new Document("$group", operationObject);
}

원인은 GroupOperation 내부에 있습니다.
Aggregation 클래스들의 toDocument 메서드는 MongoDB에 보낼 실제 명령어 구조를 짜는 메서드입니다.

  • 첫 번째 if 조건인 this.idFields.exposesNoNonSyntheticFields()은 idFields.isEmpty() 인 경우를 체크 합니다.
    • Group key가 없는 경우는 _id 필드가 null이 되어집니다.
  • 두 번째 if 조건인 this.idFields.exposesSingleNonSyntheticFieldOnly() 은 group key가 1개인 경우를 체크 합니다.
    • group key가 1개인 경우에는 group key 필드에 대한 필드명이 _id 로 표현되어 다음 파이프라인에서 사용됩니다.
    • operationObject.put("_id", reference.toString()); 여기서 _id에 대한 타입은 String 으로 결정나기 때문에
      다음 파이프라인에서 _id.field와 같은 행위를 할 수 없게 됩니다.
    • 따라서 {_id : “$deptId”}로 BSON이 생성되기 때문에 다음 파이프라인인 Projection에서는 deptId인 필드는 찾을 수 없게 되는 것입니다.
  • 마지막 else 조건은 group key가 여러 개인 경우에 대해 Document 객체에 key값을 매핑시킵니다.
    • 다음 파이프라인에서 _id.field와 같은 형태로 접근이 가능하기 때문에 group key가 여러개 인 경우에는
      projection operation에서 group key만 써줘도 자연스럽게 표현이 됩니다.
    • 내부적으로는 {deptId : "$_id.deptId", deptName: "$_id.deptName"} 과 같은 형태로 사용 됩니다.