Mgo聚合:如何重用模型类型以查询和解组“混合”消息。结果? [英] Mgo aggregation: how to reuse model types to query and unmarshal "mixed" results?
问题描述
假设我们有2个馆藏:用户
和帖子
,按以下类型建模:
type用户结构{
ID字符串`bson: _ id`
名称字符串`bson: name`
注册时间。时间`bson: registered`
}
type post struct {
ID字符串`bson: _ id`
用户ID字符串`bson: userID`
内容字符串`bson: content`
日期时间。时间`bson: date`
}
这些可以在存储/检索单个或什至文档集合时使用,例如:
usersColl:= sess.DB()。C( users)
postsColl:= sess.DB()。C( posts)
//插入新用户:
u:=&用户{
ID: 1,
名称: Bob,
已注册:time.Now(),
},
err:= usersColl.Insert(u)
//处理err
//获取帖子最近10分钟:
var个帖子s [] * Post
err:= postsColl.Find(
bson.M { date:bson.M { $ gt:time.Now()。Add(-10 * time。分钟)}},
).Limit(20).All(& posts)
//处理错误
如果我们使用聚合来获取以下内容的混合物,该怎么办?这些文件?例如 Collection.Pipe()
:
//用帖子查询用户:
pipe:= collUsers.Pipe( [] bson.M {
{
$ lookup:bson.M {
from: posts,
localField: _id,
foreignField: userID,
as: posts,
},
},
})
var doc bson.M
it:= pipe.Iter()
for.Next(& doc){
fmt.Println(doc)
}
//处理它Err()
我们在单个查询中查询用户的帖子。结果是用户和帖子的混合。我们如何重用我们的 User
和 Post
模型类型,而不必将结果作为原始文档处理(类型 bson.M
)?
上面的查询返回文档该几乎匹配 User
个文档,但它们也包含每个用户的帖子。所以基本上结果是一系列 User
个文档,这些文档带有 Post
个数组或切片嵌入 。
一种方法是将 Posts [] * Post
字段添加到用户
本身,我们将完成:
type用户结构{
ID字符串`bson: _ id`
名称字符串`bson: name`
注册时间。时间`bson: registered`
Posts [] * Post`bson: posts, omitempty`
}
虽然可行,但扩展 User
个带有 Posts
的帖子,仅用于单个查询。如果我们继续走这条路,我们的 User
类型将因大量用于不同查询的额外字段而变得肿。更不用说如果我们填写帖子
字段并保存用户,这些帖子最终将保存在 User
文档中。不是我们想要的。
另一种方法是创建 UserWithPosts
类型复制 User
,并添加一个 Posts [] * Post
字段。不用说这是丑陋且不灵活的(对 User
所做的任何更改都必须手动反映到 UserWithPosts
中)。
具有结构嵌入
而不是修改原始的 User
,而不是从 scratch创建新的 UserWithPosts
类型,我们可以利用结构嵌入(重用现有的 User
和 Post
类型)一个小技巧:
type UserWithPosts struct {
User`bson:,inline`
帖子[] *发布`bson: posts`
}
注意bson 标签值 ,内嵌
。记录在 bson.Marshal()
和 bson.Unmarshal ()
(我们将使用它进行编组):
inline内联字段,该字段必须是结构或映射。
内联结构的处理方式就像其字段是外部结构的
的一部分一样。内联映射会导致将
与任何其他struct字段都不匹配的键插入
映射中,而不是照常丢弃。
通过嵌入和,内嵌
标记值, UserWithPosts
类型本身将成为解编 User
的有效目标文档,其 Post [] * Post
字段将是查找帖子
的理想选择。 / p>
使用它:
var uwp * UserWithPosts
it := pipe.Iter()
为此。Next(& uwp){
//使用uwp:
fmt.Println(uwp)
}
/ /处理它.Err()
或一步一步获取所有结果:
var uwps [] * UserWithPosts
err:= pipe.All(& uwps)
//处理错误
UserWithPosts
的类型声明可能是当地声明。如果您在其他地方不需要它,则可以在执行和处理聚合查询的函数中使用局部声明,这样就不会膨胀现有的类型和声明。如果要重用它,可以在包级别(导出或未导出)声明它,并在需要的地方使用它。
修改聚合
另一种选择是使用MongoDB的 $ replaceRoot
来重新排列结果文档,因此简单的结构将完全覆盖文档:
//用帖子查询用户:
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`
帖子[] * Post`bson: posts`
}
请注意,尽管此方法有效,所有文档的code> 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}},
})
Let's say we have 2 collections: "users"
and "posts"
, modeled by the following types:
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"`
}
These can be used when storing / retrieving individual or even collection of documents, e.g.:
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
What if we use 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()
We query users with their posts in a single query. The result is a mixture of users and posts. How can we reuse our User
and Post
model types to not have to deal with the result as "raw" documents (of type bson.M
)?
The query above returns documents that "almost" match User
documents, but they also have the posts of each users. So basically the result is a series of User
documents with a Post
array or slice embedded.
One way would be to add a Posts []*Post
field to the User
itself, and we would be done:
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Registered time.Time `bson:"registered"`
Posts []*Post `bson:"posts,omitempty"`
}
While this works, it seems "overkill" to extend User
with Posts
just for the sake of a single query. If we'd continue down this road, our User
type would get bloated with lots of "extra" fields for different queries. Not to mention if we fill the Posts
field and save the user, those posts would end up saved inside the User
document. Not what we want.
Another way would be to create a UserWithPosts
type copying User
, and adding a Posts []*Post
field. Needless to say this is ugly and inflexible (any changes made to User
would have to be reflected to UserWithPosts
manually).
With Struct Embedding
Instead of modifying the original User
, and instead of creating a new UserWithPosts
type from "scratch", we can utilize struct embedding (reusing the existing User
and Post
types) with a little trick:
type UserWithPosts struct {
User `bson:",inline"`
Posts []*Post `bson:"posts"`
}
Note the bson tag value ",inline"
. This is documented at bson.Marshal()
and bson.Unmarshal()
(we'll use it for unmarshaling):
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.
By using embedding and the ",inline"
tag value, the UserWithPosts
type itself will be a valid target for unmarshaling User
documents, and its Post []*Post
field will be a perfect choice for the looked up "posts"
.
Using it:
var uwp *UserWithPosts
it := pipe.Iter()
for it.Next(&uwp) {
// Use uwp:
fmt.Println(uwp)
}
// Handle it.Err()
Or getting all results in one step:
var uwps []*UserWithPosts
err := pipe.All(&uwps)
// Handle error
The type declaration of UserWithPosts
may or may not be a local declaration. If you don't need it elsewhere, it can be a local declaration in the function where you execute and process the aggregation query, so it will not bloat your existing types and declarations. If you want to reuse it, you can declare it at package level (exported or unexported), and use it wherever you need it.
Modifying the aggregation
Another option is to use MongoDB's $replaceRoot
to "rearrange" the result documents, so a "simple" struct will perfectly cover the documents:
// 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",
},
},
},
})
With this remapping, the result documents can be modeled like this:
type UserWithPosts struct {
User *User `bson:"user"`
Posts []*Post `bson:"posts"`
}
Note that while this works, the posts
field of all documents will be fetched from the server twice: once as the posts
field of the returned documents, and once as the field of user
; we don't map / use it but it is present in the result documents. So if this solution is chosen, the user.posts
field should be removed e.g. with a $project
stage:
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}},
})
这篇关于Mgo聚合:如何重用模型类型以查询和解组“混合”消息。结果?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!