Redis: Any easy way to convert the result of HGetAll to a struct?

Created on 30 Nov 2017  ·  11Comments  ·  Source: go-redis/redis

This is a follow up on #348

I'm having the same difficulty with func (*StringStringMapCmd) Result(). It returns map[string]string, which is quite cumbersome to deal with, especially you need to convert string to different types.

I wonder if there is an API like func (*StringStringMapCmd) Scan(interface{}):

type Foo struct {
     Key1 string
     Key2 int
}

var foo Foo
err := client.HGetAll("key").Scan(&foo)

Pretty much like how json works.

Most helpful comment

I also would find this useful. I'm migrating from redigo and am missing the ScanStruct function.

Meanwhile you could use mitchellh/mapstructure

All 11 comments

I also think this is a good feature, because I need it.

I also would find this useful. I'm migrating from redigo and am missing the ScanStruct function.

Meanwhile you could use mitchellh/mapstructure

Just curious, what would the HSet/HMSet for this look like?

Hello,
This issue is still relevant to many.

I am using a pipe to HGetAll but cannot find a way to convert the result to a struct.
I tried to work around this by json.Marshal then json.Unmarshal with no luck.

import (
goredis "gopkg.in/redis.v5"
)
c:= goredis.Client()
pipe := c.Pipeline()

pipe.HGetAll("my-key")

cmder, err := pipe.Exec()

if err != nil {
   panic(err)
}

for _, cmd := range cmder {
   cmd = cmd.(*goredis.StringStringMapCmd).Val() // casts value to `map[string]string`

   value := json.Marshal(cmd) // converts the map to `[]byte`

   json.Unmarshal([]byte(value), &myStruct)
}

myStruct remains empty.

@vmihailenco How would you suggested to go about this?

Thank you.

UPDATE
I found no way to unamarshal to the struct directly, so i worked around it as follow:

hash := cmd.(*goredis.StringStringMapCmd).Val()
json.Unmarshal([]byte(hash["myKey"]), &myStruct)

I also would find this useful. I'm migrating from redigo and am missing the ScanStruct function.

Meanwhile you could use mitchellh/mapstructure

Hello, herkyl!this is my test code:

type Article struct {
    Title string `json:"title" form:"title"`
    Link string `json:"link" form:"link"`
    Poster string `json:"poster" form:"poster"`
    Time int64 `json:"time" form:"time"`
    Votes int `json:"votes" form:"votes"`
}

articleData, err := redisdb.HGetAll(key).Result()
if err != nil {
    return err
}
article := new(Article)
err = mapstructure.Decode(articleData, article)
if err != nil {
    return err
}

Since the value type returned by the function HGetAll is map[string]string, the mapstructure.Decode encounters a field that is not of type string and will fail to parse.

@sswanv would love to help but I actually stayed on redigo, so I'm not using mapstructure anymore 🤷‍♂

I am also looking for a feature like this. I moved from Radix as go-redis has better performance but I am definitely missing the ability to unmarshal to structs.

Hey @herkyl, @cmbernard333 and anyone else tackling this thing, I used struct receivers with the same receiver signatures as the commands I need(hmset, hgetall) to tackle this problem... it's kind of like this:

type SomeStruct struct {
    StringKey    string    `json:"string_key"`
    LastUpdated     time.Time `json:"last_updated"`
}

func (p *SomeStruct) ToMap() (map[string]interface{}, error) {
    fieldSet := map[string]interface{}{}
    v := reflect.Indirect(reflect.ValueOf(p))
    typeOfS := v.Type()
    for i := 0; i < v.NumField(); i++ {
        fieldName := typeOfS.Field(i).Name
        field, ok := typeOfS.FieldByName(fieldName)
        hashKey := fieldName
        if ok {
            if tag := field.Tag.Get("json"); tag != "" {
                hashKey = tag
            }
        }
        value := v.Field(i).Interface()
        switch valueType := value.(type) {
        case string:
            if valueType != "" {
                fieldSet[hashKey] = value.(string)
            }
        case time.Time:
            if !valueType.IsZero() {
                fieldSet[hashKey] = value.(time.Time).Unix()
            }
        default:
            return nil, errors.New(fmt.Sprintf("Unsupported <SomeStruct> type %v for field name %s", reflect.TypeOf(v),
                fieldName))
        }
    }
    return fieldSet, nil
}

func (p *SomeStruct) FromMap(fieldMap map[string]string) error {
    v := reflect.Indirect(reflect.ValueOf(p))
    typeOfS := v.Type()
    for i := 0; i < v.NumField(); i++ {
        fieldName := typeOfS.Field(i).Name
        fieldInterface := v.Field(i).Interface()
        field, ok := typeOfS.FieldByName(fieldName)
        hashKey := fieldName
        if ok {
            if tag := field.Tag.Get("json"); tag != "" {
                hashKey = tag
            }
        }
        rawValue := ""
        if rawValue, ok = fieldMap[hashKey]; ok != true {
            rawValue, ok = fieldMap[fieldName]
        }
        if !ok {
            continue
        }
        switch fieldInterface.(type) {
        case string:
            v.FieldByName(fieldName).Set(reflect.ValueOf(rawValue))
        case time.Time:
            timeInt, err := strconv.ParseInt(rawValue, 10, 64)
            if err != nil {
                return err
            }
            pTime := time.Unix(timeInt, 0)
            v.FieldByName(fieldName).Set(reflect.ValueOf(pTime))
        }

    }
    return nil
}

do note that my actual struct only has string and time attributes but you get the point so you can add the types you need to the type switch

I also would find this useful. I'm migrating from redigo and am missing the ScanStruct function.
Meanwhile you could use mitchellh/mapstructure

Hello, herkyl!this is my test code:

type Article struct {
  Title string `json:"title" form:"title"`
  Link string `json:"link" form:"link"`
  Poster string `json:"poster" form:"poster"`
  Time int64 `json:"time" form:"time"`
  Votes int `json:"votes" form:"votes"`
}

articleData, err := redisdb.HGetAll(key).Result()
if err != nil {
  return err
}
article := new(Article)
err = mapstructure.Decode(articleData, article)
if err != nil {
  return err
}

Since the value type returned by the function HGetAll is map[string]string, the mapstructure.Decode encounters a field that is not of type string and will fail to parse.

type Reward struct {
    Welfare bool json:"welfare" redis:"w"
    Sign bool json:"sign" redis:"s"
}

func Get() (r Reward, err error) {
    res, err := m.redis.HGetAll("key").Result()
    if err != nil {
        return
    }

    decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "redis", Result: &r, WeaklyTypedInput: true})
    if err != nil {
        return
    }

    return r, decoder.Decode(res)
}

func Set(r Reward) (err error) {
    v := make(map[string]interface{}, 0)
    objKey := reflect.TypeOf(r)
    objVal := reflect.ValueOf(r)
    for i := 0; i < objKey.NumField(); i++ {
        if tag := objKey.Field(i).Tag.Get("redis"); tag != "" {
            v[tag] = objVal.Field(i).Interface()
        }
    }
    res := m.redis.HMSet("key", v)

    return res.Err()
}

support tag and weaklyTypedInput

Thanks go to @knadh. I've also updated the changelog - https://redis.uptrace.dev/changelog/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mouhong picture mouhong  ·  3Comments

aahoughton picture aahoughton  ·  5Comments

mollylogue picture mollylogue  ·  4Comments

qiqisteve picture qiqisteve  ·  3Comments

pranas picture pranas  ·  8Comments