Skip to content

Commit

Permalink
Implemented query marshalling to help with remote caching (#7)
Browse files Browse the repository at this point in the history
* Implemented query marshalling to help with remote caching

* Improved query marshalling, and prepared version 3 as API has changed
  • Loading branch information
ktsivkov authored Jan 17, 2024
1 parent 6797e62 commit 3ad6e81
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 82 deletions.
171 changes: 144 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,128 @@ func main() {
}
```

## Cacher Example
## Cacher Example (Redis)

```go
package main

import (
"context"
"fmt"
"time"

"github.com/go-gorm/caches/v3"
"github.com/redis/go-redis/v9"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

type UserRoleModel struct {
gorm.Model
Name string `gorm:"unique"`
}

type UserModel struct {
gorm.Model
Name string
RoleId uint
Role *UserRoleModel `gorm:"foreignKey:role_id;references:id"`
}

type redisCacher struct {
rdb *redis.Client
}

func (c *redisCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
res, err := c.rdb.Get(ctx, key).Result()
if err == redis.Nil {
return nil, nil
}

if err != nil {
return nil, err
}

if err := q.Unmarshal([]byte(res)); err != nil {
return nil, err
}

return q, nil
}

func (c *redisCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
res, err := val.Marshal()
if err != nil {
return err
}

c.rdb.Set(ctx, key, res, 300*time.Second) // Set proper cache time
return nil
}

func main() {
db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})

cachesPlugin := &caches.Caches{Conf: &caches.Config{
Cacher: &redisCacher{
rdb: redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
}),
},
}}

_ = db.Use(cachesPlugin)

_ = db.AutoMigrate(&UserRoleModel{})
_ = db.AutoMigrate(&UserModel{})

db.Delete(&UserRoleModel{})
db.Delete(&UserModel{})

adminRole := &UserRoleModel{
Name: "Admin",
}
db.Save(adminRole)

guestRole := &UserRoleModel{
Name: "Guest",
}
db.Save(guestRole)

db.Save(&UserModel{
Name: "ktsivkov",
Role: adminRole,
})

db.Save(&UserModel{
Name: "anonymous",
Role: guestRole,
})

q1User := &UserModel{}
db.WithContext(context.Background()).Find(q1User, "Name = ?", "ktsivkov")
q2User := &UserModel{}
db.WithContext(context.Background()).Find(q2User, "Name = ?", "ktsivkov")

fmt.Println(fmt.Sprintf("%+v", q1User))
fmt.Println(fmt.Sprintf("%+v", q2User))
}
```

## Cacher Example (Memory)

```go
package main

import (
"context"
"fmt"
"sync"

"github.com/go-gorm/caches"
"gorm.io/driver/mysql"
"github.com/go-gorm/caches/v3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

Expand All @@ -153,77 +264,83 @@ type UserModel struct {
Role *UserRoleModel `gorm:"foreignKey:role_id;references:id"`
}

type dummyCacher struct {
type memoryCacher struct {
store *sync.Map
}

func (c *dummyCacher) init() {
func (c *memoryCacher) init() {
if c.store == nil {
c.store = &sync.Map{}
}
}

func (c *dummyCacher) Get(key string) *caches.Query {
func (c *memoryCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
c.init()
val, ok := c.store.Load(key)
if !ok {
return nil
return nil, nil
}

return val.(*caches.Query)
if err := q.Unmarshal(val.([]byte)); err != nil {
return nil, err
}

return q, nil
}

func (c *dummyCacher) Store(key string, val *caches.Query) error {
func (c *memoryCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
c.init()
c.store.Store(key, val)
res, err := val.Marshal()
if err != nil {
return err
}

c.store.Store(key, res)
return nil
}

func main() {
db, _ := gorm.Open(
mysql.Open("DATABASE_DSN"),
&gorm.Config{},
)
db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})

cachesPlugin := &caches.Caches{Conf: &caches.Config{
Cacher: &dummyCacher{},
Cacher: &memoryCacher{},
}}

_ = db.Use(cachesPlugin)

_ = db.AutoMigrate(&UserRoleModel{})

_ = db.AutoMigrate(&UserModel{})

db.Delete(&UserRoleModel{})
db.Delete(&UserModel{})

adminRole := &UserRoleModel{
Name: "Admin",
}
db.FirstOrCreate(adminRole, "Name = ?", "Admin")
db.Save(adminRole)

guestRole := &UserRoleModel{
Name: "Guest",
}
db.FirstOrCreate(guestRole, "Name = ?", "Guest")
db.Save(guestRole)

db.Save(&UserModel{
Name: "ktsivkov",
Role: adminRole,
})

db.Save(&UserModel{
Name: "anonymous",
Role: guestRole,
})

var (
q1Users []UserModel
q2Users []UserModel
)
q1User := &UserModel{}
db.WithContext(context.Background()).Find(q1User, "Name = ?", "ktsivkov")
q2User := &UserModel{}
db.WithContext(context.Background()).Find(q2User, "Name = ?", "ktsivkov")

db.Model(&UserModel{}).Joins("Role").Find(&q1Users, "Role.Name = ? AND Sleep(1) = false", "Admin")
fmt.Println(fmt.Sprintf("%+v", q1Users))

db.Model(&UserModel{}).Joins("Role").Find(&q2Users, "Role.Name = ? AND Sleep(1) = false", "Admin")
fmt.Println(fmt.Sprintf("%+v", q2Users))
fmt.Println(fmt.Sprintf("%+v", q1User))
fmt.Println(fmt.Sprintf("%+v", q2User))
}
```

Expand Down
12 changes: 10 additions & 2 deletions cacher.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package caches

import (
"context"
)

type Cacher interface {
Get(key string) *Query
Store(key string, val *Query) error
// Get impl should check if a specific key exists in the cache and return its value
// look at Query.Marshal
Get(ctx context.Context, key string, q *Query[any]) (*Query[any], error)
// Store is supposed to store a cached representation of the val param
// look at Query.Unmarshal
Store(ctx context.Context, key string, val *Query[any]) error
}
35 changes: 16 additions & 19 deletions cacher_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package caches

import (
"context"
"errors"
"sync"
)
Expand All @@ -15,42 +16,38 @@ func (c *cacherMock) init() {
}
}

func (c *cacherMock) Get(key string) *Query {
func (c *cacherMock) Get(_ context.Context, key string, _ *Query[any]) (*Query[any], error) {
c.init()
val, ok := c.store.Load(key)
if !ok {
return nil
return nil, nil
}

return val.(*Query)
return val.(*Query[any]), nil
}

func (c *cacherMock) Store(key string, val *Query) error {
func (c *cacherMock) Store(_ context.Context, key string, val *Query[any]) error {
c.init()
c.store.Store(key, val)
return nil
}

type cacherStoreErrorMock struct {
store *sync.Map
type cacherStoreErrorMock struct{}

func (c *cacherStoreErrorMock) Get(context.Context, string, *Query[any]) (*Query[any], error) {
return nil, nil
}

func (c *cacherStoreErrorMock) init() {
if c.store == nil {
c.store = &sync.Map{}
}
func (c *cacherStoreErrorMock) Store(context.Context, string, *Query[any]) error {
return errors.New("store-error")
}

func (c *cacherStoreErrorMock) Get(key string) *Query {
c.init()
val, ok := c.store.Load(key)
if !ok {
return nil
}
type cacherGetErrorMock struct{}

return val.(*Query)
func (c *cacherGetErrorMock) Get(context.Context, string, *Query[any]) (*Query[any], error) {
return nil, errors.New("get-error")
}

func (c *cacherStoreErrorMock) Store(string, *Query) error {
return errors.New("store-error")
func (c *cacherGetErrorMock) Store(context.Context, string, *Query[any]) error {
return nil
}
14 changes: 11 additions & 3 deletions caches.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (c *Caches) ease(db *gorm.DB, identifier string) {
return
}

q := Query{
q := Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
}
Expand All @@ -96,7 +96,15 @@ func (c *Caches) ease(db *gorm.DB, identifier string) {

func (c *Caches) checkCache(db *gorm.DB, identifier string) bool {
if c.Conf.Cacher != nil {
if res := c.Conf.Cacher.Get(identifier); res != nil {
res, err := c.Conf.Cacher.Get(db.Statement.Context, identifier, &Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
})
if err != nil {
_ = db.AddError(err)
}

if res != nil {
res.replaceOn(db)
return true
}
Expand All @@ -106,7 +114,7 @@ func (c *Caches) checkCache(db *gorm.DB, identifier string) bool {

func (c *Caches) storeInCache(db *gorm.DB, identifier string) {
if c.Conf.Cacher != nil {
err := c.Conf.Cacher.Store(identifier, &Query{
err := c.Conf.Cacher.Store(db.Statement.Context, identifier, &Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
})
Expand Down
Loading

0 comments on commit 3ad6e81

Please sign in to comment.