Skip to content

Commit

Permalink
feat: support om by redisjson
Browse files Browse the repository at this point in the history
  • Loading branch information
rueian committed Jan 9, 2022
1 parent e760b43 commit 45b81da
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 264 deletions.
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ c.DoCache(ctx, c.B().Hmget().Key("myhash").Field("1", "2").Cache(), time.Second*

## Object Mapping

The `NewHashRepository` creates an OM repository backed by redis hash.
The `NewHashRepository` and `NewJSONRepository` creates an OM repository backed by redis hash or RedisJSON.

```golang
package main
Expand All @@ -274,27 +274,26 @@ import (
)

type Example struct {
ID string `redis:"-,pk"` // the pk option indicate that this field is the ULID key
Ver int64 `redis:"_v"` // the _v field is required for optimistic locking to prevent the lost update
MyStr string `redis:"f1"`
MyArr []string `redis:"f2,sep=|"` // the sep=<ooo> option is required for converting the slice to/from a string
Key string `json:"key" redis:",key"` // the redis:",key" is required to indicate which field is the ULID key
Ver int64 `json:"ver" redis:",ver"` // the redis:",ver" is required to do optimistic locking to prevent lost update
Str string `json:"myStr"` // both NewHashRepository and NewJSONRepository use json tag as field name
}

func main() {
ctx := context.Background()
c, _ := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}})
// create the hash repo.
// create the repo with NewHashRepository or NewJSONRepository
repo := om.NewHashRepository("my_prefix", Example{}, c)

exp := repo.NewEntity().(*Example)
exp.MyArr = []string{"1", "2"}
fmt.Println(exp.ID) // output 01FNH4FCXV9JTB9WTVFAAKGSYB
exp.Str = "mystr"
fmt.Println(exp.Key) // output 01FNH4FCXV9JTB9WTVFAAKGSYB
repo.Save(ctx, exp) // success

// lookup "my_prefix:01FNH4FCXV9JTB9WTVFAAKGSYB" through client side caching
cache, _ := repo.FetchCache(ctx, exp.ID, time.Second*5)
cache, _ := repo.FetchCache(ctx, exp.Key, time.Second*5)
exp2 := cache.(*Example)
fmt.Println(exp2.MyArr) // output [1 2], which equals to exp.MyArray
fmt.Println(exp2.Str) // output "mystr", which equals to exp.Str

exp2.Ver = 0 // if someone changes the version during your GET then SET operation,
repo.Save(ctx, exp2) // the save will fail with ErrVersionMismatch.
Expand All @@ -308,12 +307,20 @@ If you have RediSearch, you can create and search the repository against the ind

```golang

repo.CreateIndex(ctx, func(schema om.FtCreateSchema) om.Completed {
return schema.FieldName("f1").Text().Build() // you have full index capability by building the command from om.FtCreateSchema
})
if _, ok := repo.(*om.HashRepository); ok {
repo.CreateIndex(ctx, func(schema om.FtCreateSchema) om.Completed {
return schema.FieldName("myStr").Text().Build() // Note that the Example.Str field is mapped to myStr on redis by its json tag
})
}

if _, ok := repo.(*om.JSONRepository); ok {
repo.CreateIndex(ctx, func(schema om.FtCreateSchema) om.Completed {
return schema.FieldName("$.myStr").Text().Build() // the field name of json index should be a json path syntax
})
}

exp := repo.NewEntity().(*Example)
exp.MyStr = "foo" // Note that MyStr is mapped to "f1" in redis by the `redis:"f1"` tag
exp.Str = "foo"
repo.Save(ctx, exp)

n, records, _ := repo.Search(ctx, func(search om.FtSearchIndex) om.Completed {
Expand All @@ -323,10 +330,20 @@ n, records, _ := repo.Search(ctx, func(search om.FtSearchIndex) om.Completed {
fmt.Println("total", n) // n is total number of results matched in redis, which is >= len(records)

for _, v := range records.([]*Example) {
fmt.Println(v.MyStr) // print "foo"
fmt.Println(v.Str) // print "foo"
}
```

### Object Mapping Limitation

`NewHashRepository` only accepts these field types:
* string, *string
* int64, *int64
* bool, *bool
* []byte

Field projection by RediSearch is not supported.

## Not Yet Implement

The following subjects are not yet implemented.
Expand Down
187 changes: 41 additions & 146 deletions om/hash_conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,171 +4,75 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"unsafe"
)

type HashConverter interface {
ToHash() (id string, fields map[string]string)
FromHash(id string, fields map[string]string) error
}

const (
PKOption = "pk"
IgnoreField = "-"
VersionField = "_v"
SliceSepTag = "sep"
)

func newHashConvFactory(t reflect.Type) *hashConvFactory {
if t.Kind() != reflect.Struct {
panic(fmt.Sprintf("schema %q should be a struct", t))
}

v := reflect.New(t)

fields := make(map[string]field, t.NumField())
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
name, options, ok := parseTag(f.Tag)
if !ok {
continue
}
if name == "" {
panic(fmt.Sprintf("schema %q should not contain fields with empty redis tag", t))
}
if _, ok = fields[name]; ok {
panic(fmt.Sprintf("schema %q should not contain fields with duplicated redis tag", t))
}
if !v.Elem().Field(i).CanSet() {
panic(fmt.Sprintf("schema %q should not contain private fields with redis tag", t))
}
if name == IgnoreField {
if _, ok := options[PKOption]; !ok {
panic(fmt.Sprintf("schema %q should non pk fields with redis %q tag", t, "-"))
}
}
if name == VersionField {
if f.Type.Kind() != reflect.Int64 {
panic(fmt.Sprintf("field with tag `redis:%q` in schema %q should be a int64", VersionField, t))
}
}
if _, ok := options[PKOption]; ok {
if f.Type.Kind() != reflect.String {
panic(fmt.Sprintf("field with tag `redis:\",pk\"` in schema %q should be a string", t))
}
}
func newHashConvFactory(t reflect.Type, schema schema) *hashConvFactory {
factory := &hashConvFactory{converters: make(map[string]conv, len(schema.fields))}
for name, f := range schema.fields {
var converter converter
var ok bool

var conv converter
switch f.Type.Kind() {
switch f.typ.Kind() {
case reflect.Ptr:
conv, ok = converters.ptr[f.Type.Elem().Kind()]
converter, ok = converters.ptr[f.typ.Elem().Kind()]
case reflect.Slice:
if builder := converters.slice[f.Type.Elem().Kind()]; builder != nil {
sep := options[SliceSepTag]
if len(sep) == 0 {
panic(fmt.Sprintf("string slice field should have separator in tag `redis:\"%s,sep=<xxx>\"` in schema %q", name, t))
}
conv, ok = builder(sep), true
}
converter, ok = converters.slice[f.typ.Elem().Kind()]
default:
conv, ok = converters.val[f.Type.Kind()]
converter, ok = converters.val[f.typ.Kind()]
}
if !ok {
panic(fmt.Sprintf("schema %q should not contain unsupported field type %s.", t, f.Type.Kind()))
}
fields[name] = field{position: i, options: options, converter: conv}
}

factory := &hashConvFactory{fields: fields, pk: -1}
for _, f := range fields {
if _, ok := f.options[PKOption]; ok {
if factory.pk != -1 {
panic(fmt.Sprintf("schema %q should contain only one field with tag `redis:\",pk\"`", t))
}
factory.pk = f.position
panic(fmt.Sprintf("schema %q should not contain unsupported field type %s.", t, f.typ.Kind()))
}
factory.converters[name] = conv{conv: converter, idx: f.idx}
}
if factory.pk == -1 {
panic(fmt.Sprintf("schema %q should contain a string field with tag `redis:\",pk\"` as primary key", t))
}
if _, ok := fields[VersionField]; !ok {
panic(fmt.Sprintf("schema %q should contain a int64 field with tag `redis:%q` as version tag", VersionField, t))
}
delete(fields, IgnoreField)

return factory
}

type hashConvFactory struct {
pk int
fields map[string]field
converters map[string]conv
}

type field struct {
position int
converter converter
options map[string]string
type conv struct {
idx int
conv converter
}

func (f hashConvFactory) NewConverter(entity reflect.Value) hashConv {
if entity.Kind() == reflect.Ptr {
entity = entity.Elem()
}
return hashConv{
factory: f,
entity: entity,
}
return hashConv{factory: f, entity: entity}
}

type hashConv struct {
factory hashConvFactory
entity reflect.Value
}

func (r hashConv) ToHash() (id string, fields map[string]string) {
fields = make(map[string]string, len(r.factory.fields))
for f, field := range r.factory.fields {
ref := r.entity.Field(field.position)
if v, ok := field.converter.ValueToString(ref); ok {
func (r hashConv) ToHash() (fields map[string]string) {
fields = make(map[string]string, len(r.factory.converters))
for f, converter := range r.factory.converters {
ref := r.entity.Field(converter.idx)
if v, ok := converter.conv.ValueToString(ref); ok {
fields[f] = v
}
}
return r.entity.Field(r.factory.pk).String(), fields
return fields
}

func (r hashConv) FromHash(id string, fields map[string]string) error {
r.entity.Field(r.factory.pk).Set(reflect.ValueOf(id))
for f, field := range r.factory.fields {
func (r hashConv) FromHash(fields map[string]string) error {
for f, field := range r.factory.converters {
v, ok := fields[f]
if !ok {
continue
}
val, err := field.converter.StringToValue(v)
val, err := field.conv.StringToValue(v)
if err != nil {
return err
}
r.entity.Field(field.position).Set(val)
r.entity.Field(field.idx).Set(val)
}
return nil
}

func parseTag(tag reflect.StructTag) (name string, options map[string]string, ok bool) {
if name, ok = tag.Lookup("redis"); !ok {
return "", nil, false
}
tokens := strings.Split(name, ",")
options = make(map[string]string, len(tokens)-1)
for _, token := range tokens[1:] {
kv := strings.SplitN(token, "=", 2)
if len(kv) == 2 {
options[kv[0]] = kv[1]
} else {
options[kv[0]] = ""
}
}
return tokens[0], options, true
}

type converter struct {
ValueToString func(value reflect.Value) (string, bool)
StringToValue func(value string) (reflect.Value, error)
Expand All @@ -177,7 +81,7 @@ type converter struct {
var converters = struct {
val map[reflect.Kind]converter
ptr map[reflect.Kind]converter
slice map[reflect.Kind]func(sep string) converter
slice map[reflect.Kind]converter
}{
ptr: map[reflect.Kind]converter{
reflect.Int64: {
Expand Down Expand Up @@ -256,28 +160,19 @@ var converters = struct {
},
},
},
slice: map[reflect.Kind]func(sep string) converter{
reflect.String: func(sep string) converter {
return converter{
ValueToString: func(value reflect.Value) (string, bool) {
length := value.Len()
if length == 0 {
return "", false
}
sb := strings.Builder{}
for i := 0; i < length; i++ {
sb.WriteString(value.Index(i).String())
if i != length-1 {
sb.WriteString(sep)
}
}
return sb.String(), true
},
StringToValue: func(value string) (reflect.Value, error) {
s := strings.Split(value, sep)
return reflect.ValueOf(s), nil
},
}
slice: map[reflect.Kind]converter{
reflect.Uint8: {
ValueToString: func(value reflect.Value) (string, bool) {
buf, ok := value.Interface().([]byte)
if !ok {
return "", false
}
return *(*string)(unsafe.Pointer(&buf)), true
},
StringToValue: func(value string) (reflect.Value, error) {
buf := []byte(value)
return reflect.ValueOf(buf), nil
},
},
},
}
Loading

0 comments on commit 45b81da

Please sign in to comment.