【问题标题】:Mgo aggregation: how to reuse model types to query and unmarshal "mixed" results?Mgo 聚合:如何重用模型类型来查询和解组“混合”结果?
【发布时间】:2018-03-28 05:12:50
【问题描述】:

假设我们有 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 来获取这些文档的混合,会怎样?例如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 模型类型而不必将结果作为“原始”文档(bson.M 类型)处理?

【问题讨论】:

    标签: mongodb go types aggregation-framework mgo


    【解决方案1】:

    上面的查询返回“几乎”匹配User 文档的文档,但它们也包含每个用户的帖子。所以基本上结果是一系列带有Post 数组或切片的User 文档嵌入

    一种方法是将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)。

    使用结构嵌入

    我们可以利用struct embedding(重用现有的UserPost类型),而不是修改原来的User,而不是从“scratch”创建一个新的UserWithPosts类型。 :

    type UserWithPosts struct {
        User  `bson:",inline"`
        Posts []*Post `bson:"posts"`
    }
    

    注意 bson tag value ",inline"。这记录在 bson.Marshal()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 来“重新排列”结果文档,因此“简单”结构将完美地覆盖文档:

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

    【讨论】:

      猜你喜欢
      • 2018-02-07
      • 1970-01-01
      • 1970-01-01
      • 2021-12-15
      • 1970-01-01
      • 1970-01-01
      • 2019-01-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多