Go + MongoDB: 多态查询

Go + MongoDB: polymorphic queries

(抱歉,这个问题比我想象的要长...)

我正在使用 Go 和 MongoDB 以及 mgo 驱动程序。我试图在同一个 MongoDB 集合中保留和检索不同的结构(实现通用接口)。我来自 Java 世界(用 Spring 很容易完成,几乎没有配置),我很难用 Go 做类似的事情。 我已经阅读了我能找到的所有最新相关文章或 post 或 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() 正确填充值,我需要通过指向 S1S2 的现有值的指针,这意味着我已经知道我要读取的对象的类型!! 我希望能够从 MongoDB 集合中检索文档,而不知道它是 S1 还是 S2,或者实际上是实现了 I 的任何对象。

这是我到目前为止的位置:我没有直接保存我想要持久化的对象,而是保存了一个 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

首先,您应该始终检查 returned 错误,始终如此。 bson.Marshal() and bson.Unmarshal() return 个您未检查的错误。这样做揭示了为什么它不起作用:

unmarshal can't deal with struct values. Use a pointer

pt 的类型是 reflect.Value (which happens to be a struct), not something you should pass to bson.Unmarshal(). You should pass e.g. a pointer to a struct value you want to unmarshal into (which will be wrapped in an interface{} value). So call Value.Interface() on the value returned by reflect.New():

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

(在你的真实代码中你想做一些除了恐慌之外的事情,这只是为了告诉你应该总是检查错误!)

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

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

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

<type>-<id>

例如:

my.package.S1-123

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

根据@icza 的评论,这里是 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 而不是编组-解组处理。