class RecordCache::Index

Constants

MAX_FETCH
NULL

Attributes

expiry[R]
fields[R]
index_field[R]
limit[R]
model_class[R]
name[R]
order_by[R]
prefix[R]

Public Class Methods

disable_db() click to toggle source
# File lib/record_cache/index.rb, line 224
def self.disable_db
  @@disable_db = true
end
enable_db() click to toggle source
# File lib/record_cache/index.rb, line 228
def self.enable_db
  @@disable_db = false
end
new(opts) click to toggle source
# File lib/record_cache/index.rb, line 11
def initialize(opts)
  raise ':by => index_field required for cache'       if opts[:by].nil?
  raise 'explicit name or prefix required with scope' if opts[:scope] and opts[:name].nil? and opts[:prefix].nil?

  @auto_name     = opts[:name].nil?
  @write_ahead   = opts[:write_ahead]
  @cache         = opts[:cache].kind_of?(Symbol) ? Memcache.pool[opts[:cache]] : (opts[:cache] || CACHE)
  @expiry        = opts[:expiry]
  @model_class   = opts[:class]
  @set_class     = opts[:set_class] || "#{@model_class}Set"
  @index_field   = opts[:by].to_s
  @fields        = opts[:fields].collect {|field| field.to_s}
  @prefix        = opts[:prefix]
  @name          = ( opts[:name] || [prefix, 'by', index_field].compact.join('_') ).to_s
  @order_by      = opts[:order_by]
  @limit         = opts[:limit]
  @disallow_null = opts[:null] == false
  @scope_query   = opts[:scope] || {}
end

Public Instance Methods

auto_name?() click to toggle source
# File lib/record_cache/index.rb, line 43
def auto_name?;     @auto_name;     end
cache() click to toggle source
# File lib/record_cache/index.rb, line 35
def cache
  if RecordCache.config[:thread_safe]
    Thread.current[:record_cache] ||= @cache.clone
  else
    @cache
  end
end
cached_set(id) click to toggle source
# File lib/record_cache/index.rb, line 251
def cached_set(id)
  # Used for debugging. Gives you the RecordCache::Set that is currently in the cache.
  id = stringify([id]).first
  cache.in_namespace(namespace) do
    cache.get(id)
  end
end
disallow_null?() click to toggle source
# File lib/record_cache/index.rb, line 45
def disallow_null?; @disallow_null; end
field_lookup(keys, model_class, field, flag = nil) click to toggle source
# File lib/record_cache/index.rb, line 141
def field_lookup(keys, model_class, field, flag = nil)
  keys = [*keys]
  keys = stringify(keys)
  field = field.to_s if field
  records_by_key = get_records(keys)

  field_by_index = {}
  all_fields = []
  keys.each do |key|
    records = records_by_key[key]
    fields = field ? records.fields(field, model_class) : records.all_fields(model_class, :except => index_field)
    if flag == :all
      all_fields.concat(fields)
    elsif flag == :first
      next if fields.empty?
      field_by_index[index_column.type_cast(key)] = fields.first
    else
      field_by_index[index_column.type_cast(key)] = fields
    end
  end
  if flag == :all
    all_fields.uniq
  else
    field_by_index
  end
end
fields_hash() click to toggle source
# File lib/record_cache/index.rb, line 64
def fields_hash
  if @fields_hash.nil?
    fields = full_record? ? model_class.column_names : self.fields
    md5 = Digest::MD5::new
    md5 << fields.sort.join(',')
    @fields_hash = md5.hexdigest
  end
  @fields_hash
end
find_by_field(keys, model_class, type) click to toggle source
# File lib/record_cache/index.rb, line 108
def find_by_field(keys, model_class, type)
  keys = [keys] if not keys.kind_of?(Array)
  keys = stringify(keys)
  records_by_key = get_records(keys)

  case type
  when :first
    keys.each do |key|
      model = records_by_key[key].instantiate_first(model_class, full_record?)
      return model if model
    end
    return nil
  when :all
    models = []
    keys.each do |key|
      models.concat( records_by_key[key].instantiate(model_class, full_record?) )
    end
    models
  when :set, :ids
    ids = []
    keys.each do |key|
      ids.concat( records_by_key[key].ids(model_class) )
    end
    type == :set ? set_class.new(ids) : ids
  when :raw
    raw_records = []
    keys.each do |key|
      raw_records.concat( records_by_key[key].records(model_class) )
    end
    raw_records
  end
end
find_by_ids(ids, model_class) click to toggle source
# File lib/record_cache/index.rb, line 74
def find_by_ids(ids, model_class)
  expects_array = ids.first.kind_of?(Array)
  ids = ids.flatten.compact.collect {|id| id.to_i}
  ids = stringify(ids)

  if ids.empty?
    return [] if expects_array
    raise ActiveRecord::RecordNotFound, "Couldn't find #{model_class} without an ID"
  end

  records_by_id = get_records(ids)

  models = ids.collect do |id|
    records = records_by_id[id]
    model   = records.instantiate_first(model_class, full_record?) if records

    # try to get record from db again before we throw an exception
    if model.nil?
      invalidate(id)
      records = get_records([id])[id]
      model   = records.instantiate_first(model_class, full_record?) if records
    end

    raise ActiveRecord::RecordNotFound, "Couldn't find #{model_class} with ID #{id}" unless model
    model
  end

  if models.size == 1 and not expects_array
    models.first
  else
    models
  end
end
find_method_name(type) click to toggle source
# File lib/record_cache/index.rb, line 232
def find_method_name(type)
  if name =~ /(^|_)by_/
    if type == :first
      "find_#{name}"
    else
      "find_#{type}_#{name}"
    end
  else
    case type
    when :all
      "find_#{name}"
    when :first
      "find_#{type}_#{name.singularize}"
    else
      "find_#{name.singularize}_#{type}"
    end
  end
end
full_record?() click to toggle source
# File lib/record_cache/index.rb, line 47
def full_record?
  fields.empty?
end
includes_id?() click to toggle source
# File lib/record_cache/index.rb, line 51
def includes_id?
  full_record? or fields.include?('id')
end
invalidate(*keys) click to toggle source
# File lib/record_cache/index.rb, line 168
def invalidate(*keys)
  return if model_class.record_cache_config[:disable_write]

  keys = stringify(keys)
  cache.in_namespace(namespace) do
    keys.each do |key|
      cache.delete(key)
    end
  end
end
invalidate_from_conditions(conditions) click to toggle source
# File lib/record_cache/index.rb, line 186
def invalidate_from_conditions(conditions)
  invalidate_from_conditions_lambda(conditions).call
end
invalidate_from_conditions_lambda(conditions) click to toggle source
# File lib/record_cache/index.rb, line 179
def invalidate_from_conditions_lambda(conditions)
  sql = "SELECT #{index_field} FROM #{table_name} "
  model_class.send(:add_conditions!, sql, conditions, model_class.send(:scope, :find))
  ids = db.select_values(sql)
  lambda { invalidate(*ids) }
end
invalidate_model(model) click to toggle source
# File lib/record_cache/index.rb, line 190
def invalidate_model(model)
  attribute     = model.read_attribute(index_field)
  attribute_was = model.attr_was(index_field)
  if scope.match_previous?(model)
    if write_ahead?
      remove_from_cache(model)
    else
      now_and_later do
        invalidate(attribute_was)
      end
    end
  end

  if scope.match_current?(model)
    if write_ahead?
      add_to_cache(model)
    elsif not (scope.match_previous?(model) and attribute_was == attribute)
      now_and_later do
        invalidate(attribute)
      end
    end
  end
end
namespace() click to toggle source
# File lib/record_cache/index.rb, line 59
def namespace
  record_cache_class = model_class.record_cache_class
  "#{record_cache_class.name}_#{record_cache_class.version}_#{RecordCache.version}_#{fields_hash}:#{name}"
end
scope() click to toggle source
# File lib/record_cache/index.rb, line 219
def scope
  @scope ||= Scope.new(model_class, scope_query)
end
scope_query() click to toggle source
# File lib/record_cache/index.rb, line 214
def scope_query
  @scope_query[:type] ||= model_class.to_s if sub_class?
  @scope_query
end
set_class() click to toggle source
# File lib/record_cache/index.rb, line 55
def set_class
  @set_class.constantize
end
to_s() click to toggle source
# File lib/record_cache/index.rb, line 31
def to_s
  "<RecordCache::Index #{@model_class} by #{@index_field} (#{@name})>"
end
write_ahead?() click to toggle source
# File lib/record_cache/index.rb, line 44
def write_ahead?;   @write_ahead;   end

Private Instance Methods

add_to_cache(model) click to toggle source
# File lib/record_cache/index.rb, line 329
def add_to_cache(model)
  return if model_class.record_cache_config[:disable_write]

  record = model_to_record(model)
  return unless record
  key = record[index_field] || NULL

  now_and_later do
    cache.in_namespace(namespace) do
      cache.with_lock(key) do
        if records = cache.get(key)
          records.delete(record)
          records << record
          records.sort!(order_by) if order_by
          records.limit!(limit)   if limit
          cache.set(key, records)
        end
      end
    end
  end
end
base_class?() click to toggle source
# File lib/record_cache/index.rb, line 364
def base_class?
  @base_class ||= single_table_inheritance? and model_class == model_class.base_class
end
db() click to toggle source
# File lib/record_cache/index.rb, line 396
def db
  RecordCache.db(model_class)
end
get_records(keys) click to toggle source
# File lib/record_cache/index.rb, line 262
def get_records(keys)
  cache.in_namespace(namespace) do
    opts = {
      :expiry        => expiry,
      :disable_write => model_class.record_cache_config[:disable_write],
    }
    cache.get_some(keys, opts) do |keys_to_fetch|
      raise 'db access is disabled' if @@disable_db
      fetched_records = {}
      keys_to_fetch.each do |key|
        fetched_records[key] = RecordCache::Set.new(model_class, fields_hash)
      end

      keys_to_fetch.each_slice(MAX_FETCH) do |keys_batch|
        sql = "SELECT #{select_fields} FROM #{table_name} WHERE (#{in_clause(keys_batch)})"
        sql << " AND #{scope.conditions}" if not scope.empty?
        sql << " ORDER BY #{order_by}"    if order_by
        sql << " LIMIT #{limit}"          if limit

        db.select_all(sql).each do |record|
          key = record[index_field] || NULL
          if fetched_records[key]
            fetched_records[key] << record
          else
            raise "no records found in #{self} for #{key.inspect}. existing keys: #{fetched_records.keys.inspect}"
          end
        end
      end
      fetched_records
    end
  end
end
in_clause(keys) click to toggle source
# File lib/record_cache/index.rb, line 300
def in_clause(keys)
  conditions = []
  conditions << "#{index_field} IS NULL" if keys.delete(NULL)

  if keys.any?
    values = keys.collect {|value| quote_index_value(value)}.join(',')
    conditions << "#{index_field} IN (#{values})"
  end
  conditions.join(' OR ')
end
index_column() click to toggle source
# File lib/record_cache/index.rb, line 380
def index_column
  @index_column ||= model_class.columns_hash[index_field]
end
model_to_record(model) click to toggle source
# File lib/record_cache/index.rb, line 295
def model_to_record(model)
  sql = "SELECT #{select_fields} FROM #{table_name} WHERE id = #{model.id}"
  db.select_all(sql).first
end
quote_index_value(value) click to toggle source
# File lib/record_cache/index.rb, line 376
def quote_index_value(value)
  model_class.quote_value(value, index_column)
end
remove_from_cache(model) click to toggle source
# File lib/record_cache/index.rb, line 311
def remove_from_cache(model)
  return if model_class.record_cache_config[:disable_write]

  record = model.attributes
  key    = model.attr_was(index_field) || NULL

  now_and_later do
    cache.in_namespace(namespace) do
      cache.with_lock(key) do
        if records = cache.get(key)
          records.delete(record)
          cache.set(key, records)
        end
      end
    end
  end
end
select_fields() click to toggle source
# File lib/record_cache/index.rb, line 351
def select_fields
  if @select_fields.nil?
    if full_record?
      @select_fields = model_class.respond_to?(:default_select, true) ? model_class.send(:default_select, nil) : '*'
    else
      @select_fields = [index_field, 'id'] + fields
      @select_fields << 'type' if base_class?
      @select_fields = @select_fields.uniq.join(', ')
    end
  end
  @select_fields
end
single_table_inheritance?() click to toggle source
# File lib/record_cache/index.rb, line 372
def single_table_inheritance?
  @single_table_inheritance ||= model_class.columns_hash.has_key?(model_class.inheritance_column)
end
stringify(keys) click to toggle source
# File lib/record_cache/index.rb, line 388
def stringify(keys)
  keys.compact! if disallow_null?
  keys.collect do |key|
    key = key.nil? ? NULL : key.to_s
    index_column.number? ? key.strip : key
  end.uniq
end
sub_class?() click to toggle source
# File lib/record_cache/index.rb, line 368
def sub_class?
  @sub_class ||= single_table_inheritance? and model_class != model_class.base_class
end
table_name() click to toggle source
# File lib/record_cache/index.rb, line 384
def table_name
  model_class.table_name
end