Class: Lyra::Projections::CachedProjection

Inherits:
Object
  • Object
show all
Defined in:
lib/lyra/projections/cached_projection.rb

Overview

Cached projection layer using Solid Cache (or any Rails.cache backend).

In disabled projection mode, this caches reconstructed model state from events. Provides fast reads while keeping events as the source of truth.

Cache Strategy:

  • Individual records: cached by model class + id
  • Collections: cached by query fingerprint
  • Invalidation: on event publish for affected streams

Usage:

CachedProjection.find(User, 123)
CachedProjection.where(User, status: "active")
CachedProjection.invalidate(User, 123)

Constant Summary collapse

CACHE_VERSION =

Cache configuration

1
DEFAULT_EXPIRES_IN =
1.hour

Class Method Summary collapse

Class Method Details

.all(model_class) ⇒ Array<Hash>

Get all records (cached, use with caution on large datasets)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

Returns:

  • (Array<Hash>) —

    All record attributes



70
71
72
73
74
75
76
# File 'lib/lyra/projections/cached_projection.rb', line 70

def all(model_class)
  cache_key = collection_cache_key(model_class, :all)

  cache_fetch(cache_key, expires_in: 5.minutes) do
    build_all_from_events(model_class)
  end
end

.count(model_class, conditions = {}) ⇒ Integer

Count records (cached)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • conditions (Hash) (defaults to: {}) —

    Optional conditions

Returns:

  • (Integer) —

    Count of matching records



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/lyra/projections/cached_projection.rb', line 96

def count(model_class, conditions = {})
  if conditions.empty?
    cache_key = collection_cache_key(model_class, :count)
  else
    cache_key = query_cache_key(model_class, :count, conditions)
  end

  cache_fetch(cache_key, expires_in: 1.minute) do
    build_count_from_events(model_class, conditions)
  end
end

.exists?(model_class, id) ⇒ Boolean

Check if a record exists (cached)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • id (Integer, String) —

    The record ID

Returns:

  • (Boolean) —

    True if record exists and not destroyed



113
114
115
116
# File 'lib/lyra/projections/cached_projection.rb', line 113

def exists?(model_class, id)
  # Use find - if it returns data, it exists
  find(model_class, id).present?
end

.find(model_class, id, force: false) ⇒ Hash?

Find a single record by ID (cached)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • id (Integer, String) —

    The record ID

  • force (Boolean) (defaults to: false) —

    Bypass cache and rebuild

Returns:

  • (Hash, nil) —

    The record attributes or nil



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/lyra/projections/cached_projection.rb', line 32

def find(model_class, id, force: false)
  cache_key = record_cache_key(model_class, id)

  if force
    result = build_from_events(model_class, id)
    cache_write(cache_key, result) if result
    result
  else
    cache_fetch(cache_key) do
      build_from_events(model_class, id)
    end
  end
end

.find_by(model_class, attributes) ⇒ Hash?

Find a record by attributes (cached)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • attributes (Hash) —

    The attributes to match

Returns:

  • (Hash, nil) —

    The first matching record attributes or nil



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/lyra/projections/cached_projection.rb', line 51

def find_by(model_class, attributes)
  # Optimization: if looking up by :id only, use the fast find() path
  # which uses single-stream lookup instead of scanning all events
  if attributes.size == 1 && (attributes.key?(:id) || attributes.key?("id"))
    id = attributes[:id] || attributes["id"]
    return find(model_class, id)
  end

  cache_key = query_cache_key(model_class, :find_by, attributes)

  cache_fetch(cache_key, expires_in: 5.minutes) do
    build_find_by_from_events(model_class, attributes)
  end
end

.invalidate(model_class, id) ⇒ Object

Invalidate cache for a specific record

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • id (Integer, String) —

    The record ID



122
123
124
125
126
127
128
# File 'lib/lyra/projections/cached_projection.rb', line 122

def invalidate(model_class, id)
  # Invalidate individual record cache
  cache_delete(record_cache_key(model_class, id))

  # Invalidate collection caches (they may contain this record)
  invalidate_collection_caches(model_class)
end

.invalidate_all(model_class) ⇒ Object

Invalidate all caches for a model class

Parameters:

  • model_class (Class) —

    The ActiveRecord model class



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/lyra/projections/cached_projection.rb', line 133

def invalidate_all(model_class)
  # Use cache key prefix deletion if available
  prefix = "#{cache_namespace}/#{model_class.name}/"

  if cache_store.respond_to?(:delete_matched)
    cache_store.delete_matched("#{prefix}*")
  else
    # Fallback: invalidate known collection keys
    invalidate_collection_caches(model_class)
  end
end

.warm(model_class, id) ⇒ Object

Warm the cache for a record (call after event is stored)

Also invalidates collection caches (all, count) to ensure accurate counts after creates/updates.

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • id (Integer, String) —

    The record ID



152
153
154
155
156
157
158
159
# File 'lib/lyra/projections/cached_projection.rb', line 152

def warm(model_class, id)
  # Rebuild the individual record cache
  find(model_class, id, force: true)

  # Invalidate collection caches so count/all queries are rebuilt
  # This ensures assert_difference "Model.count" works correctly
  invalidate_collection_caches(model_class)
end

.where(model_class, conditions) ⇒ Array<Hash>

Query records with conditions (cached)

Parameters:

  • model_class (Class) —

    The ActiveRecord model class

  • conditions (Hash) —

    Query conditions

Returns:

  • (Array<Hash>) —

    Matching record attributes



83
84
85
86
87
88
89
# File 'lib/lyra/projections/cached_projection.rb', line 83

def where(model_class, conditions)
  cache_key = query_cache_key(model_class, :where, conditions)

  cache_fetch(cache_key, expires_in: 5.minutes) do
    build_where_from_events(model_class, conditions)
  end
end