나의개발일지
[mongoose] mongoose의 1:N 관계를 설정해보자!!! 본문
본 글은 작성자가 어디선가 주워듣고 이해한 내용들을 개인적인 언어로 작성한 게시물입니다.
잘못된 내용이 존재할 수 있으니, 읽게 되신다면 이점을 감안해 주세요!!!
- 우리는 결국 자신이 가진 이야기로 상대방을 이해할 수 있을 뿐이다.-
웹애플리케이션의 데이터를 다루다보면, 데이터 끼리의 연결이 필요한 경우가 있다. mongoose에서도 데이터끼리의 연결, 다큐먼트끼리의 연결을 돕는 기능이 존재한다. 보통 SQL데이터베이스에 많이 사용되는 객체 간의 관계에는 1:1(일대일), 1:N(일대다), 1:M(다대다)관계가 있다. 오늘은 mongoose에서 다큐먼트끼리의 1:N(일대다)의 관계를 설정하는 방법에 대해서 정리해보고자 한다.
1대N의 관계를 여러개로 나눌 수 있다???
SQL에서 1대N은 단순히 N에 해당하는 부분에서 1에 해당하는 부분의 키를 가지고 있는 형태였다. 예를 들어 하나의 출판사가 있고 여러개의 책이 있을 경우 출판사가 1이 되고 책이 N이 된다. 그러므로 외래키는 책에서 가지고 있었다. mongoose에서는 이와 비슷한 방법을 사용하나 1대N의 관계를 다시 3가지 방법으로 세분화한다. one-to-few, one-to-many, one-to-bajillions관계이다. 각 관계에 따라서 관계의 방식도 달라진다. 관계의 방식은 Embeded방식과 Reference방식으로 나뉜다. Embeded방식은 데이터를 원본 그대로 저장하는 것이라면 Reference방식은 다큐먼트의 Id를 저장해서 참고하는 방식이다. 위의 세분화된 3가지 방식에 따라서 방식이 달라지게되는데 그와 관련해서 천천히 알아보자!!!
one-to-few관계
one-to-few관계는 두개의 다큐먼트가 있다면 참조하는 것이 대량이 아닌 두개이상 여러개일 경우에 해당한다. 예를 들어 한명의 유저는 여러개의 주소를 등록할 수 있다고 한다면, 유저와 주소 다큐먼트를 따로 만드는 것이 아니라, 유저 안에 주소데이터를 Embeded하는 형태이다. 두가지 또는 그 이상의 관계를 저장할 경우 리스트를 중첩으로 데이터를 넣어서 관리할 수 있다. 즉, 직접 임베드하려는 경우는 정보의 집합의 크기가 작을 경우 사용가능한 방법이다.
// 유저스키마
const UserSchema = new mongoose.Schema({
first:String,
last:String,
addresses:[
{
_id:{id:false},
street:String,
city:String,
state:String,
country:String
}
]
})
// 스키마를 이용해서 모델 만들기
const User = mongoose.model("User", UserSchema);
// 유저 만드는 함수
const makeUser = async()=>{
const u = new User({
first:"lee",
last:"amuge",
});
u.addresses.push({
street:"777 street",
city:"7city",
state: "7state",
country:"7country"
})
const res = await u.save();
}
// 주소를 추가해주는 함수
const addAddress = async (id)=>{
const user = await User.findById(id);
user.addresses.push({
street:"999 street",
city:"9city",
state: "9state",
country:"9country"
})
const res = await user.save();
}
스키마에서 정의한데로 Embeded하게 주소를 넣어서 따로 관리하는 것이 아니라 배열로 만들어서 필요에 따라 데이터를 넣어주는 형태이다. 이와 같은 방법은 위에서 말한데로 해당 데이터가 대량이 아닌 두개이상의 다수일 경우만 가능한 방법이다. 만약 데이터가 늘어 난다면 Reference방식으로 다큐먼트를 따로 분리하는 것이 좋다.
one-to-many 관계
one-to-many관계는 부모 문서 안에 정보를 직접적으로 Embeded하는 것이 아니라 참조하는 레퍼런스를 다른 곳에 정의된 다큐먼트에 임베드하거나 저장하는 방법이다. 보통은 객체 id를 쓴다. sql방식과 매우 유사한 방법이다. 이후 populate()메서드를 이용해서 id에 해당하는 데이터를 가지고 올 수 있다.
// 상품스키마작성
const productSchema = new mongoose.Schema({
name: String,
price : Number,
season:{
type:String,
enum:["Spring", "Summer", "Fall", "Winter"]
}
});
// 농장스키마작성
const farmSchema = new mongoose.Schema({
name:String,
city:String,
// 참조하는 것을 알려준다.
// products의 엔티티의 타입은 objectid라는 것을 알려준다.
// ref를 사용해서 populate시 사용할 모델을 알려준다.
// ref로는 모델의 이름을 설정해야한다.
products:[{type: mongoose.Schema.Types.ObjectId, ref:"Product"}]
})
// 모델생성
const Farm = mongoose.model("Farm", farmSchema);
const Product = mongoose.model("Product", productSchema);
// 상품만들기
Product.insertMany([
{name:"melon", price:5.00, season:"Summer"},
{name:"apple", price:3.00, season:"Spring"},
{name:"orange", price:2.00, season:"Fall"},
]);
// 농장만들고 상품 넣어주기
const makeFarm = async()=>{
const farm = new Farm({name:"Very Good Farms", city:"Seoul"});
const apple = await Product.findOne({name:"apple"});
// 상품의 id이외에 전체를 넣어주는 것처럼 보이지만 실제로는 id만 들어간다.
farm.products.push(apple)
await farm.save()
}
// 농장에 상품 추가해주기
const addProduct = async()=>{
const farm = await Farm.findOne({name:"Very Good Farms"});
const melon = await Product.findOne({name:"melon"});
farm.products.push(melon)
await farm.save()
}
위의 코드는 농장과 상품사이의 1:N관계에 해당하며 하나의 농장은 여러개의 상품을 가질 수 있다. 따로 다큐먼트를 만드는 경우는 Reference를 이용해서 다큐먼트를 참고해야 한다. 부모쪽에 해당하는 농장에 ref를 넣어서 농장에 포함된 상품의 정보를 농장을 호출시 함께 받을 수 있다. products:[{type: mongoose.Schema.Types.ObjectId, ref:"Product"}]를 이용해서 상품에 대한 참조를 할 수 있다. 그렇다면 해당 데이터를 어떻게 받아 올 수 있을까? 그 방법은 populate()메서드를 이용하면 된다.
// populate()사용 전
Farm.findOne({name:"Very Good Farms"}).then(farm => console.log(farm))
// {
// _id: new ObjectId("63cce9c701c21f060816c9c3"),
// name: 'Very Good Farms',
// city: 'Seoul',
// products: [
// new ObjectId("63cce53c6acae1628f849c84"),
// new ObjectId("63cce53c6acae1628f849c83")
// ],
// __v: 1
// }
// populate 채워넣기
// populate()사용 후
Farm.findOne({name:"Very Good Farms"})
.populate("products")//필드명
.then(farm => console.log(farm))
// {
// _id: new ObjectId("63cce9c701c21f060816c9c3"),
// name: 'Very Good Farms',
// city: 'Seoul',
// products: [
// {
// _id: new ObjectId("63cce53c6acae1628f849c84"),
// name: 'apple',
// price: 3,
// season: 'Spring',
// __v: 0
// },
// {
// _id: new ObjectId("63cce53c6acae1628f849c83"),
// name: 'melon',
// price: 5,
// season: 'Summer',
// __v: 0
// }
// ],
// __v: 1
// }
populate()메서드를 사용전에는 단순히 어떠한 다큐먼트를 참고하고 있는지 ID정보만 보여주지만, populate()메서드를 적용하면 해당 상품에 대한 정보도 보여준다.
one-to-bajillions관계
one-to-bajillions관계는 하나의 다큐먼트가 대량의 다룬 다큐먼트를 참고하려는 경우에 해당한다. 자식문서나 항목이 아주 많은 경우 그 많은 데이터를 하나의 데이터에 연결하려 경우이다. 한 사용자가 트윗(블로그 글)을 수만개 씩 엄청나게 많이 남긴 경우, 인스타그램에 사진을 엄청 많이 남긴경우에 해당한다. 데이터양이 아주 많을 때는 위와 같이 자식에 대한 레퍼런스를 부모에 저장하는 것보다는 부모에 대한 레퍼런스를 자식에 저장하는 것이 더 효율적일 때가 많다. 즉, 위의 농장과 상품에서 상품의 데이터가 엄청나게 많기 때문에 참조를 상품이 농장을 참조하게 하는 것이다.
const userSchema = new mongoose.Schema({
username:String,
age:Number,
})
// 앞서 farm과 products의 관계에서는 farm이 여러개의 상품을 가지고 있어서 farm에 참조 레퍼런스가 있었으나
// 아래와 같이 대량의 데이터가 필요한 경우는 참조 당하는 쪽에서 레퍼런스를 가지는 것이 효율적이다.
const tweetSchema = new mongoose.Schema({
text:String,
likes:Number,
user: {type: mongoose.Schema.Types.ObjectId, ref:"User2"}
})
const User = mongoose.model("User2", userSchema);
const Tweet = mongoose.model("Tweet", tweetSchema);
const makeTweets = async ()=>{
const user = new User({username:"kim", age:10})
const tweet1 = new Tweet({text:"oh i like code", likes:0});
tweet1.user = user;
await user.save();
await tweet1.save();
}
const makeTweets2 = async ()=>{
const user = User.findOne({username:"kim", age:10})
const tweet2 = new Tweet({text:"second tweet", likes:0});
tweet2.user = user;
await user.save();
await tweet2.save();
}
이 경우는 자식쪽에서 부모를 참조하는 경우로 대량의 데이터가 저장될 경우에 사용하면 효율적이다. 위의 관계 역시 populate()를 이용해서 다른 다큐먼트의 데이터에 대한 접근이 가능하다.
// products항목을 farm에 채워넣기(populate)한 것처럼
// user항목을 tweet에 채워넣기 해본다.
const findTweet = async()=>{
const t = await Tweet.find({}).populate("user")
// user필드에서 username항목만 가지고 온다.
const t2 = await Tweet.find({}).populate("user", "username")
console.log(t)
console.log(t2)
}
// console.log(t)
// [
// {
// _id: new ObjectId("63cd39bbd8c347f944852b3d"),
// text: 'oh i like code',
// likes: 0,
// user: {
// _id: new ObjectId("63cd39bbd8c347f944852b3c"),
// username: 'kim',
// age: 10,
// __v: 0
// },
// __v: 0
// },
// {
// _id: new ObjectId("63cd3a387e10e5264bd72ca1"),
// text: 'second tweet',
// likes: 0,
// user: {
// _id: new ObjectId("63cd39bbd8c347f944852b3c"),
// username: 'kim',
// age: 10,
// __v: 0
// },
// __v: 0
// }
// ]
// console.log(t2)
// [
// {
// _id: new ObjectId("63cd39bbd8c347f944852b3d"),
// text: 'oh i like code',
// likes: 0,
// user: { _id: new ObjectId("63cd39bbd8c347f944852b3c"), username: 'kim' },
// __v: 0
// },
// {
// _id: new ObjectId("63cd3a387e10e5264bd72ca1"),
// text: 'second tweet',
// likes: 0,
// user: { _id: new ObjectId("63cd39bbd8c347f944852b3c"), username: 'kim' },
// __v: 0
// }
// ]
정리하자면
mongoose를 이용한 1대N관계에 대해서 알아보았으나 아직은 완전히 이해가 되지 않은 상태이다. 어떤 경우에 Embeded를 어떤경우에 reference를 이용하면 좋은지 판단하기가 어렵다. 또한 reference시 부모와 자식 중 어느 쪽에서 참조를 하는 것이 좋은지도 명확하지 않다. 데이터를 어떤 식으로 출력할 것인가에 따라서 또는 데이터를 어떻게 사용할 것인가에 따라서 다양하게 달라진다는 것 같은데, 명확한 기준이 잡히지 않는다. 사용하려는 데이터가 어떤지 명확히 한 후 스키마에 대한 정의가 필요해 보인다.
본 글은 작성자가 공부한 내용을 토대로 추가해가면서 정리한 글입니다.
잘못된 내용이 다수 포함 될 수 있습니다.
너무 어렵다 어렵다 어렵다!!!!!!!!!!!!
'NodeJS.Express.MongoDB' 카테고리의 다른 글
| [Express+mongoose] AuthenticationAuthorization (0) | 2023.01.25 |
|---|---|
| [Express] 맛있는 cookie!!!! 멋있는 session!!! (0) | 2023.01.23 |
| [Joi] 유효성 검사를 해 볼까????? 해보자!!!!!! (0) | 2023.01.21 |
| [Express] 알아보자!! Error-handler 사용하자!! Error-handler 포기하자..... (1) | 2023.01.20 |
| [Express] express는 Middleware가 엄청나게 많다. 와 middleware야 너는 정말 좋겠구나 (1) | 2023.01.20 |