cache_index所做的就是新增一个fetch_by_XXX方法(uniq的话还有_bang方法)以及fetch_id_by_XXX方法

调用fetch_by_XXX时,会先用fetch_id_by_XXX根据列值查出id,然后再用fetch_by_id找出对象

[70] pry(main)> h = Human.fetch_by_name 'zed'
   (0.1ms)  SELECT "humen"."id" FROM "humen" WHERE "humen"."name" = ?  [["name", "zed"]]
[IdentityCache] cache miss for IDC:7:attrs:Human:id:name:7416271391315032905
  Human Load (0.1ms)  SELECT "humen".* FROM "humen" WHERE "humen"."id" = 3
[IdentityCache] cache miss for IDC:7:blob:Human:5897275080375446043:3 (multi)
=> [#<human:0x007f57dd69a7a8 id:="" 3,="" name:="" "zed",="" created_at:="" sun,="" 30="" jul="" 2017="" 15:14:01="" utc="" +00:00,="" updated_at:="" wed,="" 16="" aug="" 2017="" 14:52:13="" utc="" +00:00="">]</human:0x007f57dd69a7a8>


源码如下

# identity_cache-0.5.1/lib/identity_cache/configuration_dsl.rb
def cache_index(*fields)
  raise NotImplementedError, "Cache indexes need an enabled primary index" unless primary_cache_index_enabled
  options = fields.extract_options!
  unique = options[:unique] || false
  cache_attribute_by_alias('primary_key', 'id', by: fields, unique: unique)

  field_list = fields.join("_and_")
  arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')

  if unique
    self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
      def fetch_by_#{field_list}(#{arg_list}, options={})
        id = fetch_id_by_#{field_list}(#{arg_list})
        id && fetch_by_id(id, options)
      end

      # exception throwing variant
      def fetch_by_#{field_list}!(#{arg_list}, options={})
        fetch_by_#{field_list}(#{arg_list}, options) or raise ActiveRecord::RecordNotFound
      end
    CODE
  else
    self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
      def fetch_by_#{field_list}(#{arg_list}, options={})
        ids = fetch_id_by_#{field_list}(#{arg_list})
        ids.empty? ? ids : fetch_multi(ids, options)
      end
    CODE
  end
end

def cache_attribute_by_alias(attribute, alias_name, options)
  ensure_base_model
  options[:by] ||= :id
  alias_name = alias_name.to_sym
  unique = options[:unique].nil? ? true : !!options[:unique]
  fields = Array(options[:by])

  self.cache_indexes.push [alias_name, fields, unique]

  field_list = fields.join("_and_")
  arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')

  self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
    def fetch_#{alias_name}_by_#{field_list}(#{arg_list})
      attribute_dynamic_fetcher(#{attribute}, #{fields.inspect}, [#{arg_list}], #{unique})
    end
  CODE
end

def attribute_dynamic_fetcher(attribute, fields, values, unique_index)
  raise_if_scoped

  if should_use_cache?
    cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
    IdentityCache.fetch(cache_key) do
      dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
    end
  else
    dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
  end
end

def dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
  query = reorder(nil).where(Hash[fields.zip(values)])
  query = query.limit(1) if unique_index
  results = query.pluck(attribute)
  unique_index ? results.first : results
end


为什么无论是否uniq,fetch_id_by_XXX查出的id/ids用到fetch_by_id去都没关系?因为fetch_by_id是where(primary_key => id).first的,如下

# identity_cache-0.5.1/lib/identity_cache/query_api.rb
def resolve_cache_miss(id)
  record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
  if record
    preload_id_embedded_associations([record])
    record.readonly! if IdentityCache.fetch_read_only_records && should_use_cache?
  end
  record
end


fetch_by_id自然是有缓存,以前研究过了。而fetch_id_by_XXX,当然也有缓存(id),其key规则如下,需要依靠列名和列值

# identity_cache-0.5.1/lib/identity_cache/cache_key_generation.rb
module ClassMethods

  def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique)
    unique_indicator = unique ? '' : 's'
    "#{rails_cache_key_namespace}" \
      "attr#{unique_indicator}" \
      ":#{base_class.name}" \
      ":#{attribute}" \
      ":#{rails_cache_string_for_fields_and_values(fields, values)}"
  end

  private
  def rails_cache_string_for_fields_and_values(fields, values)
    "#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
  end
end


其中transaction_changed_attributes来自IDC依赖的一个gem,如下

module ArTransactionChanges
  def _run_commit_callbacks
    super
  ensure
    @transaction_changed_attributes = nil
  end

  def _run_rollback_callbacks
    super
  ensure
    @transaction_changed_attributes = nil
  end

  def transaction_changed_attributes
    @transaction_changed_attributes ||= HashWithIndifferentAccess.new
  end

  def write_attribute(attr_name, value) # override
    attr_name = attr_name.to_s
    old_value = attributes[attr_name]
    ret = super
    unless transaction_changed_attributes.key?(attr_name) || value == old_value
      transaction_changed_attributes[attr_name] = old_value
    end
    ret
  end
end


当要做缓存失效时,除了删掉id => object缓存,还要删掉[fields, values] => id的缓存。

如果不是新记录(所谓新记录:pk列有变且其旧值为nil),则是更新或删除,应根据cache列的旧值查找缓存来删除,以防fetch旧值时仍命中此记录

如果不是删记录,则是更新或新增,应根据cache列的新值查找缓存来删除,以防fetch新值时命中的缓存不包含此记录

# identity_cache-0.5.1/lib/identity_cache/query_api.rb
def expire_attribute_indexes
  cache_indexes.each do |(attribute, fields, unique)|
    unless was_new_record?
      old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
      IdentityCache.cache.delete(old_cache_attribute_key)
    end
    unless destroyed?
      new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
      if new_cache_attribute_key != old_cache_attribute_key
        IdentityCache.cache.delete(new_cache_attribute_key)
      end
    end
  end
end

def was_new_record?
  pk = self.class.primary_key
  !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
end


新值旧值的逻辑如下

# identity_cache-0.5.1/lib/identity_cache/cache_key_generation.rb
def attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
  self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields), unique)
end

def attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
  self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields), unique)
end

def current_values_for_fields(fields)
  fields.collect {|field| self.send(field)}
end

def old_values_for_fields(fields)
  fields.map do |field|
    field_string = field.to_s
    if destroyed? && transaction_changed_attributes.has_key?(field_string)
      transaction_changed_attributes[field_string]
    elsif persisted? && transaction_changed_attributes.has_key?(field_string)
      transaction_changed_attributes[field_string]
    else
      self.send(field)
    end
  end
end


运行试试,假设Human.cache_index :name,可见create删一次,update删两次,destroy删一次(说的都是expire_attribute_indexes)

[14] pry(main)> leo = Human.new name: 'leo'
=> #<human:0x007f10ebe1ee68 id:="" nil,="" name:="" "leo",="" created_at:="" nil,="" updated_at:="" nil="">
[15] pry(main)> leo.save
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "humen" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "leo"], ["created_at", "2017-08-17 15:11:27.009507"], ["updated_at", "2017-08-17 15:11:27.009507"]]
   (1.9ms)  commit transaction
[IdentityCache] expiring=Human expiring_id=8 expiring_last_updated_at=
[IdentityCache] delete recorded for IDC:7:blob:Human:5897275080375446043:8
[IdentityCache] delete recorded for IDC:7:attrs:Human:id:name:15104357434540190954
=> true
[16] pry(main)> leo.update name: 'neo'
   (0.1ms)  begin transaction
  SQL (0.2ms)  UPDATE "humen" SET "name" = ?, "updated_at" = ? WHERE "humen"."id" = ?  [["name", "neo"], ["updated_at", "2017-08-17 15:20:08.148600"], ["id", 8]]
   (2.0ms)  commit transaction
[IdentityCache] expiring=Human expiring_id=8 expiring_last_updated_at=2017-08-17 15:11:27 UTC
[IdentityCache] delete recorded for IDC:7:blob:Human:5897275080375446043:8
[IdentityCache] delete recorded for IDC:7:attrs:Human:id:name:15104357434540190954
[IdentityCache] delete recorded for IDC:7:attrs:Human:id:name:10331356276309087694
=> true
[17] pry(main)> leo.destroy
   (0.1ms)  begin transaction
  SQL (0.2ms)  DELETE FROM "humen" WHERE "humen"."id" = ?  [["id", 8]]
   (1.6ms)  commit transaction
[IdentityCache] expiring=Human expiring_id=8 expiring_last_updated_at=2017-08-17 15:20:08 UTC
[IdentityCache] delete recorded for IDC:7:blob:Human:5897275080375446043:8
[IdentityCache] delete recorded for IDC:7:attrs:Human:id:name:10331356276309087694
=> #<human:0x007f10ebe1ee68 id:="" 8,="" name:="" "neo",="" created_at:="" thu,="" 17="" aug="" 2017="" 15:11:27="" utc="" +00:00,="" updated_at:="" thu,="" 17="" aug="" 2017="" 15:20:08="" utc="" +00:00=""></human:0x007f10ebe1ee68></human:0x007f10ebe1ee68>