caches

package module
v5.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2026 License: MIT Imports: 10 Imported by: 0

README

Gorm Caches

Gorm Caches plugin using database request reductions (easer), and response caching mechanism provide you an easy way to optimize database performance.

Features

  • Database request reduction. If three identical requests are running at the same time, only the first one is going to be executed, and its response will be returned for all.
  • Database response caching. By implementing the Cacher interface, you can easily setup a caching mechanism for your database queries.
  • Granular cache invalidation. Mutations automatically provide table names, entity IDs, and mutation type via InvalidationEvent.
  • Tag-based invalidation. Optionally tag cache entries with TagsFunc and selectively invalidate them with WithInvalidateTags.
  • Supports all databases that are supported by gorm itself.

Install

go get -u github.com/adrielcodeco/gorm-cache/v5

Usage

Configure the easer, and the cacher, and then load the plugin to gorm.

package main

import (
	"fmt"
	"sync"

	"github.com/adrielcodeco/gorm-cache/v5"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	db, _ := gorm.Open(
		mysql.Open("DATABASE_DSN"),
		&gorm.Config{},
	)
	cachesPlugin := &caches.Caches{Conf: &caches.Config{
		Easer: true,
		Cacher: &yourCacherImplementation{},
	}}
	_ = db.Use(cachesPlugin)
}

Easer Example

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/adrielcodeco/gorm-cache/v5"
	"gorm.io/driver/mysql"
	"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"`
}

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

	cachesPlugin := &caches.Caches{Conf: &caches.Config{
		Easer: true,
	}}

	_ = db.Use(cachesPlugin)

	_ = db.AutoMigrate(&UserRoleModel{})

	_ = db.AutoMigrate(&UserModel{})

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

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

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

	var (
		q1Users []UserModel
		q2Users []UserModel
	)
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go func() {
		db.Model(&UserModel{}).Joins("Role").Find(&q1Users, "Role.Name = ? AND Sleep(1) = false", "Admin")
		wg.Done()
	}()
	go func() {
		time.Sleep(500 * time.Millisecond)
		db.Model(&UserModel{}).Joins("Role").Find(&q2Users, "Role.Name = ? AND Sleep(1) = false", "Admin")
		wg.Done()
	}()
	wg.Wait()

	fmt.Println(fmt.Sprintf("%+v", q1Users))
	fmt.Println(fmt.Sprintf("%+v", q2Users))
}

Cacher Example (Redis)

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/adrielcodeco/gorm-cache/v5"
	"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
	}

	// You can use caches.TagsFromContext(ctx) here to store tags alongside the cache entry
	c.rdb.Set(ctx, key, res, 300*time.Second)
	return nil
}

func (c *redisCacher) Invalidate(ctx context.Context, event *caches.InvalidationEvent) error {
	// Use event.Tables, event.EntityIDs, event.MutationType for granular invalidation
	// Use event.Tags for tag-based invalidation (if WithInvalidateTags was used)
	// Fallback: invalidate all if no tags are present
	var (
		cursor uint64
		keys   []string
	)
	for {
		var (
			k   []string
			err error
		)
		k, cursor, err = c.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", caches.IdentifierPrefix), 0).Result()
		if err != nil {
			return err
		}
		keys = append(keys, k...)
		if cursor == 0 {
			break
		}
	}

	if len(keys) > 0 {
		if _, err := c.rdb.Del(ctx, keys...).Result(); err != nil {
			return err
		}
	}
	return nil
}

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

	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)

package main

import (
	"context"
	"fmt"
	"sync"

	"github.com/adrielcodeco/gorm-cache/v5"
	"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 memoryCacher struct {
	store *sync.Map
}

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

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, nil
	}

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

	return q, nil
}

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

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

func (c *memoryCacher) Invalidate(ctx context.Context, event *caches.InvalidationEvent) error {
	c.store = &sync.Map{}
	return nil
}

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

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

	_ = 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))
}

Tags (Query Keys)

Tags allow you to selectively invalidate cache entries, similar to TanStack React Query's query keys. Instead of invalidating all cache entries on every mutation, you can tag cached queries and only invalidate the relevant ones.

Setup TagsFunc

Use Config.TagsFunc to generate tags for each cached query. Tags are passed to your Cacher.Store implementation via context (retrievable with caches.WithTags):

cachesPlugin := &caches.Caches{Conf: &caches.Config{
	Cacher: &yourCacher{},
	TagsFunc: func(db *gorm.DB) []string {
		return []string{db.Statement.Table}
	},
}}
Invalidate by Tags

When performing mutations, use caches.WithInvalidateTags to specify which tags to invalidate:

ctx := caches.WithInvalidateTags(context.Background(), "users")
db.WithContext(ctx).Create(&User{Name: "John"})

In your Cacher.Invalidate implementation, check event.Tags to determine which entries to invalidate:

func (c *yourCacher) Invalidate(ctx context.Context, event *caches.InvalidationEvent) error {
	if len(event.Tags) > 0 {
		// Selectively invalidate entries matching these tags
		return c.invalidateByTags(ctx, event.Tags)
	}
	// Fallback: invalidate all (no tags specified)
	return c.invalidateAll(ctx)
}
InvalidationEvent

Every mutation callback receives an InvalidationEvent with:

Field Type Description
Tables []string Tables involved in the mutation (main table + relationships)
EntityIDs []interface{} Primary key values of affected entities
MutationType MutationType MutationCreate, MutationUpdate, or MutationDelete
Tags []string Tags from WithInvalidateTags context (empty if not set)

License

MIT license.

Easer

The easer is an adjusted version of the ServantGo library to fit the needs of this plugin.

Documentation

Index

Constants

View Source
const IdentifierPrefix = "gorm-caches::"

Variables

This section is empty.

Functions

func SetPointedValue

func SetPointedValue(dest interface{}, src interface{})

func WithInvalidateTags

func WithInvalidateTags(ctx context.Context, tags ...string) context.Context

WithInvalidateTags sets tags on the context to indicate which cache entries should be invalidated during a mutation. Use this in your application code before performing CREATE/UPDATE/DELETE operations.

func WithTags

func WithTags(ctx context.Context, tags ...string) context.Context

WithTags associates cache tags with the context. Used internally by the caching system to tag cache entries.

Types

type Cacher

type Cacher interface {
	// 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 impl should store a cached representation of the val param
	// look at Query.Unmarshal
	Store(ctx context.Context, key string, val *Query[any]) error
	// Invalidate impl should invalidate cached values based on the given event.
	// The event contains metadata about the mutation (tables, entity IDs, mutation type, and tags).
	// It will be called when INSERT / UPDATE / DELETE queries are sent to the DB.
	Invalidate(ctx context.Context, event *InvalidationEvent) error
}

type Caches

type Caches struct {
	Conf *Config
	// contains filtered or unexported fields
}

func (*Caches) Initialize

func (c *Caches) Initialize(db *gorm.DB) error

func (*Caches) Name

func (c *Caches) Name() string

type Config

type Config struct {
	Easer    bool
	Cacher   Cacher
	TagsFunc func(db *gorm.DB) []string
	Prefix   string
}

type InvalidationEvent

type InvalidationEvent struct {
	Tables       []string
	EntityIDs    []interface{}
	MutationType MutationType
	Tags         []string
}

InvalidationEvent contains metadata about a mutation that triggered cache invalidation.

type MutationType

type MutationType int

MutationType represents the type of mutation that triggered cache invalidation.

const (
	MutationCreate MutationType = iota
	MutationUpdate
	MutationDelete
)

func (MutationType) String

func (m MutationType) String() string

type Query

type Query[T any] struct {
	Dest         T
	RowsAffected int64
}

func (*Query[T]) Marshal

func (q *Query[T]) Marshal() ([]byte, error)

func (*Query[T]) Unmarshal

func (q *Query[T]) Unmarshal(bytes []byte) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL