tercul-backend/internal/repositories/cached_repository.go
Damir Mukimov fa336cacf3
wip
2025-09-01 00:43:59 +02:00

378 lines
10 KiB
Go

package repositories
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
"tercul/internal/platform/cache"
"tercul/internal/platform/log"
)
// simpleKeyGenerator implements the cache.KeyGenerator interface
type simpleKeyGenerator struct {
prefix string
}
// EntityKey generates a key for an entity by ID
func (g *simpleKeyGenerator) EntityKey(entityType string, id uint) string {
return g.prefix + entityType + ":id:" + fmt.Sprintf("%d", id)
}
// ListKey generates a key for a list of entities
func (g *simpleKeyGenerator) ListKey(entityType string, page, pageSize int) string {
return g.prefix + entityType + ":list:" + fmt.Sprintf("%d:%d", page, pageSize)
}
// QueryKey generates a key for a custom query
func (g *simpleKeyGenerator) QueryKey(entityType string, queryName string, params ...interface{}) string {
key := g.prefix + entityType + ":" + queryName
for _, param := range params {
key += ":" + fmt.Sprintf("%v", param)
}
return key
}
// CachedRepository wraps a BaseRepository with caching functionality
type CachedRepository[T any] struct {
repo BaseRepository[T]
cache cache.Cache
keyGenerator cache.KeyGenerator
entityType string
cacheExpiry time.Duration
cacheEnabled bool
}
// NewCachedRepository creates a new CachedRepository
func NewCachedRepository[T any](
repo BaseRepository[T],
cache cache.Cache,
keyGenerator cache.KeyGenerator,
entityType string,
cacheExpiry time.Duration,
) *CachedRepository[T] {
if keyGenerator == nil {
// Create a simple key generator
keyGenerator = &simpleKeyGenerator{prefix: "tercul:"}
}
if cacheExpiry == 0 {
cacheExpiry = 1 * time.Hour // Default expiry of 1 hour
}
return &CachedRepository[T]{
repo: repo,
cache: cache,
keyGenerator: keyGenerator,
entityType: entityType,
cacheExpiry: cacheExpiry,
cacheEnabled: true,
}
}
// EnableCache enables caching
func (r *CachedRepository[T]) EnableCache() {
r.cacheEnabled = true
}
// DisableCache disables caching
func (r *CachedRepository[T]) DisableCache() {
r.cacheEnabled = false
}
// Create adds a new entity to the database
func (r *CachedRepository[T]) Create(ctx context.Context, entity *T) error {
err := r.repo.Create(ctx, entity)
if err != nil {
return err
}
// Invalidate cache for this entity type
if r.cacheEnabled {
if redisCache, ok := r.cache.(*cache.RedisCache); ok {
if err := redisCache.InvalidateEntityType(ctx, r.entityType); err != nil {
log.LogWarn("Failed to invalidate cache",
log.F("entityType", r.entityType),
log.F("error", err))
}
}
}
return nil
}
// CreateInTx creates an entity within a transaction
func (r *CachedRepository[T]) CreateInTx(ctx context.Context, tx *gorm.DB, entity *T) error {
return r.repo.CreateInTx(ctx, tx, entity)
}
// GetByID retrieves an entity by its ID
func (r *CachedRepository[T]) GetByID(ctx context.Context, id uint) (*T, error) {
if !r.cacheEnabled {
return r.repo.GetByID(ctx, id)
}
cacheKey := r.keyGenerator.EntityKey(r.entityType, id)
var entity T
err := r.cache.Get(ctx, cacheKey, &entity)
if err == nil {
// Cache hit
log.LogDebug("Cache hit",
log.F("entityType", r.entityType),
log.F("id", id))
return &entity, nil
}
// Cache miss, get from database
log.LogDebug("Cache miss",
log.F("entityType", r.entityType),
log.F("id", id))
entity_ptr, err := r.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
// Store in cache
if err := r.cache.Set(ctx, cacheKey, entity_ptr, r.cacheExpiry); err != nil {
log.LogWarn("Failed to cache entity",
log.F("entityType", r.entityType),
log.F("id", id),
log.F("error", err))
}
return entity_ptr, nil
}
// GetByIDWithOptions retrieves an entity by its ID with query options
func (r *CachedRepository[T]) GetByIDWithOptions(ctx context.Context, id uint, options *QueryOptions) (*T, error) {
// For complex queries with options, we don't cache as the cache key would be too complex
return r.repo.GetByIDWithOptions(ctx, id, options)
}
// Update updates an existing entity
func (r *CachedRepository[T]) Update(ctx context.Context, entity *T) error {
err := r.repo.Update(ctx, entity)
if err != nil {
return err
}
// Invalidate cache for this entity
if r.cacheEnabled {
// Invalidate specific entity cache
cacheKey := r.keyGenerator.EntityKey(r.entityType, 0) // We don't have ID here, so invalidate all
if err := r.cache.Delete(ctx, cacheKey); err != nil {
log.LogWarn("Failed to invalidate entity cache",
log.F("entityType", r.entityType),
log.F("error", err))
}
// Invalidate list caches
if redisCache, ok := r.cache.(*cache.RedisCache); ok {
if err := redisCache.InvalidateEntityType(ctx, r.entityType); err != nil {
log.LogWarn("Failed to invalidate cache",
log.F("entityType", r.entityType),
log.F("error", err))
}
}
}
return nil
}
// UpdateInTx updates an entity within a transaction
func (r *CachedRepository[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *T) error {
return r.repo.UpdateInTx(ctx, tx, entity)
}
// Delete removes an entity by its ID
func (r *CachedRepository[T]) Delete(ctx context.Context, id uint) error {
err := r.repo.Delete(ctx, id)
if err != nil {
return err
}
// Invalidate cache for this entity
if r.cacheEnabled {
cacheKey := r.keyGenerator.EntityKey(r.entityType, id)
if err := r.cache.Delete(ctx, cacheKey); err != nil {
log.LogWarn("Failed to invalidate entity cache",
log.F("entityType", r.entityType),
log.F("id", id),
log.F("error", err))
}
// Invalidate list caches
if redisCache, ok := r.cache.(*cache.RedisCache); ok {
if err := redisCache.InvalidateEntityType(ctx, r.entityType); err != nil {
log.LogWarn("Failed to invalidate cache",
log.F("entityType", r.entityType),
log.F("error", err))
}
}
}
return nil
}
// DeleteInTx removes an entity by its ID within a transaction
func (r *CachedRepository[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
return r.repo.DeleteInTx(ctx, tx, id)
}
// List returns a paginated list of entities
func (r *CachedRepository[T]) List(ctx context.Context, page, pageSize int) (*PaginatedResult[T], error) {
if !r.cacheEnabled {
return r.repo.List(ctx, page, pageSize)
}
cacheKey := r.keyGenerator.ListKey(r.entityType, page, pageSize)
var result PaginatedResult[T]
err := r.cache.Get(ctx, cacheKey, &result)
if err == nil {
// Cache hit
log.LogDebug("Cache hit for list",
log.F("entityType", r.entityType),
log.F("page", page),
log.F("pageSize", pageSize))
return &result, nil
}
// Cache miss, get from database
log.LogDebug("Cache miss for list",
log.F("entityType", r.entityType),
log.F("page", page),
log.F("pageSize", pageSize))
result_ptr, err := r.repo.List(ctx, page, pageSize)
if err != nil {
return nil, err
}
// Store in cache
if err := r.cache.Set(ctx, cacheKey, result_ptr, r.cacheExpiry); err != nil {
log.LogWarn("Failed to cache list",
log.F("entityType", r.entityType),
log.F("page", page),
log.F("pageSize", pageSize),
log.F("error", err))
}
return result_ptr, nil
}
// ListWithOptions returns entities with query options
func (r *CachedRepository[T]) ListWithOptions(ctx context.Context, options *QueryOptions) ([]T, error) {
// For complex queries with options, we don't cache as the cache key would be too complex
return r.repo.ListWithOptions(ctx, options)
}
// ListAll returns all entities (use with caution for large datasets)
func (r *CachedRepository[T]) ListAll(ctx context.Context) ([]T, error) {
if !r.cacheEnabled {
return r.repo.ListAll(ctx)
}
cacheKey := r.keyGenerator.QueryKey(r.entityType, "listAll")
var entities []T
err := r.cache.Get(ctx, cacheKey, &entities)
if err == nil {
// Cache hit
log.LogDebug("Cache hit for listAll",
log.F("entityType", r.entityType))
return entities, nil
}
// Cache miss, get from database
log.LogDebug("Cache miss for listAll",
log.F("entityType", r.entityType))
entities, err = r.repo.ListAll(ctx)
if err != nil {
return nil, err
}
// Store in cache
if err := r.cache.Set(ctx, cacheKey, entities, r.cacheExpiry); err != nil {
log.LogWarn("Failed to cache listAll",
log.F("entityType", r.entityType),
log.F("error", err))
}
return entities, nil
}
// Count returns the total number of entities
func (r *CachedRepository[T]) Count(ctx context.Context) (int64, error) {
if !r.cacheEnabled {
return r.repo.Count(ctx)
}
cacheKey := r.keyGenerator.QueryKey(r.entityType, "count")
var count int64
err := r.cache.Get(ctx, cacheKey, &count)
if err == nil {
// Cache hit
log.LogDebug("Cache hit for count",
log.F("entityType", r.entityType))
return count, nil
}
// Cache miss, get from database
log.LogDebug("Cache miss for count",
log.F("entityType", r.entityType))
count, err = r.repo.Count(ctx)
if err != nil {
return 0, err
}
// Store in cache
if err := r.cache.Set(ctx, cacheKey, count, r.cacheExpiry); err != nil {
log.LogWarn("Failed to cache count",
log.F("entityType", r.entityType),
log.F("error", err))
}
return count, nil
}
// CountWithOptions returns the count with query options
func (r *CachedRepository[T]) CountWithOptions(ctx context.Context, options *QueryOptions) (int64, error) {
// For complex queries with options, we don't cache as the cache key would be too complex
return r.repo.CountWithOptions(ctx, options)
}
// FindWithPreload retrieves an entity by its ID with preloaded relationships
func (r *CachedRepository[T]) FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error) {
// For preloaded queries, we don't cache as the cache key would be too complex
return r.repo.FindWithPreload(ctx, preloads, id)
}
// GetAllForSync returns entities in batches for synchronization
func (r *CachedRepository[T]) GetAllForSync(ctx context.Context, batchSize, offset int) ([]T, error) {
// For sync operations, we don't cache as the data is constantly changing
return r.repo.GetAllForSync(ctx, batchSize, offset)
}
// Exists checks if an entity exists by ID
func (r *CachedRepository[T]) Exists(ctx context.Context, id uint) (bool, error) {
// For existence checks, we don't cache as the result can change frequently
return r.repo.Exists(ctx, id)
}
// BeginTx starts a new transaction
func (r *CachedRepository[T]) BeginTx(ctx context.Context) (*gorm.DB, error) {
return r.repo.BeginTx(ctx)
}
// WithTx executes a function within a transaction
func (r *CachedRepository[T]) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return r.repo.WithTx(ctx, fn)
}