MongoDB를 쓰는 가장 큰 이유는 객체지향 프로그래밍 모델을 Document에 그대로 사용할 수 있다는 장점 때문이다. JDBC에서 ORM을 따로 쓰는 것처럼 복잡한 설정 필요 없이 간단하게 POJO class를 Collection의 Document에 매핑할 수 있다.
import lombok.*;
import org.bson.codecs.pojo.annotations.BsonId;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.types.ObjectId;
import java.time.LocalDateTime;
// 모든 필드를 파라미터로 갖는 생성자
@AllArgsConstructor
// 아무 파라미터도 받지 않는 생성자
@NoArgsConstructor
// 필드를 가져오는 getter 함수
@Getter
// 필드를 설정하는 setter 함수
@Setter
// 출력을 용이하게 하는 함수
@ToString
public class Product {
@BsonId
ObjectId id;
String name;
@BsonProperty("updated_at")
LocalDateTime updatedAt;
String contents;
int Price;
}
collection 객체에 insertOne 또는 insertMany 함수에 객체를 통채로 넘겨준다. 이것이 가능하려면 먼저 POJO를 해석할 수 있는 Codec이 등록되어있어야 한다.
import com.mongodb.client.result.InsertManyResult;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// collection 이 다루는 class를 Document -> Product 변경
// getCollection은 class 를 입력하면 TDocument를 return 해준다. getCollection( collectionName , class)
MongoCollection<Product> collection = mongoDatabase.getCollection("products", Product.class);
// DateTime 을 어떻게 포매팅할지
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
List<Product> products = new ArrayList<>();
// 데이터 insert
// _id 는 자동으로 생성
products.add(new Product(null, "shoes1", LocalDateTime.parse("2022-08-01 01:00:00", formatter),
"This is shoes1", 10000));
products.add(new Product(null, "shoes2", LocalDateTime.parse("2022-08-01 02:00:00", formatter),
"This is shoes2", 20000));
products.add(new Product(null, "shoes3", LocalDateTime.parse("2022-08-01 03:00:00", formatter),
"This is shoes3", 30000));
products.add(new Product(null, "shoes4", LocalDateTime.parse("2022-08-01 04:00:00", formatter),
"This is shoes4", 40000));
products.add(new Product(null, "shoes5", LocalDateTime.parse("2022-08-01 05:00:00", formatter),
"This is shoes5", 50000));
products.add(new Product(null, "shoes6", LocalDateTime.parse("2022-08-01 06:00:00", formatter),
"This is shoes6", 60000));
products.add(new Product(null, "backpack", LocalDateTime.parse("2022-08-02 04:00:00", formatter),
"This is backpack", 50000));
products.add(new Product(null, "shirt", LocalDateTime.parse("2022-08-03 05:00:00", formatter),
"This is shirt", 20000));
products.add(new Product(null, "glasses", LocalDateTime.parse("2022-08-04 06:00:00", formatter),
"This is glasses", 10000));
InsertManyResult insertManyResult = collection.insertMany(products);
// 내가 보낸 쿼리의 Acknowleged가 True 라면 = 응답이 왔다면
if(insertManyResult.wasAcknowledged()) {
// insert 된 ID를 전달받아 출력
System.out.println("Inserted ids : " + insertManyResult.getInsertedIds());
}
}
}
9개의 데이터가 올바르게 들어간 것을 확인할 수 있다.
find()
함수로 검색을 할 수 있다. 검색 결과는 MongoIterator인데, JDBC와 유사하게 cursor 방식으로 데이터를 확인할 수 있다.
import com.mongodb.client.*;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import java.time.format.DateTimeFormatter;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static com.mongodb.client.model.Filters.regex;
import static com.mongodb.client.model.Projections.include;
import static com.mongodb.client.model.Sorts.descending;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
import static com.mongodb.client.model.Filters.eq;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// collection 이 다루는 class를 Document -> Product 변경
// getCollection은 class 를 입력하면 TDocument를 return 해준다. getCollection( collectionName , class)
MongoCollection<Product> collection = mongoDatabase.getCollection("products", Product.class);
// DateTime 을 어떻게 포매팅할지
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(" --- cursor --- ");
System.out.println();
// JDBC의 커서와 동일한 형태의 Mongo Cursor 를 반환
MongoCursor<Product> cursor = collection.find().cursor();
// 커서의 다음이 가르킬 곳이 True라면
while(cursor.hasNext()) {
// 커서를 다음으로 이동
System.out.println(cursor.next());
}
System.out.println();
System.out.println(" ---- find eq ----- ");
System.out.println();
// price 필드의 값이 10000 인 값을 조회.
MongoCursor<Product> cursor2 = collection.find(eq("price",10000)).cursor();
while (cursor2.hasNext()) {
System.out.println(cursor2.next());
}
System.out.println();
System.out.println(" ---- find regex(like) + sort ----- ");
System.out.println();
// regular expression / name 필드에 shoes가 포함된 모든 레코드 조회
// sort.descending 을 import / indexes.descending 은 텍스트 관련 정렬
MongoCursor<Product> cursor3 = collection.find(regex("name","shoes")).sort((descending("price"))).cursor();
while (cursor3.hasNext()) {
System.out.println(cursor3.next());
}
System.out.println();
System.out.println(" ---- projection 원하는 데이터만 조회 (다른 필드값은 기본값으로 조회) ----- ");
System.out.println();
MongoCursor<Product> cursor4 = collection.find(regex("name","shoes")).projection(include("name")).sort((descending("price"))).cursor();
while (cursor4.hasNext()) {
System.out.println(cursor4.next());
}
}
}
--- cursor ---
Product(id=639489cb3e8aff05670e251b, name=shoes1, updatedAt=2022-08-01T01:00, contents=This is shoes1, Price=10000)
Product(id=639489cb3e8aff05670e251c, name=shoes2, updatedAt=2022-08-01T02:00, contents=This is shoes2, Price=20000)
Product(id=639489cb3e8aff05670e251d, name=shoes3, updatedAt=2022-08-01T03:00, contents=This is shoes3, Price=30000)
Product(id=639489cb3e8aff05670e251e, name=shoes4, updatedAt=2022-08-01T04:00, contents=This is shoes4, Price=40000)
Product(id=639489cb3e8aff05670e251f, name=shoes5, updatedAt=2022-08-01T05:00, contents=This is shoes5, Price=50000)
Product(id=639489cb3e8aff05670e2520, name=shoes6, updatedAt=2022-08-01T06:00, contents=This is shoes6, Price=60000)
Product(id=639489cb3e8aff05670e2521, name=backpack, updatedAt=2022-08-02T04:00, contents=This is backpack, Price=50000)
Product(id=639489cb3e8aff05670e2522, name=shirt, updatedAt=2022-08-03T05:00, contents=This is shirt, Price=20000)
Product(id=639489cb3e8aff05670e2523, name=glasses, updatedAt=2022-08-04T06:00, contents=This is glasses, Price=10000)
---- find eq -----
Product(id=639489cb3e8aff05670e251b, name=shoes1, updatedAt=2022-08-01T01:00, contents=This is shoes1, Price=10000)
Product(id=639489cb3e8aff05670e2523, name=glasses, updatedAt=2022-08-04T06:00, contents=This is glasses, Price=10000)
---- find regex(like) + sort -----
Product(id=639489cb3e8aff05670e2520, name=shoes6, updatedAt=2022-08-01T06:00, contents=This is shoes6, Price=60000)
Product(id=639489cb3e8aff05670e251f, name=shoes5, updatedAt=2022-08-01T05:00, contents=This is shoes5, Price=50000)
Product(id=639489cb3e8aff05670e251e, name=shoes4, updatedAt=2022-08-01T04:00, contents=This is shoes4, Price=40000)
Product(id=639489cb3e8aff05670e251d, name=shoes3, updatedAt=2022-08-01T03:00, contents=This is shoes3, Price=30000)
Product(id=639489cb3e8aff05670e251c, name=shoes2, updatedAt=2022-08-01T02:00, contents=This is shoes2, Price=20000)
Product(id=639489cb3e8aff05670e251b, name=shoes1, updatedAt=2022-08-01T01:00, contents=This is shoes1, Price=10000)
---- projection 원하는 데이터만 조회 다른 조회는 더미값 -----
Product(id=639489cb3e8aff05670e2520, name=shoes6, updatedAt=null, contents=null, Price=0)
Product(id=639489cb3e8aff05670e251f, name=shoes5, updatedAt=null, contents=null, Price=0)
Product(id=639489cb3e8aff05670e251e, name=shoes4, updatedAt=null, contents=null, Price=0)
Product(id=639489cb3e8aff05670e251d, name=shoes3, updatedAt=null, contents=null, Price=0)
Product(id=639489cb3e8aff05670e251c, name=shoes2, updatedAt=null, contents=null, Price=0)
Product(id=639489cb3e8aff05670e251b, name=shoes1, updatedAt=null, contents=null, Price=0)
Process finished with exit code 0
업데이트는 대상을 고르는 filter, 내용인 update 이 두 종류의 BSON parameter를 이용해서 update 한다. 조건은 fields method 로 여러개를 이어 붙일 수 있다.
update는 set, unset, set on insert, increment, multiply, rename, min, max, current date 등 타입 안정성을 제공하는 전용 함수들이
많다. 참고
name
필드에 shoes가 포함 된 레코드에서 price
필드가 10000원보다 큰 레코드의 price
에 0.9배를 해준다.
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.internal.bulk.UpdateRequest;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import java.time.format.DateTimeFormatter;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static com.mongodb.client.model.Filters.gt;
import static com.mongodb.client.model.Filters.regex;
import static com.mongodb.client.model.Projections.fields;
import static com.mongodb.client.model.Updates.mul;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// collection 이 다루는 class를 Document -> Product 변경
// getCollection은 class 를 입력하면 TDocument를 return 해준다. getCollection( collectionName , class)
MongoCollection<Product> collection = mongoDatabase.getCollection("products", Product.class);
// updateMany 의 return type은 UpdateResult 이다.
// fields를 사용하여 여러 조건을 넣을 수 있다
// regex = regular expression , gt = greater than
// name 필드에 shoes가 들어간 레코드에 price 필드가 10000원보다 큰 레코드의 price에 0.9배를 해준다.
UpdateResult updateResult = collection.updateMany(fields(regex("name","shoes"),
gt("price",10000)),
mul("price",0.9));
// 쿼리에 응답이 있다면
if(updateResult.wasAcknowledged()) {
// update 된 개수를 출력
System.out.println("modified count : " + updateResult.getModifiedCount());
}
}
}
modified count : 5
Process finished with exit code 0
원래 데이터
바뀐 데이터
삭제는 filter 조건만 넣으면 삭제가 된다.
연관된 document가 있는 경우 cascading 옵션이 없으므로 삭제시 참조하는 데이터를 어떻게 처리할지를 고민해서 자동화할 수 있도록 해야한다.
(cascading 자동화 코드를 작성해 놓으면 편하다.)
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.internal.bulk.DeleteRequest;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import java.time.format.DateTimeFormatter;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Projections.fields;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// collection 이 다루는 class를 Document -> Product 변경
// getCollection은 class 를 입력하면 TDocument를 return 해준다. getCollection( collectionName , class)
MongoCollection<Product> collection = mongoDatabase.getCollection("products", Product.class);
// name 에 shoes 가 포함되어 있고, price가 10000원 이상이고, price가 20000원 미만인 레코드를 삭제
// 10000 <= price < 20000
DeleteResult deleteResult = collection.deleteMany(fields(regex("name", "shoes"),
gte("price", 10000),
lt("price", 20000)
));
if(deleteResult.wasAcknowledged()) {
System.out.println("delete : " + deleteResult.getDeletedCount());
}
}
}
delete : 2
Process finished with exit code 0
원래 데이터
바뀐 데이터
MongoDB 에서 집계를 위한 연산을 Aggregation Pipeline 기능을 통해서 할 수 있다.
Aggregation 에서 사용하는 대표적인 기능은 다음 3종류이다.
$match
: 원하는 조건에 맞는 document 만 고른다. SQL의 WHERE 조건절이라고 생각하면 된다.$group
: 어느 단위로 묶어서 집계를 할지 정한다. SQL의 group by 라고 생각하면 된다.$sort
, $limit
, $count
: 집계 결과에 대해서 한정한다.$group
의 효과적인 확인을 위해 데이터를 추가해준다.
원래 데이터
바뀐 데이터 (위에 Create 재활용)
import com.mongodb.client.*;
import com.mongodb.client.model.Accumulators;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.Sorts;
import org.bson.Document;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static com.mongodb.client.model.Filters.gt;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// POJO classs 를 쓰면 안되고 일반 Document class 를 사용해야 함
// collection에 있는 데이터만 가져오는게 아니라 가져온 데이터를 기반으로 avg_price 필드를 만들었기 때문에
// product document 의 형식에 벗어난다. 따라서 product document의 pojo class 매칭을 사용할 수 없고 기본 Document class를 사용해야 한다.
MongoCollection<Document> collection = mongoDatabase.getCollection("products");
// AggregateIterable의 type 을 받아야함
MongoCursor<Document> cursor = collection.aggregate(
Arrays.asList(
// match() 로 필터링 price > 10000
Aggregates.match(gt("price",10000)),
// 필드이름 앞에 $를 붙혀야 해당 필드를 찾을 수 있다. (필드에 조건을 걸려면)
// name 으로 group by
// group by 한 price들의 평균을 구한다 그 필드의 이름은 avg_price로 한다.
Aggregates.group("$name", Accumulators.avg("avg_price", "$price")),
Aggregates.sort(Sorts.descending("avg_price"))
)
// .iterator() 추가
).iterator();
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
}
name
필드를 기준으로 Group by 되었으며 각 그룹의 평균 가격을 avg_price
필드에서 나타냈다.
Aggregation 을 활용할 때는 모든 Document type 을 POJO class 로 만들어 놓지 않는 이상 필드의 타입과 이름을 외워서 조회하는 수 밖에 없다.
Document{{_id=glasses, avg_price=100000.0}}
Document{{_id=backpack, avg_price=65000.0}}
Document{{_id=shoes6, avg_price=62000.0}}
Document{{_id=shirt, avg_price=55000.0}}
Document{{_id=shoes5, avg_price=52500.0}}
Document{{_id=shoes4, avg_price=43000.0}}
Document{{_id=shoes3, avg_price=33500.0}}
Document{{_id=shoes2, avg_price=30000.0}}
Document{{_id=shoes1, avg_price=20000.0}}
Process finished with exit code 0