Горната заявка връща документи, които "почти" съответстват на User
документи, но имат и публикациите на всеки потребител. Така че основно резултатът е поредица от User
документи с Post
масив или срез вграден .
Един от начините е да добавите Posts []*Post
поле към User
и щяхме да сме готови:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
Posts []*Post `bson:"posts,omitempty"`
}
Въпреки че това работи, изглежда "прекалено" да се разшири User
с Posts
само в името на едно запитване. Ако продължим по този път, нашият User
тип ще се раздуе с много "допълнителни" полета за различни заявки. Да не говорим, ако попълним Posts
поле и запишете потребителя, тези публикации ще бъдат запазени в User
документ. Не е това, което искаме.
Друг начин би бил да създадете UserWithPosts
тип копиране User
и добавяне на Posts []*Post
поле. Излишно е да казвам, че това е грозно и негъвкаво (всички промени, направени в User
трябва да се отрази на UserWithPosts
ръчно).
С вграждане на структура
Вместо да модифицирате оригиналния User
и вместо да създадете нов UserWithPosts
тип от "нулата", можем да използваме вграждане на структура
(повторно използване на съществуващия User
и Post
типове) с малък трик:
type UserWithPosts struct {
User `bson:",inline"`
Posts []*Post `bson:"posts"`
}
Обърнете внимание на стойността на маркера bson
",inline"
. Това е документирано в bson.Marshal()
и bson.Unmarshal()
(ще го използваме за демаршалинг):
Чрез използване на вграждане и ",inline"
стойност на маркера, UserWithPosts
самият тип ще бъде валидна цел за демаршалинг на User
документи и неговия Post []*Post
ще бъде идеален избор за търсените "posts"
.
Използвайки го:
var uwp *UserWithPosts
it := pipe.Iter()
for it.Next(&uwp) {
// Use uwp:
fmt.Println(uwp)
}
// Handle it.Err()
Или получаване на всички резултати в една стъпка:
var uwps []*UserWithPosts
err := pipe.All(&uwps)
// Handle error
Декларацията на типа на UserWithPosts
може или не може да бъде местна декларация. Ако не се нуждаете от него другаде, това може да бъде локална декларация във функцията, където изпълнявате и обработвате заявката за агрегиране, така че няма да раздуе вашите съществуващи типове и декларации. Ако искате да го използвате повторно, можете да го декларирате на ниво пакет (експортиран или неекспортиран) и да го използвате, където имате нужда.
Промяна на агрегацията
Друг вариант е да използвате $replaceRoot
на MongoDB
за да „пренаредите“ резултатите от документите, така че една „проста“ структура ще покрие перфектно документите:
// Query users with their posts:
pipe := collUsers.Pipe([]bson.M{
{
"$lookup": bson.M{
"from": "posts",
"localField": "_id",
"foreignField": "userID",
"as": "posts",
},
},
{
"$replaceRoot": bson.M{
"newRoot": bson.M{
"user": "$$ROOT",
"posts": "$posts",
},
},
},
})
С това пренасочване резултатите могат да бъдат моделирани по следния начин:
type UserWithPosts struct {
User *User `bson:"user"`
Posts []*Post `bson:"posts"`
}
Обърнете внимание, че макар това да работи, posts
полето на всички документи ще бъде извлечено от сървъра два пъти:веднъж като posts
поле на върнатите документи и веднъж като поле на user
; ние не го картографираме/използваме, но той присъства в документите с резултатите. Така че, ако бъде избрано това решение, user.posts
полето трябва да се премахне, напр. с $project
етап:
pipe := collUsers.Pipe([]bson.M{
{
"$lookup": bson.M{
"from": "posts",
"localField": "_id",
"foreignField": "userID",
"as": "posts",
},
},
{
"$replaceRoot": bson.M{
"newRoot": bson.M{
"user": "$$ROOT",
"posts": "$posts",
},
},
},
{"$project": bson.M{"user.posts": 0}},
})