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()

我们在单个查询中查询用户的帖子。结果是用户和帖子的混合体。我们如何重用 UserPost 模型类型,而不必将结果作为 "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(重用现有 UserPost 类型)用一个小技巧:

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}},
})