mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
378 lines
10 KiB
Go
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)
|
|
}
|