Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions cache/afterDelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,58 @@ func AfterDelete(cache *Gorm2Cache) func(db *gorm.DB) {
if err != nil {
cache.Logger.CtxError(ctx, "[AfterDelete] invalidating cache for primary keys: %v error: %v",
primaryKeys, err)
return
} else {
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating cache for primary keys: %v finished.", primaryKeys)
}
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating cache for primary keys: %v finished.", primaryKeys)
} else {
cache.Logger.CtxInfo(ctx, "[AfterDelete] now start to invalidate all primary cache for table: %s", tableName)
err := cache.InvalidateAllPrimaryCache(ctx, tableName)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterDelete] invalidating primary cache for table %s error: %v",
tableName, err)
return
} else {
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating all primary cache for table: %s finished.", tableName)
}
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating all primary cache for table: %s finished.", tableName)
}

// 失效unique键缓存
// 尝试从WHERE子句中提取unique键
uniqueKeysMap, _ := getUniqueKeysFromWhereClause(db)
if len(uniqueKeysMap) > 0 {
for indexName, uniqueKeys := range uniqueKeysMap {
if len(uniqueKeys) > 0 {
cache.Logger.CtxInfo(ctx, "[AfterDelete] now start to invalidate unique cache for index %s keys: %+v", indexName, uniqueKeys)
err := cache.BatchInvalidateUniqueCache(ctx, tableName, indexName, uniqueKeys)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterDelete] invalidating unique cache for index %s keys %v error: %v",
indexName, uniqueKeys, err)
} else {
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating unique cache for index %s keys: %+v finished.", indexName, uniqueKeys)
}
}
}
} else {
// 如果没有从WHERE子句提取到unique键,失效所有unique键缓存
s := db.Statement.Schema
if s == nil && db.Statement.Model != nil {
stmt := &gorm.Statement{DB: db}
if err := stmt.Parse(db.Statement.Model); err == nil {
s = stmt.Schema
}
}
if s != nil {
allUniqueIndexes := getAllUniqueIndexes(s)
for indexName := range allUniqueIndexes {
cache.Logger.CtxInfo(ctx, "[AfterDelete] now start to invalidate all unique cache for index %s", indexName)
err := cache.InvalidateAllUniqueCache(ctx, tableName, indexName)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterDelete] invalidating all unique cache for index %s error: %v", indexName, err)
} else {
cache.Logger.CtxInfo(ctx, "[AfterDelete] invalidating all unique cache for index %s finished.", indexName)
}
}
}
}
}
}()

Expand Down
47 changes: 43 additions & 4 deletions cache/afterUpdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,57 @@ func AfterUpdate(cache *Gorm2Cache) func(db *gorm.DB) {
if err != nil {
cache.Logger.CtxError(ctx, "[AfterUpdate] invalidating primary cache for key %v error: %v",
primaryKeys, err)
return
} else {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating cache for primary keys: %+v finished.", primaryKeys)
}
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating cache for primary keys: %+v finished.", primaryKeys)
} else {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] now start to invalidate all primary cache for table: %s", tableName)
err := cache.InvalidateAllPrimaryCache(ctx, tableName)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterUpdate] invalidating primary cache for table %s error: %v",
tableName, err)
return
} else {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating all primary cache for table: %s finished.", tableName)
}
}

// 失效unique键缓存
// 尝试从WHERE子句中提取unique键
uniqueKeysMap, _ := getUniqueKeysFromWhereClause(db)
if len(uniqueKeysMap) > 0 {
for indexName, uniqueKeys := range uniqueKeysMap {
if len(uniqueKeys) > 0 {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] now start to invalidate unique cache for index %s keys: %+v", indexName, uniqueKeys)
err := cache.BatchInvalidateUniqueCache(ctx, tableName, indexName, uniqueKeys)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterUpdate] invalidating unique cache for index %s keys %v error: %v",
indexName, uniqueKeys, err)
} else {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating unique cache for index %s keys: %+v finished.", indexName, uniqueKeys)
}
}
}
} else {
// 如果没有从WHERE子句提取到unique键,失效所有unique键缓存
s := db.Statement.Schema
if s == nil && db.Statement.Model != nil {
stmt := &gorm.Statement{DB: db}
if err := stmt.Parse(db.Statement.Model); err == nil {
s = stmt.Schema
}
}
if s != nil {
allUniqueIndexes := getAllUniqueIndexes(s)
for indexName := range allUniqueIndexes {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] now start to invalidate all unique cache for index %s", indexName)
err := cache.InvalidateAllUniqueCache(ctx, tableName, indexName)
if err != nil {
cache.Logger.CtxError(ctx, "[AfterUpdate] invalidating all unique cache for index %s error: %v", indexName, err)
} else {
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating all unique cache for index %s finished.", indexName)
}
}
}
cache.Logger.CtxInfo(ctx, "[AfterUpdate] invalidating all primary cache for table: %s finished.", tableName)
}
}
}()
Expand Down
57 changes: 54 additions & 3 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@ func (c *Gorm2Cache) InvalidateSearchCache(ctx context.Context, tableName string
}

func (c *Gorm2Cache) InvalidatePrimaryCache(ctx context.Context, tableName string, primaryKey string) error {
// primaryKey 已经是最终格式(单个值或已用":"连接的联合主键),直接传入
return c.cache.DeleteKey(ctx, util.GenPrimaryCacheKey(c.InstanceId, tableName, primaryKey))
}

func (c *Gorm2Cache) BatchInvalidatePrimaryCache(ctx context.Context, tableName string, primaryKeys []string) error {
cacheKeys := make([]string, 0, len(primaryKeys))
for _, primaryKey := range primaryKeys {
// primaryKey 已经是最终格式(单个值或已用":"连接的联合主键),直接传入
cacheKeys = append(cacheKeys, util.GenPrimaryCacheKey(c.InstanceId, tableName, primaryKey))
}
return c.cache.BatchDeleteKeys(ctx, cacheKeys)
Expand All @@ -135,6 +137,7 @@ func (c *Gorm2Cache) InvalidateAllPrimaryCache(ctx context.Context, tableName st
func (c *Gorm2Cache) BatchPrimaryKeyExists(ctx context.Context, tableName string, primaryKeys []string) (bool, error) {
cacheKeys := make([]string, 0, len(primaryKeys))
for _, primaryKey := range primaryKeys {
// primaryKey 已经是最终格式(单个值或已用":"连接的联合主键),直接传入
cacheKeys = append(cacheKeys, util.GenPrimaryCacheKey(c.InstanceId, tableName, primaryKey))
}
return c.cache.BatchKeyExist(ctx, cacheKeys)
Expand All @@ -146,10 +149,14 @@ func (c *Gorm2Cache) SearchKeyExists(ctx context.Context, tableName string, SQL
}

func (c *Gorm2Cache) BatchSetPrimaryKeyCache(ctx context.Context, tableName string, kvs []util.Kv) error {
for idx, kv := range kvs {
kvs[idx].Key = util.GenPrimaryCacheKey(c.InstanceId, tableName, kv.Key)
cacheKvs := make([]util.Kv, 0, len(kvs))
for _, kv := range kvs {
cacheKvs = append(cacheKvs, util.Kv{
Key: util.GenPrimaryCacheKey(c.InstanceId, tableName, kv.Key),
Value: kv.Value,
})
}
return c.cache.BatchSetKeys(ctx, kvs)
return c.cache.BatchSetKeys(ctx, cacheKvs)
}

func (c *Gorm2Cache) SetSearchCache(ctx context.Context, cacheValue string, tableName string,
Expand All @@ -169,7 +176,51 @@ func (c *Gorm2Cache) GetSearchCache(ctx context.Context, tableName string, sql s
func (c *Gorm2Cache) BatchGetPrimaryCache(ctx context.Context, tableName string, primaryKeys []string) ([]string, error) {
cacheKeys := make([]string, 0, len(primaryKeys))
for _, primaryKey := range primaryKeys {
// primaryKey 已经是最终格式(单个值或已用":"连接的联合主键),直接传入
cacheKeys = append(cacheKeys, util.GenPrimaryCacheKey(c.InstanceId, tableName, primaryKey))
}
return c.cache.BatchGetValues(ctx, cacheKeys)
}

// BatchGetUniqueCache 批量获取unique键缓存
func (c *Gorm2Cache) BatchGetUniqueCache(ctx context.Context, tableName string, uniqueIndexName string, uniqueKeys []string) ([]string, error) {
cacheKeys := make([]string, 0, len(uniqueKeys))
for _, uniqueKey := range uniqueKeys {
// uniqueKey 已经是最终格式(单个值或已用":"连接的联合unique键),直接传入
cacheKeys = append(cacheKeys, util.GenUniqueCacheKey(c.InstanceId, tableName, uniqueIndexName, uniqueKey))
}
return c.cache.BatchGetValues(ctx, cacheKeys)
}

// BatchSetUniqueCache 批量设置 unique 键缓存。不会修改调用方传入的 kvs。
func (c *Gorm2Cache) BatchSetUniqueCache(ctx context.Context, tableName string, uniqueIndexName string, kvs []util.Kv) error {
cacheKvs := make([]util.Kv, 0, len(kvs))
for _, kv := range kvs {
cacheKvs = append(cacheKvs, util.Kv{
Key: util.GenUniqueCacheKey(c.InstanceId, tableName, uniqueIndexName, kv.Key),
Value: kv.Value,
})
}
return c.cache.BatchSetKeys(ctx, cacheKvs)
}

// InvalidateUniqueCache 失效unique键缓存
func (c *Gorm2Cache) InvalidateUniqueCache(ctx context.Context, tableName string, uniqueIndexName string, uniqueKey string) error {
// uniqueKey 已经是最终格式(单个值或已用":"连接的联合unique键),直接传入
return c.cache.DeleteKey(ctx, util.GenUniqueCacheKey(c.InstanceId, tableName, uniqueIndexName, uniqueKey))
}

// BatchInvalidateUniqueCache 批量失效unique键缓存
func (c *Gorm2Cache) BatchInvalidateUniqueCache(ctx context.Context, tableName string, uniqueIndexName string, uniqueKeys []string) error {
cacheKeys := make([]string, 0, len(uniqueKeys))
for _, uniqueKey := range uniqueKeys {
// uniqueKey 已经是最终格式(单个值或已用":"连接的联合unique键),直接传入
cacheKeys = append(cacheKeys, util.GenUniqueCacheKey(c.InstanceId, tableName, uniqueIndexName, uniqueKey))
}
return c.cache.BatchDeleteKeys(ctx, cacheKeys)
}

// InvalidateAllUniqueCache 失效所有unique键缓存
func (c *Gorm2Cache) InvalidateAllUniqueCache(ctx context.Context, tableName string, uniqueIndexName string) error {
return c.cache.DeleteKeysWithPrefix(ctx, util.GenUniqueCachePrefix(c.InstanceId, tableName, uniqueIndexName))
}
145 changes: 145 additions & 0 deletions cache/cache_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package cache

import (
"context"
"os"
"strings"
"sync"
"testing"
"time"

"github.com/asjdf/gorm-cache/config"
"github.com/asjdf/gorm-cache/storage"
Expand Down Expand Up @@ -703,3 +707,144 @@ func TestQueryHandler_Bind_WithError(t *testing.T) {
}
}

// recordingStorage 记录 DeleteKeysWithPrefix 的调用,用于断言 schema-less 时是否仍失效 unique 缓存
type recordingStorage struct {
*storage.Memory
mu sync.Mutex
deletedPrefixes []string
}

func (r *recordingStorage) DeleteKeysWithPrefix(ctx context.Context, keyPrefix string) error {
r.mu.Lock()
r.deletedPrefixes = append(r.deletedPrefixes, keyPrefix)
r.mu.Unlock()
return r.Memory.DeleteKeysWithPrefix(ctx, keyPrefix)
}

// testUserWithUnique 带 unique 索引的模型,用于 schema-less fallback 测试
type testUserWithUnique struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex:idx_email"`
Username string `gorm:"uniqueIndex:idx_username"`
}

func (testUserWithUnique) TableName() string {
return "test_users_unique"
}

// TestAfterDelete_SchemaNil_StillInvalidatesUniqueCache 复现:当 Schema 为 nil 但 Model 有 unique 索引时,
// fallback 路径应通过解析 Model 得到 schema 并失效所有 unique 缓存,否则会残留过期 unique 缓存。
func TestAfterDelete_SchemaNil_StillInvalidatesUniqueCache(t *testing.T) {
rec := &recordingStorage{Memory: storage.NewMem(storage.DefaultMemStoreConfig)}
cfg := &config.CacheConfig{
CacheStorage: rec,
CacheTTL: 1000,
DebugMode: false,
InvalidateWhenUpdate: true,
CacheLevel: config.CacheLevelAll,
Tables: []string{"test_users_unique"},
}
cache := &Gorm2Cache{Config: cfg, stats: &stats{}}
cache.Init()

db := setupTestDB(t)
// 模拟 schema-less 场景:Schema 为 nil,但 Model 指向带 unique 索引的模型
db.Statement.Schema = nil
db.Statement.Model = &testUserWithUnique{}
db.Statement.Table = "test_users_unique"
db.RowsAffected = 1
db.Error = nil
db.Statement.Context = context.Background()
// 不设置 WHERE,使 getUniqueKeysFromWhereClause 返回空,走「失效所有 unique」的 fallback 路径

hook := AfterDelete(cache)
hook(db)

// 回调内是 goroutine,等待执行完
time.Sleep(200 * time.Millisecond)

rec.mu.Lock()
prefixes := append([]string(nil), rec.deletedPrefixes...)
rec.mu.Unlock()

// 应至少对两个 unique 索引做 DeleteKeysWithPrefix(idx_email, idx_username)
var uniquePrefixCount int
for _, p := range prefixes {
if strings.Contains(p, ":u:") && strings.Contains(p, "test_users_unique") {
uniquePrefixCount++
}
}
if uniquePrefixCount < 2 {
t.Errorf("schema-less fallback should invalidate all unique caches (expected >= 2 unique prefix deletes), got %d, prefixes: %v", uniquePrefixCount, prefixes)
}
// 同时应包含 idx_email 与 idx_username 的 prefix(util.GenUniqueCachePrefix 格式)
hasEmail := false
hasUsername := false
for _, p := range prefixes {
if strings.Contains(p, "idx_email") {
hasEmail = true
}
if strings.Contains(p, "idx_username") {
hasUsername = true
}
}
if !hasEmail || !hasUsername {
t.Errorf("expected unique prefix deletes for idx_email and idx_username, got prefixes: %v", prefixes)
}
}

// TestAfterUpdate_SchemaNil_StillInvalidatesUniqueCache 与 AfterDelete 对称:Schema 为 nil 时 update 也应失效 unique 缓存
func TestAfterUpdate_SchemaNil_StillInvalidatesUniqueCache(t *testing.T) {
rec := &recordingStorage{Memory: storage.NewMem(storage.DefaultMemStoreConfig)}
cfg := &config.CacheConfig{
CacheStorage: rec,
CacheTTL: 1000,
DebugMode: false,
InvalidateWhenUpdate: true,
CacheLevel: config.CacheLevelAll,
Tables: []string{"test_users_unique"},
}
cache := &Gorm2Cache{Config: cfg, stats: &stats{}}
cache.Init()

db := setupTestDB(t)
db.Statement.Schema = nil
db.Statement.Model = &testUserWithUnique{}
db.Statement.Table = "test_users_unique"
db.RowsAffected = 1
db.Error = nil
db.Statement.Context = context.Background()

hook := AfterUpdate(cache)
hook(db)

time.Sleep(200 * time.Millisecond)

rec.mu.Lock()
prefixes := append([]string(nil), rec.deletedPrefixes...)
rec.mu.Unlock()

var uniquePrefixCount int
for _, p := range prefixes {
if strings.Contains(p, ":u:") && strings.Contains(p, "test_users_unique") {
uniquePrefixCount++
}
}
if uniquePrefixCount < 2 {
t.Errorf("schema-less fallback should invalidate all unique caches (expected >= 2 unique prefix deletes), got %d, prefixes: %v", uniquePrefixCount, prefixes)
}
hasEmail := false
hasUsername := false
for _, p := range prefixes {
if strings.Contains(p, "idx_email") {
hasEmail = true
}
if strings.Contains(p, "idx_username") {
hasUsername = true
}
}
if !hasEmail || !hasUsername {
t.Errorf("expected unique prefix deletes for idx_email and idx_username, got prefixes: %v", prefixes)
}
}

Loading