Go + MongoDB: 多态查询
Go + MongoDB: polymorphic queries
(抱歉,这个问题比我想象的要长...)
我正在使用 Go 和 MongoDB 以及 mgo 驱动程序。我试图在同一个 MongoDB 集合中保留和检索不同的结构(实现通用接口)。我来自 Java 世界(用 Spring 很容易完成,几乎没有配置),我很难用 Go 做类似的事情。
我已经阅读了我能找到的所有最新相关文章或 post 或 StackExchange 问题,但我仍然没有找到完整的解决方案。这包括:
- Unstructured MongoDB collections with mgo
- How do you create a new instance of a struct from its type at run time in Go?
这是我用于测试的简化设置。假设两个结构 S1
和 S2
,实现了一个公共接口 I
。 S2
有一个类型为 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()
轻松保存 S1
或 S2
的实例,但是要使用 mgo.Collection.Find().One()
正确填充值,我需要通过指向 S1
或 S2
的现有值的指针,这意味着我已经知道我要读取的对象的类型!!
我希望能够从 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 而不是编组-解组处理。
(抱歉,这个问题比我想象的要长...)
我正在使用 Go 和 MongoDB 以及 mgo 驱动程序。我试图在同一个 MongoDB 集合中保留和检索不同的结构(实现通用接口)。我来自 Java 世界(用 Spring 很容易完成,几乎没有配置),我很难用 Go 做类似的事情。 我已经阅读了我能找到的所有最新相关文章或 post 或 StackExchange 问题,但我仍然没有找到完整的解决方案。这包括:
- Unstructured MongoDB collections with mgo
- How do you create a new instance of a struct from its type at run time in Go?
这是我用于测试的简化设置。假设两个结构 S1
和 S2
,实现了一个公共接口 I
。 S2
有一个类型为 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()
轻松保存 S1
或 S2
的实例,但是要使用 mgo.Collection.Find().One()
正确填充值,我需要通过指向 S1
或 S2
的现有值的指针,这意味着我已经知道我要读取的对象的类型!!
我希望能够从 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 而不是编组-解组处理。