Mgo 聚合:如何重用模型类型来查询和解组 "mixed" 结果?
Mgo aggregation: how to reuse model types to query and unmarshal "mixed" results?
假设我们有 2 个集合:"users"
和 "posts"
,按以下类型建模:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
}
type Post struct {
ID string `bson:"_id"`
UserID string `bson:"userID"`
Content string `bson:"content"`
Date time.Time `bson:"date"`
}
这些可以在存储/检索单个甚至文档集合时使用,例如:
usersColl := sess.DB("").C("users")
postsColl := sess.DB("").C("posts")
// Insert new user:
u := &User{
ID: "1",
Name: "Bob",
Registered: time.Now(),
},
err := usersColl.Insert(u)
// Handle err
// Get Posts in the last 10 mintes:
var posts []*Post
err := postsColl.Find(
bson.M{"date": bson.M{"$gt": time.Now().Add(-10 * time.Minute)}},
).Limit(20).All(&posts)
// Handle err
如果我们使用aggregation to fetch a mixture of these documents? For example Collection.Pipe()
会怎么样:
// Query users with their posts:
pipe := collUsers.Pipe([]bson.M{
{
"$lookup": bson.M{
"from": "posts",
"localField": "_id",
"foreignField": "userID",
"as": "posts",
},
},
})
var doc bson.M
it := pipe.Iter()
for it.Next(&doc) {
fmt.Println(doc)
}
// Handle it.Err()
我们在单个查询中查询用户的帖子。结果是用户和帖子的混合体。我们如何重用 User
和 Post
模型类型,而不必将结果作为 "raw" 文档(bson.M
类型)处理?
上面的查询 returns 文档 "almost" 匹配 User
文档,但它们也有每个用户的帖子。所以基本上结果是一系列 User
文档,带有 Post
数组或切片 embedded.
一种方法是将 Posts []*Post
字段添加到 User
本身,这样我们就完成了:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
Posts []*Post `bson:"posts,omitempty"`
}
虽然这可行,但似乎 "overkill" 将 User
扩展为 Posts
只是为了单个查询。如果我们继续沿着这条路走下去,我们的 User
类型会变得臃肿,因为有很多 "extra" 字段用于不同的查询。更不用说如果我们填写 Posts
字段并保存用户,这些帖子最终会保存在 User
文档中。不是我们想要的。
另一种方法是创建一个 UserWithPosts
类型复制 User
,并添加一个 Posts []*Post
字段。不用说这是丑陋和不灵活的(对 User
所做的任何更改都必须手动反映到 UserWithPosts
)。
使用结构嵌入
我们可以利用 struct embedding(重用现有 User
和 Post
类型)用一个小技巧:
type UserWithPosts struct {
User `bson:",inline"`
Posts []*Post `bson:"posts"`
}
注意 bson tag value ",inline"
. This is documented at bson.Marshal()
and bson.Unmarshal()
(我们将用它来解组):
inline Inline the field, which must be a struct or a map.
Inlined structs are handled as if its fields were part
of the outer struct. An inlined map causes keys that do
not match any other struct field to be inserted in the
map rather than being discarded as usual.
通过使用嵌入和 ",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
的类型声明可能是也可能不是局部声明。如果您在其他地方不需要它,它可以是您执行和处理聚合查询的函数中的局部声明,因此它不会使您现有的类型和声明膨胀。如果你想重用它,你可以在包级别声明它(导出或未导出),并在你需要的地方使用它。
修改聚合
另一种选择是使用 MongoDB 的 $replaceRoot
到 "rearrange" 结果文档,因此 "simple" 结构将完美地覆盖文档:
// 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}},
})
假设我们有 2 个集合:"users"
和 "posts"
,按以下类型建模:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
}
type Post struct {
ID string `bson:"_id"`
UserID string `bson:"userID"`
Content string `bson:"content"`
Date time.Time `bson:"date"`
}
这些可以在存储/检索单个甚至文档集合时使用,例如:
usersColl := sess.DB("").C("users")
postsColl := sess.DB("").C("posts")
// Insert new user:
u := &User{
ID: "1",
Name: "Bob",
Registered: time.Now(),
},
err := usersColl.Insert(u)
// Handle err
// Get Posts in the last 10 mintes:
var posts []*Post
err := postsColl.Find(
bson.M{"date": bson.M{"$gt": time.Now().Add(-10 * time.Minute)}},
).Limit(20).All(&posts)
// Handle err
如果我们使用aggregation to fetch a mixture of these documents? For example Collection.Pipe()
会怎么样:
// Query users with their posts:
pipe := collUsers.Pipe([]bson.M{
{
"$lookup": bson.M{
"from": "posts",
"localField": "_id",
"foreignField": "userID",
"as": "posts",
},
},
})
var doc bson.M
it := pipe.Iter()
for it.Next(&doc) {
fmt.Println(doc)
}
// Handle it.Err()
我们在单个查询中查询用户的帖子。结果是用户和帖子的混合体。我们如何重用 User
和 Post
模型类型,而不必将结果作为 "raw" 文档(bson.M
类型)处理?
上面的查询 returns 文档 "almost" 匹配 User
文档,但它们也有每个用户的帖子。所以基本上结果是一系列 User
文档,带有 Post
数组或切片 embedded.
一种方法是将 Posts []*Post
字段添加到 User
本身,这样我们就完成了:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
Posts []*Post `bson:"posts,omitempty"`
}
虽然这可行,但似乎 "overkill" 将 User
扩展为 Posts
只是为了单个查询。如果我们继续沿着这条路走下去,我们的 User
类型会变得臃肿,因为有很多 "extra" 字段用于不同的查询。更不用说如果我们填写 Posts
字段并保存用户,这些帖子最终会保存在 User
文档中。不是我们想要的。
另一种方法是创建一个 UserWithPosts
类型复制 User
,并添加一个 Posts []*Post
字段。不用说这是丑陋和不灵活的(对 User
所做的任何更改都必须手动反映到 UserWithPosts
)。
使用结构嵌入
我们可以利用 struct embedding(重用现有 User
和 Post
类型)用一个小技巧:
type UserWithPosts struct {
User `bson:",inline"`
Posts []*Post `bson:"posts"`
}
注意 bson tag value ",inline"
. This is documented at bson.Marshal()
and bson.Unmarshal()
(我们将用它来解组):
inline Inline the field, which must be a struct or a map. Inlined structs are handled as if its fields were part of the outer struct. An inlined map causes keys that do not match any other struct field to be inserted in the map rather than being discarded as usual.
通过使用嵌入和 ",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
的类型声明可能是也可能不是局部声明。如果您在其他地方不需要它,它可以是您执行和处理聚合查询的函数中的局部声明,因此它不会使您现有的类型和声明膨胀。如果你想重用它,你可以在包级别声明它(导出或未导出),并在你需要的地方使用它。
修改聚合
另一种选择是使用 MongoDB 的 $replaceRoot
到 "rearrange" 结果文档,因此 "simple" 结构将完美地覆盖文档:
// 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}},
})