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
6spring:
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
public class MongoConfiguration extends AbstractMongoConfiguration {
"${spring.data.mongodb.uri}") (
private String url;
"${spring.data.mongodb.username}") (
private String username;
"${spring.data.mongodb.password}") (
private String password;
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);
}
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
26db.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
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
18db.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
19public 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
29public 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"}
과 같은 형태로 사용 됩니다.
- 다음 파이프라인에서 _id.field와 같은 형태로 접근이 가능하기 때문에 group key가 여러개 인 경우에는