go-redis icon indicating copy to clipboard operation
go-redis copied to clipboard

HGetAll doesn't seem to support key / Nil checking

Open FireDrunk opened this issue 2 years ago • 2 comments

err := rdb.HGetAll(ctx, "nonsense").Scan(&resultstruct1)

// Verify key exists
if err == redis.Nil {
  log.Printf("Empty record")
}

Gives no logs

err := rdb.Get(ctx, "nonsense")

// Verify key exists
if err == redis.Nil {
  log.Printf("Empty record")
}

Gives the correct error.

Splitting the result and scan methods into 2 seperate objects, and then checking for errors:

cmd := rdb.HGetAll(ctx, "nonsense")

// Verify key exists
err := cmd.Err()
if err == redis.Nil {
  log.Printf("Empty record")
}

if err != nil {
  log.Printf("Error reading from Redis")
  return nil, err
}

scanErr := cmd.Scan(&resultstruct1)
if scanErr != nil {
  log.Printf("Error Scanning Object")
  return nil, err
}

Yields no "empty record" line, nor a scanning error, it just results in an empty struct.

It seems that the HGetAll function works differently regarding the Nil detection.

FireDrunk avatar Jul 01 '22 14:07 FireDrunk

Duplicate of #1668

I have ended up to wrap Scan like this:

// ScanCmder is the interface used by MustScan to scan result into destinations.
//
// In go-redis v8, the following commands have implemented this interface:
//   - MapStringStringCmd
//   - SliceCmd
//   - StringCmd
type ScanCmder interface {
	Scan(dst interface{}) error
}

// MustScan enhances the Scan method of a ScanCmder with these features:
//   - it returns the error redis.Nil when the key does not exist. See https://github.com/go-redis/redis/issues/1668
//   - it supports embedded struct better. See https://github.com/go-redis/redis/issues/2005#issuecomment-1019667052
func MustScan(s ScanCmder, dest ...interface{}) error {
	if cmd, ok := s.(*redis.StringStringMapCmd); ok {
		if len(cmd.Val()) == 0 {
			return redis.Nil
		}
	}

	for _, d := range dest {
		if err := s.Scan(d); err != nil {
			return err
		}
	}

	return nil
}

Update

MustScan should be modified to handle *redis.SliceCmd:

// MustScan enhances the Scan method of a ScanCmder with these features:
//   - it returns the error redis.Nil when the key does not exist. See https://github.com/go-redis/redis/issues/1668
//   - it supports embedded struct better. See https://github.com/go-redis/redis/issues/2005#issuecomment-1019667052
func MustScan(s ScanCmder, dest ...interface{}) error {
	switch cmd := s.(type) {
	case *redis.StringStringMapCmd:
		if len(cmd.Val()) == 0 {
			return redis.Nil
		}
	case *redis.SliceCmd:
		keyExists := false
		for _, v := range cmd.Val() {
			if v != nil {
				keyExists = true
				break
			}
		}
		if !keyExists {
			return redis.Nil
		}
	}

	for _, d := range dest {
		if err := s.Scan(d); err != nil {
			return err
		}
	}

	return nil
}

ZekeLu avatar Jul 08 '22 11:07 ZekeLu

@ZekeLu thank you! I was having this same issue.

shyce avatar Jul 08 '22 16:07 shyce

Hi has anyone tried storing redis hash as proto marshalled and unmarshall and retrieve in struct? I am facing issue in that Doing below but not able to get desired result

var res interface{} res, err = redis.GoRedisInstance().Do(context.Background(), "HGETALL", key).Result() if err != nil { return nil, false, err } if response, ok := res.(string); ok { value = []byte(response) } styleEntry, err := parseStyle(ctx, value)

func parseStyle(ctx context.Context, b []byte) (*style_fetch.CMSStyleRedisEntry, error) { // convert to product redis object productRedis := style_fetch.CMSStyleRedisEntry{} err := proto.Unmarshal(b, &productRedis) if err != nil { return nil, err } return &productRedis, nil }

While debugging redis.GoRedisInstance().Do(context.Background(), "HGETALL", key) and .Result post that gives below. Screenshot 2022-11-10 at 4 19 59 PM Screenshot 2022-11-10 at 4 20 29 PM

kumari-jyoti avatar Nov 10 '22 10:11 kumari-jyoti

Resolved proto unmarshall required at invidual field level and not on whole struct as here we do hset/hmset individual fields

        unboxed, ok := res.(map[string]string)
	if !ok {
		fmt.Println("Output should be a pointer of a map")
	}
	//If error is nil but value is also empty/nil. Data not found in redis, reindex
	if unboxed == nil || len(unboxed) <= 0 {
		//async reindexing call
		isCacheMiss = true
		statsd.Instance().Incr("redis.keyNotPresentError", 1)
		return nil, isCacheMiss, nil
	}

	redisEntry := &style_fetch.CMSStyleRedisEntry{}
	for key, value := range unboxed {
		if key == "style" {
			style := &style_fetch.Style{}
			proto.Unmarshal([]byte(value), style)
			redisEntry.Style = style
		} 
               //...other keys
        }

kumari-jyoti avatar Nov 11 '22 00:11 kumari-jyoti

Will any of the maintainers fix this? Silly bug.

ilyavaiser avatar Jun 16 '23 18:06 ilyavaiser

Closing as duplicate of https://github.com/redis/go-redis/issues/1668

vmihailenco avatar Jun 21 '23 11:06 vmihailenco