이번에 교회 서비스를 개발하면서 MongoDB를 사용했는데요.
SQL 문법에 익숙했는데 새로운 문법을 배우니 기록해두면 좋겠다는 생각이 들었습니다. 스키마 관리를 위해 Mongoose를 추가했습니다. DB 생성과 설정하는 방법은 이전 포스팅을 참고해서 진행하실 수 있습니다.
SQL 문법과 비교해서 하나씩 살펴보겠습니다.
예제에서 사용될 컬렉션은 다음과 같습니다.
사용자 : users
export const UserScheme = new Schema({
_id: { type: ObjectId },
userName: { type: String },
roleCode: { type: String },
createDate: { type: Date, default: Date.now },
});
역할 : roles
export const RoleScheme = new Schema({
_id: { type: ObjectId },
roleName: { type: String },
});
INSERT
먼저 roles 컬렉션에 데이터를 삽입해보겠습니다.
데이터를 삽입하는 방법으로는 단일 삽입과 다중 삽입이 있습니다.
단일 삽입
단일 삽입 명령어로는 save()를 사용할 수 있습니다. 삽입하려는 객체 생성 후 save()를 하게 되면 데이터가 저장됩니다.
const role = new Roles({
_id: new mongoose.Types.ObjectId(),
roleName: "관리자",
});
await role.save();
다중 삽입
N개의 데이터의 삽입이 필요한 경우 bulkWrite()를 사용할 수 있습니다.
bulkWrite()는 삽입 뿐 아니라 수정, 삭제까지 지원됩니다. 다음과 같이 배열로 작성하여 사용합니다.
await Roles.bulkWrite([
{
insertOne: {
document: {
_id: new mongoose.Types.ObjectId(),
roleName: "중간관리자",
},
},
},
{
insertOne: {
document: {
_id: new mongoose.Types.ObjectId(),
roleName: "사용자",
},
},
},
]);
UPDATE
이번에는 저장된 데이터의 변경을 알아보겠습니다.
데이터 변경으로 updateOne()를 사용할 수 있습니다. 첫 번째 인자값에는 where 절에 해당하는 조건을 넣게 되고, 두 번째 인자값의 $set 안에는 변경할 값을 넣습니다.
조건이 여러개인 경우 { _id: new ObjectId(`abcd`), roleName: "사용자" } 와 같은 형태로 사용할 수 있고, 값을 여러개 변경하게 된다면 { $set: {a : "foo", b : "bar"} } 와 같은 형태로 사용할 수 있습니다.
await Roles.updateOne(
{ _id: new ObjectId(`65dc8d3b3832911cb8056337`) },
{
$set: { roleName: "일반사용자" },
}
);
MERGE
SQL MERGE문과 같은 기능을 하는 명령문으로 updateOne()을 사용할 수 있습니다. 이 때는 upsert 옵션을 사용하게 되고 값을 true로 하면 됩니다.
await Roles.updateOne(
{
_id: new mongoose.Types.ObjectId(),
},
{
$set: {
roleName: "테스터",
},
},
{
upsert: true,
}
);
DELETE
데이터를 삭제하는 경우 deleteOne()를 사용할 수 있습니다.
await Roles.deleteOne({
_id: new ObjectId(`65dc90043832911cb805633a`),
});
SELECT
이제 저장된 데이터를 조회해보겠습니다.
데이터를 조회하는 경우 find()를 사용할 수 있습니다. 여기에 체이닝 되어 있는 lean() 함수는 성능을 개선시키는 함수로 사용됐습니다.
Mongoose의 리턴 값에는 Document 클래스의 인스턴스가 존재하는데 이를 통해 get(), set(), save() 등 여러 함수의 사용이 가능합니다. 하지만 조회 결과만 필요로 할 때는 이러한 리턴 값이 필요하지 않습니다. 이 때, lean() 함수를 체이닝하게 되면 불필요한 데이터 없이 POJO(Plain Old Javascript Object)를 리턴하게 되어 성능에 큰 이점을 가져옵니다.
// 데이터 전체 조회 (좌)
const roleList = await Roles.find().lean();
// 역할명으로 정렬 (우)
const roleList = await Roles.find().lean().sort({ roleName: 1 });
조회 결과에 특정 필드만 가져오고 싶은 경우 find 함수의 두 번째 인자값으로 조회될 필드를 명시합니다. 조회될 필드에는 1값을 주고 필요하지 않는 필드는 생략합니다. _id는 0으로 명시해줘야 조회되지 않습니다.
const roleList = await Roles.find(
{},
{
_id: 0,
roleName: 1,
}
)
.lean()
.sort({ roleName: 1 });
특정 필드의 조건 검색을 하고 싶은 경우 find 함수의 첫 번째 인자값에 입력합니다.
SQL LIKE 문과 같이 문자열을 포함하는 데이터를 찾고 싶은 경우에는 $regex 옵션을 사용합니다.
// 역할명이 관리자인 데이터 조회 (좌)
const roleList = await Roles.find({ roleName: "관리자" }).lean();
// 역할명이 관리를 포함하는 데이터 조회 (우)
const roleList = await Roles.find({ roleName: { $regex: "관리" } }).lean();
JOIN
users 컬렉션에 데이터를 입력하고 roles 컬렉션과 조인해보겠습니다.
조인의 기준이 되는 필드는 roleCode라는 필드입니다. users 컬렉션에서는 roles 컬렉션의 _id를 String 형태로 저장했습니다.
조인시에 aggeregate()를 사용할 수 있습니다. 여기서 userName이 에디인 사용자의 roleName이 무엇인지 확인하는 명령문을 만들어보겠습니다.
첫번째 인자값에는 where 절에 해당하는 조건을 입력할 수 있습니다. $match 안에 검색할 필드명과 값을 입력합니다.
두번째 인자값에서는 $addFields - $toObjectId를 통해 users 컬렉션의 roleCode를 ObjectId로 변환하는 작업을 한 뒤 roleCode로 반환했습니다. 이 값을 통해 roles 컬렉션의 _id와 비교하게 됩니다.
세번째 인자값에 $lookup을 통해 roles 컬렉션과 users 컬렉션의 roleCode를 비교하게 됩니다.
from은 조인할 컬렉션 roles을 입력합니다.
localField에는 기준 컬렉션인 users의 roleCode를 입력합니다.
foreignField에는 roles 컬렉션의 _id(기준 컬렉션과 비교할 필드)를 입력합니다.
as는 별칭을 의미합니다. 다섯번째 인자값에 사용된 $project의 "$roles.roleName"가 여기에 정의된 별칭을 의미합니다.
네번째 인자값에 사용된 $unwind는 조인된 데이터가 배열로 되는 것을 풀어서 보여줍니다. (아래 좌측 이미지는 unwind가 사용되지 않은 경우입니다)
다섯번째 인자값에 $project를 통해 출력할 데이터를 정의합니다. 기준 컬렉션의 데이터는 0과 1로 구분합니다.(0:조회X, 1:조회O)
const user = await Users.aggregate([
{
$match: {
userName: "에디",
},
},
{
$addFields: { roleCode: { $toObjectId: "$roleCode" } },
},
{
$lookup: {
from: "roles",
localField: "roleCode",
foreignField: "_id",
as: "roles",
},
},
{ $unwind: "$roles" },
{
$project: {
_id: 1,
userName: 1,
roleName: "$roles.roleName",
},
},
]);
return Response.json({
message: "OK",
result: user,
});