【问题标题】:Go + MongoDB: polymorphic queriesGo + MongoDB:多态查询
【发布时间】:2019-02-26 00:55:41
【问题描述】:

(对不起,这个问题比我想象的要长……)

我将 Go 和 MongoDB 与 mgo 驱动程序一起使用。我试图在同一个 MongoDB 集合中持久化和检索不同的结构(实现一个通用接口)。我来自 Java 世界(使用 Spring 很容易做到这一点,几乎没有配置),我很难用 Go 做类似的事情。 我已经阅读了我能找到的所有相关文章或帖子或 StackExchange 问题,但我仍然没有找到完整的解决方案。这包括:

这是我用于测试的简化设置。假设有两个结构S1S2,实现了一个公共接口IS2 有一个 S1 类型的隐式字段,这意味着结构方面的 S2 嵌入了 S1 值,而类型方面的 S2 实现 I

type I interface {
    f1()
}

type S1 struct {
    X int
}

type S2 struct {
    S1
    Y int
}

func (*S1) f1() {
    fmt.Println("f1")
}

现在我可以使用mgo.Collection.Insert() 轻松保存S1S2 的实例,但是要使用mgo.Collection.Find().One() 正确填充值,我需要将指针传递给S1 的现有值或S2,表示我已经知道我要读取的对象的类型!! 我希望能够在不知道它是S1S2,或者实际上是实现I 的任何对象的情况下从MongoDB 集合中检索文档。

到目前为止,这里是:我没有直接保存我想要持久化的对象,而是保存了一个 Wrapper 结构,它包含 MongoDB id、类型的标识符和实际值。类型标识符是 packageName + "." 的串联。 + typeName,它用于在类型注册表中查找类型,因为在 Go 中没有将类型名称映射到 Type 对象的本地机制。这意味着我需要注册我希望能够持久和检索的类型,但我可以接受。事情是这样的:

typeregistry.Register(reflect.TypeOf((*S1)(nil)).Elem())
typeregistry.Register(reflect.TypeOf((*S2)(nil)).Elem())

这是类型注册表的代码:

var types map[string]reflect.Type

func init() {
    types = make(map[string]reflect.Type)
}

func Register(t reflect.Type) {
    key := GetKey(t)
    types[key] = t
}

func GetKey(t reflect.Type) string {
    key := t.PkgPath() + "." + t.Name()
    return key
}

func GetType(key string) reflect.Type {
    t := types[key]
    return t
}

保存对象的代码非常简单:

func save(coll *mgo.Collection, s I) (bson.ObjectId, error) {
    t := reflect.TypeOf(s)
    wrapper := Wrapper{
        Id:      bson.NewObjectId(),
        TypeKey: typeregistry.GetKey(t),
        Val:     s,
    }
    return wrapper.Id, coll.Insert(wrapper)
}

检索对象的代码有点棘手:

func getById(coll *mgo.Collection, id interface{}) (*I, error) {
    // read wrapper
    wrapper := Wrapper{}
    err := coll.Find(bson.M{"_id": id}).One(&wrapper)
    if err != nil {
        return nil, err
    }

    // obtain Type from registry
    t := typeregistry.GetType(wrapper.TypeKey)

    // get a pointer to a new value of this type
    pt := reflect.New(t)

    // FIXME populate value using wrapper.Val (type bson.M)
    // HOW ???

    // return the value as *I
    i := pt.Elem().Interface().(I)
    return &i, nil
}

当返回的对象键入正确时,这部分起作用,但我不知道如何使用从 MongoDB 检索到的数据填充值 pt,该数据存储在 wrapper.Val 中作为 bson.M

我尝试了以下方法,但它不起作用:

m := wrapper.Val.(bson.M)
bsonBytes, _ := bson.Marshal(m)
bson.Unmarshal(bsonBytes, pt)

所以基本上剩下的问题是:如何从bson.M 值填充未知结构?我确信必须有一个简单的解决方案...... 提前感谢您的帮助。

这是一个包含所有代码的 Github 要点:https://gist.github.com/ogerardin/5aa272f69563475ba9d7b3194b12ae57

【问题讨论】:

    标签: mongodb go polymorphism mgo


    【解决方案1】:

    首先,您应该始终检查返回的错误。 bson.Marshal()bson.Unmarshal() 返回您不检查的错误。这样做揭示了为什么它不起作用:

    unmarshal 无法处理结构值。使用指针

    ptreflect.Value 类型(恰好是一个结构),而不是你应该传递给bson.Unmarshal() 的东西。你应该通过例如指向要解组到的结构值的指针(将包装在 interface{} 值中)。所以在reflect.New()返回的值上调用Value.Interface()

    pt := reflect.New(t).Interface()
    

    你可以把这个传给bson.Unmarshal():

    bsonBytes, err := bson.Marshal(m)
    if err != nil {
        panic(err)
    }
    if err = bson.Unmarshal(bsonBytes, pt); err != nil {
        panic(err)
    }
    

    (在你的真实代码中,你想要做的不是恐慌,这只是为了表明你应该经常检查错误!)

    还请注意,可以直接将映射转换为结构(直接表示无需编组和解组)。您可以手动实现它或使用现成的 3rd 方库。详情见Converting map to struct

    另请注意,还有更聪明的方法可以解决您想做的事情。您可以将类型存储在 ID 本身中,因此如果您有 ID,则可以构造一个类型的值来解组到查询结果中,这样您就可以跳过整个过程。它会更简单,效率更高。

    例如,您可以使用以下 ID 结构:

    <type>-<id>
    

    例如:

    my.package.S1-123
    

    在获取/加载此文档时,您可以使用反射创建一个值my.package.S1,然后直接解组到该值(将其传递给Query.One())。

    【讨论】:

    • 首先感谢您的详细解答!有意跳过错误处理以使代码更具可读性,但这样做我可能会错过一条有用的错误消息...我已更改我的代码以将 pt.Interface() 而不是 pt 传递给 Unmarshal 并且它的工作原理是预期的。当然 marshalling-unmarshalling 不是处理这个问题的最有效方法,但我认为它是一种简单的方法。我现在将研究更有效的替代方案:)
    • 关于在ID中包含类型,我不喜欢这个,因为我认为ID不应该传达任何商业意义,应该保持完全透明。这也意味着每个可持久结构都必须有一个“id”字段,而在我的解决方案中,只有 Wrapper 需要它......
    • @OlivierGérardin 在键(或 ID)本身中包含实体类型(或类型)并不罕见。 Google Cloud Datastore 实体键 (see here) 就是一个很好的例子。
    【解决方案2】:

    根据 @icza 的 cmets,这里是 getById() 的修改版本,它确实有效:

    func getById(coll *mgo.Collection, id interface{}) (*I, error) {
        // read wrapper
        wrapper := Wrapper{}
        err := coll.Find(bson.M{"_id": id}).One(&wrapper)
        if err != nil {
            return nil, err
        }
    
        // obtain Type from registry
        t := typeregistry.GetType(wrapper.TypeKey)
    
        // get a pointer to a new value of this type
        pt := reflect.New(t)
    
        // populate value using wrapper.Val
        err = mapstructure.Decode(wrapper.V, pt.Interface())
        if err != nil {
            return nil, err
        }
    
        // return the value as *I
        i := pt.Elem().Interface().(I)
        return &i, nil
    }
    

    bson.M 到结构的转换由https://github.com/mitchellh/mapstructure 处理,而不是marshalling-unmarshaling。

    【讨论】:

      猜你喜欢
      • 2021-01-19
      • 2017-02-20
      • 1970-01-01
      • 2019-08-16
      • 1970-01-01
      • 2017-08-26
      • 2020-12-15
      • 2015-03-01
      • 2017-04-07
      相关资源
      最近更新 更多