A consulta acima retorna documentos que "quase" correspondem a
User
documentos, mas também têm as postagens de cada usuário. Então, basicamente, o resultado é uma série de User
documentos com um Post
array ou slice incorporado . Uma maneira seria adicionar um
Posts []*Post
campo para o User
em si, e teríamos feito:type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
Posts []*Post `bson:"posts,omitempty"`
}
Embora isso funcione, parece "exagero" estender
User
com Posts
apenas por causa de uma única consulta. Se continuarmos por esse caminho, nosso User
type ficaria inchado com muitos campos "extras" para diferentes consultas. Sem contar se preenchermos os Posts
campo e salvar o usuário, essas postagens acabariam salvas dentro do User
documento. Não é o que queremos. Outra maneira seria criar um
UserWithPosts
digite copiando User
, e adicionando um Posts []*Post
campo. Desnecessário dizer que isso é feio e inflexível (qualquer alteração feita em User
teria que ser refletido para UserWithPosts
manualmente). Com incorporação de estrutura
Em vez de modificar o
User
original , e em vez de criar um novo UserWithPosts
type do "scratch", podemos utilizar incorporação de struct
(reutilizando o User
existente e Post
tipos) com um pequeno truque:type UserWithPosts struct {
User `bson:",inline"`
Posts []*Post `bson:"posts"`
}
Observe o valor da tag bson
",inline"
. Isso está documentado em bson.Marshal()
e bson.Unmarshal()
(vamos usá-lo para unmarshaling):Usando a incorporação e o
",inline"
valor da tag, o UserWithPosts
o próprio tipo será um destino válido para unmarshaling de User
documentos e seu Post []*Post
será uma escolha perfeita para os "posts"
pesquisados . Usando isso:
var uwp *UserWithPosts
it := pipe.Iter()
for it.Next(&uwp) {
// Use uwp:
fmt.Println(uwp)
}
// Handle it.Err()
Ou obtendo todos os resultados em uma única etapa:
var uwps []*UserWithPosts
err := pipe.All(&uwps)
// Handle error
A declaração de tipo de
UserWithPosts
pode ou não ser uma declaração local. Se você não precisar dele em outro lugar, pode ser uma declaração local na função em que você executa e processa a consulta de agregação, para que não aumente seus tipos e declarações existentes. Se você quiser reutilizá-lo, você pode declará-lo no nível do pacote (exportado ou não exportado) e usá-lo sempre que precisar. Modificando a agregação
Outra opção é usar o
$replaceRoot
do MongoDB
para "reorganizar" os documentos de resultado, para que uma estrutura "simples" cubra perfeitamente os documentos:// 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",
},
},
},
})
Com este remapeamento, os documentos resultantes podem ser modelados assim:
type UserWithPosts struct {
User *User `bson:"user"`
Posts []*Post `bson:"posts"`
}
Observe que enquanto isso funciona, os
posts
campo de todos os documentos será obtido do servidor duas vezes:uma vez como posts
campo dos documentos retornados, e uma vez como o campo de user
; não mapeamos/usamos, mas está presente nos documentos de resultado. Portanto, se esta solução for escolhida, o user.posts
campo deve ser removido, por exemplo com um $project
palco: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}},
})