2017-04-28 04:05:04 +08:00
#
# Copyright (C) 2012 - present Instructure, Inc.
2012-03-03 06:39:56 +08:00
#
# 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
2017-02-18 02:45:55 +08:00
# ensure the target class is ModelCache-aware, and set up the :id lookup
2016-09-23 04:09:09 +08:00
target_klass = reflections [ method . to_s ] . klass
2017-02-18 02:45:55 +08:00
raise " ` #{ target_klass } ` needs to `include ModelCache` before you can make ` #{ self } # #{ method } ` cacheable " unless target_klass . included_modules . include? ( ModelCache )
2015-07-08 22:06:33 +08:00
unless ModelCache . keys [ target_klass . name ] . include? ( options [ :key_lookup ] )
ModelCache . keys [ target_klass . name ] << options [ :key_lookup ]
2012-03-03 06:39:56 +08:00
end
2015-07-08 22:06:33 +08:00
ModelCache . make_cacheable self , method , options
2012-03-03 06:39:56 +08:00
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 |
2014-02-04 07:06:42 +08:00
h [ k ] = records . index_by ( & k )
2012-03-03 06:39:56 +08:00
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 } "
2012-05-12 06:32:11 +08:00
klass . send ( options [ :type ] == :instance ? :class_eval : :instance_eval , <<-CODE, __FILE__, __LINE__+1)
2012-03-03 06:39:56 +08:00
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
2012-05-12 06:32:11 +08:00
end