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; public List<UserListByDeptNo> getUserListByDeptNo (List<Integer> deptNos) { Criteria criteria = new Criteria ().where("deptId" ).in(deptNos); MatchOperation matchOperation = Aggregation.match(criteria); GroupOperation groupOperation = Aggregation.group("deptId" ) .addToSet("ename" ).as("enames" ); ProjectionOperation projectionOperation = Aggregation.project("enames" ) .and(previousOperation()).as("deptId" ); 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 () { GroupOperation groupOperation = Aggregation.group("deptId" , "deptName" ) .sum("sal" ).as("totalSal" ); SortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "totalSal" ); ProjectionOperation projectionOperation = Aggregation.project("deptId" , "deptName" ,"totalSal" ); AggregationResults<SalRankByDept> aggregate = this .mongoTemplate.aggregate(newAggregation(groupOperation, sortOperation, projectionOperation), Employee.class, SalRankByDept.class); return aggregate.getMappedResults(); }
삽질 끝판왕 도전기 위의 예제 코드 2개를 보면 특이한 점이 한가지 있습니다. 바로 Projection Operation 부분이 살짝 다른데요.
1 2 ProjectionOperation projectionOperation = Aggregation.project("enames" ).and(previousOperation()).as("deptId" );
1 2 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"}
과 같은 형태로 사용 됩니다.