canvas-lms/lib/model_cache.rb

144 lines
4.2 KiB
Ruby

# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# on-demand in-memory model caching for those times where the rails query
# cache can't help you, like in this contrived example:
#
# # given:
#
# class Foo < ActiveRecord::Base
# has_many :bars
# end
#
# class Bar < ActiveRecord::Base
# belongs_to :foo
# def something!
# update_attribute :stuff, "my foo is: #{foo.name}"
# end
# end
#
# # if you do:
# foo.bars.each(&:something!)
#
# # then by default, AR loads each bar's foo separately and won't use the
# # query cache, since each update blows it away.
#
# granted, the example is a bit contrived, as preloading would be one possible
# solution. ModelCache is useful for those times when preloading is just not
# feasible. See Conversation and ConversatonParticipant for real-world usage.
module ModelCache
module ClassMethods
# e.g. use this to cache calls to ConversationParticipant#conversation,
# no matter how those conversation_participants were loaded
def cacheable_method(method, options={})
options[:cache_name] ||= method.to_s.pluralize.to_sym
options[:key_method] ||= "#{method}_id"
options[:key_lookup] ||= :id
options[:type] ||= :instance
ModelCache.make_cacheable self, method, options
end
# e.g. use this to cache calls to Conversation.find_by_id
def cacheable_by(*keys)
keys.each do |key|
ModelCache.keys[self.name] << key
ModelCache.make_cacheable self, "find_by_#{key}", :key_lookup => key
end
end
end
module InstanceMethods
def add_to_caches
return unless cache = ModelCache[self.class.name.underscore.pluralize.to_sym]
cache.keys.each do |key|
cache[key][send(key)] = self
end
end
def update_in_caches
return unless cache = ModelCache[self.class.name.underscore.pluralize.to_sym]
cache.keys.each do |key|
if send("#{key}_changed?")
cache[key][send(key)] = self
cache[key].delete(send("#{key}_was"))
end
end
end
end
def self.with_cache(lookups)
@cache = lookups.inject({}){ |h, (k, v)| h[k] = prepare_lookups(v); h }
yield
ensure
@cache = nil
end
def self.[](cache_name)
return nil unless @cache
@cache[cache_name] || {}
end
def self.keys
@keys ||= Hash.new{ |h, k| h[k] = [] }
end
def self.prepare_lookups(records)
return records if records.is_a?(Hash)
return {} if records.empty?
keys[records.first.class.name].inject({}) do |h, k|
h[k] = records.index_by(&k)
h
end
end
def self.make_cacheable(klass, method, options={})
options[:type] ||= :class
options[:cache_name] ||= klass.name.underscore.pluralize.to_sym
options[:key_lookup] ||= method
orig_method = "super"
alias_method = nil
key_value = options[:key_method] || "args.first"
# if extra args are provided, we should clear out the current value
# (e.g. if you call c.user(:lock => true) )
expected_args = options[:key_method] ? 0 : 1
maybe_reset = "cache[#{key_value}] = #{orig_method} if args.size > #{expected_args}"
klass.send(options[:type] == :instance ? :class_eval : :instance_eval, <<-CODE, __FILE__, __LINE__+1)
def #{method}(*args)
if cache = ModelCache[#{options[:cache_name].inspect}] and cache = cache[#{options[:key_lookup].inspect}]
#{maybe_reset}
cache[#{key_value}]
else
#{orig_method}
end
end
#{alias_method}
CODE
end
def self.included(klass)
klass.send :include, InstanceMethods
klass.extend ClassMethods
klass.after_create :add_to_caches
klass.after_update :update_in_caches
end
end