drkenap147751
2019-02-25 11:55 阅读 90

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 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()).

    点赞 评论 复制链接分享
  • douxiegan6468 douxiegan6468 2019-02-25 14:36

    As per @icza 's comments, here's a modified version of getById() that actually works:

    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
    }
    

    Conversion from bson.M to the struct is handled by https://github.com/mitchellh/mapstructure instead of marshalling-unmarshaling.

    点赞 评论 复制链接分享

相关推荐