Class: LowCardTables::LowCardTable::Cache

Inherits:
Object
  • Object
show all
Defined in:
lib/low_card_tables/low_card_table/cache.rb

Overview

This class is responsible for caching all the rows for a given low-card table, and then returning various subsets of those rows on demand.

This class is actually pretty simple, and it’s largely for one big reason: our cache is very simple – we cache the entire contents of the table, in memory, and the only way we update it is to throw out the entire cache and create a brand-new one. As such, from the inside, this class has no mechanisms whatsoever for updating the cache (as this object is thrown away and a whole new Cache object created when the cache is invalidated) nor are there any methods for worrying about what should be in cache, doing a LRU, or anything like that.

Instance Method Summary collapse

Constructor Details

#initialize(model_class, options = { }) ⇒ Cache

Creates a new Cache for the given model_class, which must be an ActiveRecord::Base subclass that has declared is_low_card_table.

options can contain:

:max_row_count

By default, the cache will raise a fatal error if trying to cache a low-card table that contains more than DEFAULT_MAX_ROW_COUNT (5,000) rows. This is provided so that we detect early on if you start storing data via the low-card system that is not, in fact, of low cardinality. However, if you really are using a low-card table properly and just happen to have more than 5,000 distinct combinations of values, you can increase this limit via this option.



22
23
24
25
26
27
28
29
30
31
# File 'lib/low_card_tables/low_card_table/cache.rb', line 22

def initialize(model_class, options = { })
  unless model_class.respond_to?(:is_low_card_table?) && model_class.is_low_card_table?
    raise ArgumentError, "You must supply a class that is a model class for a low-card table, not #{model_class.inspect}."
  end

  @model_class = model_class
  @options = options

  fill!
end

Instance Method Details

#all_rowsObject

Returns an Array of all models of the low-card table in the cache. The order is undefined.



58
59
60
# File 'lib/low_card_tables/low_card_table/cache.rb', line 58

def all_rows
  @rows_by_id.values
end

#ids_matching(hash_or_hashes = nil, &block) ⇒ Object

This behaves identically to #rows_matching, except that, everywhere a low-card model object would be returned, a simple integer ID is returned instead.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/low_card_tables/low_card_table/cache.rb', line 41

def ids_matching(hash_or_hashes = nil, &block)
  matching = rows_matching(hash_or_hashes, &block)

  out = case matching
  when Array then matching.map(&:id)
  when Hash then
    h = { }
    matching.each { |k,v| h[k] = v.map(&:id) }
    h
  when nil then nil
  else raise "Unknown return value from #rows_matching; this should never happen: #{matching.inspect}"
  end

  out
end

#loaded_atObject

At what time was this cache loaded? This is used (in conjunction with a cache-expiration policy object) to determine if the cache is stale or not.



35
36
37
# File 'lib/low_card_tables/low_card_table/cache.rb', line 35

def loaded_at
  @rows_read_at
end

#rows_for_ids(id_or_ids) ⇒ Object

Given a single numeric ID of a low-card row, returns that low-card model object.

Given an Array of one or more IDs of low-card rows, returns a Hash mapping each of those IDs to the corresponding low-card model object.

Raises LowCardTables::Errors::LowCardIdNotFoundError if any of the supplied IDs are not found in the cache. (It is up to calling code – in our case, the RowManager – to flush the cache and try again, if desired.)



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/low_card_tables/low_card_table/cache.rb', line 69

def rows_for_ids(id_or_ids)
  ids = Array(id_or_ids)

  missing_ids = [ ]
  out = { }

  ids.each do |id|
    r = @rows_by_id[id]
    if r
      out[id] = r
    else
      missing_ids << id
    end
  end

  unless missing_ids.length == 0
    raise LowCardTables::Errors::LowCardIdNotFoundError.new("Can't find IDs for low-card table #{@model_class.table_name}: #{missing_ids.join(", ")}", missing_ids)
  end

  if id_or_ids.kind_of?(Array)
    out
  else
    out[id_or_ids]
  end
end

#rows_matching(hash_or_hashes = nil, &block) ⇒ Object

Returns a subset of rows in the cache that match a specified set of conditions. (This can be all rows or no rows, depending on the conditions, or any set in between.)

You can specify conditions in the following ways:

  • A single Hash, passed as an argument. The return value will be an Array that contains zero or more instances of the low-card model class. Only instances that have columns matching the specified Hash will be returned.

  • An array of one or more Hashes, passed as an argument. The return value will be a Hash; as keys, it will have exactly the Hashes you passed in. The value for each key will be an array of zero or more instances of the low-card model class, each of which matches the corresponding key.

  • A block, passed to the method as a normal Ruby block. The return value will be an Array that contains zero or more instances of the low-card model class. The block will be invoked with every instance of the low-card model class, and only those instances where the block returns a value that evaluates to true will be included.

In the form where you pass in an array of one or more Hashes, it is possible for the same low-card row to show up in multiple values in the returned Hash, if it matches more than one of the supplied Hashes.

Note that all matching is done via LowCardTables::LowCardTable::Base#_low_card_row_matches_any_hash?. See that method for more documentation – by overriding it, you can change the behavior of this method.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/low_card_tables/low_card_table/cache.rb', line 114

def rows_matching(hash_or_hashes = nil, &block)
  hashes = hash_or_hashes || [ ]
  hashes = [ hashes ] unless hashes.kind_of?(Array)
  hashes.each { |h| raise ArgumentError, "You must supply Hashes, not: #{h.inspect}" unless h.kind_of?(Hash) }

  if block && hashes.length > 0
    raise ArgumentError, "You can supply either one or more Hashes to match against OR a block, but not both. Hashes: #{hashes.inspect}; block: #{block.inspect}"
  elsif (! block) && hashes.length == 0
    raise ArgumentError, "You must supply either one or more Hashes to match against or a block; you supplied neither."
  end


  if hashes.length > 0
    out = { }

    hashes.each { |h| out[h] = [ ] }

    @rows_by_id.each do |id,r|
      hashes.each do |h|
        out[h] << r if r._low_card_row_matches_any_hash?([ h.with_indifferent_access ])
      end
    end

    if hash_or_hashes.kind_of?(Array)
      out
    else
      out[hash_or_hashes]
    end
  else
    @rows_by_id.values.select { |r| r._low_card_row_matches_block?(block) }
  end
end