drkenap147751 2019-02-25 11:55
浏览 111
已采纳

Go + MongoDB:多态查询

(sorry this question turned out longer than I had thought...)

I'm using Go and MongoDB with the mgo driver. I'm trying to persist and retrieve different structs (implementing a common interface) in the same MongoDB collection. I'm coming from the Java world (where this is very easily done with Spring with literally no config) and I'm having a hard time doing something similar with Go. I have read every last related article or post or StackExchange question I could find, but still I haven't found a complete solution. This includes:

Here's a simplified setup I use for testing. Suppose two structs S1 and S2, implementing a common interface I. S2 has an implicit field of type S1, meaning that structure-wise S2 embeds a S1 value, and that type-wise S2 implements I.

type I interface {
    f1()
}

type S1 struct {
    X int
}

type S2 struct {
    S1
    Y int
}

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

Now I can save an instance of S1 or S2 easily using mgo.Collection.Insert(), but to properly populate a value using mgo.Collection.Find().One() for example, I need to pass a pointer to an existing value of S1 or S2, which means I already know the type of the object I want to read!! I want to be able to retrieve a document from the MongoDB collection without knowing if it's a S1, or a S2, or in fact any object that implements I.

Here's where I am so far: instead of directly saving the object I want to persist, I save a Wrapper struct that holds the MongoDB id, an identifier of the type, and the actual value. The type identifier is a concatenation of packageName + "." + typeName, and it is used to lookup the type in a type registry, since there is no native mechanism to map from a type name to a Type object in Go. This means I need to register the types that I want to be able to persist and retrieve, but I could live with that. Here's how it goes:

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

Here's the code for the type registry:

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
}

The code for saving an object is quite straightforward:

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

The code for retrieving an object is a bit more tricky:

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
}

This partially works as the returned object is typed correctly, but what i can't figure out is how to populate the value pt with the data retrieved from MongoDB which is stored in wrapper.Val as a bson.M.

I have tried the following but it doesn't work:

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

So basically the remaining problem is: how to populate an unknown structure from a bson.M value? I'm sure there has to be an easy solution... Thanks in advance for any help.

Here's a Github gist with all the code: https://gist.github.com/ogerardin/5aa272f69563475ba9d7b3194b12ae57

  • 写回答

2条回答 默认 最新

  • douwu7563 2019-02-25 12:13
    关注

    First, you should always check returned errors, always. bson.Marshal() and bson.Unmarshal() return errors which you don't check. Doing so reveals why it doesn't work:

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

    pt is of type 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()
    

    You can pass this to bson.Unmarshal():

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

    (In your real code you want to do something else than panic, this is just to show you should always check errors!)

    Also note that it is possible to directly convert maps to structs (directly meaning without marshaling and unmarshaling). You may implement it by hand or use a ready 3rd party lib. For details, see Converting map to struct

    Also note that there are more clever ways to solve what you want to do. You could store the type in the ID itself, so if you have the ID, you can construct a value of the type to unmarshal into the query result, so you could skip this whole process. It would be a lot more simple and a lot more efficient.

    For example you could use the following ID structure:

    <type>-<id>
    

    For example:

    my.package.S1-123
    

    When fetching / loading this document, you could use reflection to create a value of my.package.S1, and unmarshal into that directly (pass that to Query.One()).

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 乌班图ip地址配置及远程SSH
  • ¥15 怎么让点阵屏显示静态爱心,用keiluVision5写出让点阵屏显示静态爱心的代码,越快越好
  • ¥15 PSPICE制作一个加法器
  • ¥15 javaweb项目无法正常跳转
  • ¥15 VMBox虚拟机无法访问
  • ¥15 skd显示找不到头文件
  • ¥15 机器视觉中图片中长度与真实长度的关系
  • ¥15 fastreport table 怎么只让每页的最下面和最顶部有横线
  • ¥15 java 的protected权限 ,问题在注释里
  • ¥15 这个是哪里有问题啊?