Go + MongoDB: полиморфные запросы - PullRequest
0 голосов
/ 25 февраля 2019

(извините, этот вопрос оказался дольше, чем я думал ...)

Я использую Go и MongoDB с драйвером mgo.Я пытаюсь сохранить и извлечь различные структуры (реализуя общий интерфейс) в одной коллекции MongoDB.Я пришел из мира Java (где это очень легко сделать с помощью Spring без буквального конфигурирования), и мне трудно делать что-то подобное с Go.Я прочитал каждую последнюю связанную статью или пост или вопрос StackExchange, который я мог найти, но все же я не нашел полного решения.Это включает в себя:

Вот упрощенная настройка, которую я использую для тестирования.Предположим, две структуры 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")
}

Теперь я могу легко сохранить экземпляр S1 или S2, используя mgo.Collection.Insert(), но для правильного заполнения значения, например, mgo.Collection.Find().One(), мне нужно передать указатель насуществующее значение S1 или S2, что означает, что я уже знаю тип объекта, который хочу прочитать !!Я хочу иметь возможность получить документ из коллекции MongoDB, не зная, является ли это S1, или S2, или фактически любым объектом, который реализует I.

Вот где я такдалеко: вместо непосредственного сохранения объекта, который я хочу сохранить, я сохраняю структуру Wrapper, которая содержит идентификатор MongoDB, идентификатор типа и фактическое значение.Идентификатор типа представляет собой объединение packageName + "."+ typeName , и он используется для поиска типа в реестре типов, поскольку отсутствует собственный механизм для сопоставления имени типа с объектом Type в Go.Это означает, что мне нужно зарегистрировать типы, которые я хочу сохранить и восстановить, но я мог бы жить с этим.Вот как это происходит:

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
}

Это частично работает, поскольку возвращаемый объект набирается правильно, но я не могу понять, как заполнить значение pt с помощьюданные, извлеченные из MongoDB, которые хранятся в 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

Ответы [ 2 ]

0 голосов
/ 25 февраля 2019

Согласно комментариям @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 вместомаршаллинга-маршаллинга.

0 голосов
/ 25 февраля 2019

Во-первых, вы всегда должны проверять возвращенные ошибки, всегда.bson.Marshal() и bson.Unmarshal() возвращают ошибки, которые вы не проверяете.Это показывает, почему это не работает:

unmarshal не может работать со значениями структуры.Используйте указатель

pt типа reflect.Value (который является структурой), а не то, что вы должны передать bson.Unmarshal().Вы должны передать, например, указатель на значение структуры, в которое вы хотите разобрать (которое будет заключено в значение interface{}).Поэтому вызовите Value.Interface() для значения, возвращаемого 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)
}

(В вашем реальном коде вы хотите сделать что-то еще, кроме паники, это просто для того, чтобы показать, что вы всегда должны проверять ошибки!)

Также обратите внимание, что есть возможность напрямую преобразовывать карты в структуры (непосредственнобез маршалинга и демаршалинга).Вы можете реализовать это вручную или использовать готовую стороннюю библиотеку.Подробнее см. Преобразование карты в структуру

Также обратите внимание, что есть более умные способы решить, что вы хотите сделать.Вы можете сохранить тип в самом идентификаторе, поэтому, если у вас есть идентификатор, вы можете создать значение типа, которое будет разбивать в результате запроса, чтобы вы могли пропустить весь этот процесс.Это было бы намного проще и эффективнее.

Например, вы можете использовать следующую структуру идентификатора:

<type>-<id>

Например:

my.package.S1-123

При извлечении / загрузке этого документа вы можете использовать отражение, чтобы создать значение my.package.S1, и распаковать его непосредственно (передать его в Query.One()).

...