package repositories import ( "context" "fmt" "time" "gorm.io/gorm" "tercul/cache" "tercul/logger" ) // 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 { logger.LogWarn("Failed to invalidate cache", logger.F("entityType", r.entityType), logger.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 logger.LogDebug("Cache hit", logger.F("entityType", r.entityType), logger.F("id", id)) return &entity, nil } // Cache miss, get from database logger.LogDebug("Cache miss", logger.F("entityType", r.entityType), logger.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 { logger.LogWarn("Failed to cache entity", logger.F("entityType", r.entityType), logger.F("id", id), logger.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 { logger.LogWarn("Failed to invalidate entity cache", logger.F("entityType", r.entityType), logger.F("error", err)) } // Invalidate list caches if redisCache, ok := r.cache.(*cache.RedisCache); ok { if err := redisCache.InvalidateEntityType(ctx, r.entityType); err != nil { logger.LogWarn("Failed to invalidate cache", logger.F("entityType", r.entityType), logger.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 { logger.LogWarn("Failed to invalidate entity cache", logger.F("entityType", r.entityType), logger.F("id", id), logger.F("error", err)) } // Invalidate list caches if redisCache, ok := r.cache.(*cache.RedisCache); ok { if err := redisCache.InvalidateEntityType(ctx, r.entityType); err != nil { logger.LogWarn("Failed to invalidate cache", logger.F("entityType", r.entityType), logger.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 logger.LogDebug("Cache hit for list", logger.F("entityType", r.entityType), logger.F("page", page), logger.F("pageSize", pageSize)) return &result, nil } // Cache miss, get from database logger.LogDebug("Cache miss for list", logger.F("entityType", r.entityType), logger.F("page", page), logger.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 { logger.LogWarn("Failed to cache list", logger.F("entityType", r.entityType), logger.F("page", page), logger.F("pageSize", pageSize), logger.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 logger.LogDebug("Cache hit for listAll", logger.F("entityType", r.entityType)) return entities, nil } // Cache miss, get from database logger.LogDebug("Cache miss for listAll", logger.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 { logger.LogWarn("Failed to cache listAll", logger.F("entityType", r.entityType), logger.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 logger.LogDebug("Cache hit for count", logger.F("entityType", r.entityType)) return count, nil } // Cache miss, get from database logger.LogDebug("Cache miss for count", logger.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 { logger.LogWarn("Failed to cache count", logger.F("entityType", r.entityType), logger.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) }