cached_values 1.0.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,65 @@
1
+ Cached Values
2
+ =
3
+
4
+ [http://github.com/JackDanger/cached_values/](http://github.com/JackDanger/cached_values/)
5
+
6
+ Cache the result of any Ruby expression or SQL into an ActiveRecord attribute. Expire and update automatically.
7
+
8
+ INSTALL:
9
+ ===
10
+
11
+ * as gem: sudo gem install cached_values
12
+ * as plugin: ./script/plugin install git://github.com/JackDanger/cached_values.git
13
+
14
+ USAGE:
15
+ ===
16
+
17
+ You can calculate values with a single line in any ActiveRecord model. To actually save those values you'll need to create
18
+ an attribute in your schema. By default the cached_value instance will look for an attribute with the same name as itself.
19
+ You can override this by specifying a :cache => :some_attribute option
20
+
21
+ A very simple case in which cached_values works just like the .count method on a has_many association:
22
+
23
+ class Leprechaun < ActiveRecord::Base
24
+ caches_value :total_gold_coins, :sql => 'select count(*) from gold_coins where leprechaun_id = #{id}'
25
+ end
26
+
27
+ Company.find(4).total_employees # => 45
28
+
29
+ A more sophisticated example:
30
+
31
+ class Leprechaun < ActiveRecord::Base
32
+ has_many :lucky_charms
33
+ has_many :deceived_children, :through => :lucky_charms
34
+ caches_value :total_children_remaining_to_be_deceived, :sql => '... very complicated sql here ...'
35
+ end
36
+
37
+ Leprechaun.find(14).total_children_remaining_to_be_deceived # => 6,692,243,122
38
+
39
+ The values can be of any type. The plugin will attempt to cast SQL results to the type corresponding with their database cache
40
+ but calculations in Ruby are left alone.
41
+
42
+ You can also calculate the value in Ruby using a string to be eval'ed or a Proc. Both are evaluated
43
+ in the context of the record instance.
44
+
45
+ class Leprechaun < ActiveRecord::Base
46
+ caches_value :total_gold, :eval => "some_archaic_and_silly_calculation(gold_coins)"
47
+ caches_value :total_lucky_charms, :eval => Proc.new {|record| record.calculate_total_lucky_charms }
48
+ end
49
+
50
+ The cache is customizable, you can specify which attribute should be used as a cache:
51
+
52
+ caches_value :runic_formula, :sql => '...' # uses 'runic_formula' column if it exists
53
+ caches_value :standard_deviation_of_gold_over_time, # uses 'std' column if it exists
54
+ :sql => '...', :cache => 'std'
55
+ caches_value :id, :sql => '...', :cache => false # does NOT persist to db, just memoize in memory. This avoids overwriting the attribute of the same name.
56
+
57
+ ActiveRecord callbacks can be used to reload (update cache and reload instance) at certain times:
58
+
59
+ caches_value :standard_deviation, :sql => '...', :reload => [:before_save, :after_destroy]
60
+ caches_value :runic_formula, :sql => '...', :reload => :after_create
61
+
62
+
63
+ Patches welcome, forks celebrated.
64
+
65
+ Copyright (c) 2007 Jack Danger Canty @ [http://jåck.com](http://jåck.com), released under the MIT license
data/Rakefile CHANGED
@@ -1,17 +1,28 @@
1
- # -*- ruby -*-
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gem|
4
+ gem.name = "cached_values"
5
+ gem.summary = %Q{Memoize and persist calculations into ActiveRecord attributes}
6
+ gem.description = %Q{Speedup your ActiveRecord by storing and updating the results of SQL or Ruby expressions into record attributes}
7
+ gem.email = "rubygems@6brand.com"
8
+ gem.homepage = "http://github.com/JackDanger/cached_values"
9
+ gem.authors = ["Jack Danger Canty"]
10
+ gem.add_dependency "object_proxy", ">= 0"
11
+ gem.add_development_dependency "active_record", ">= 0"
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
17
+ end
2
18
 
3
- $:.unshift(File.dirname(__FILE__) + '/lib')
4
- $:.unshift(File.dirname(__FILE__) + '/lib/cached_values')
5
19
 
6
20
 
7
- require 'rubygems'
8
- require 'hoe'
9
- require "cached_values"
21
+ task :default => :test
10
22
 
11
- Hoe.new('cached_values', CachedValues::VERSION) do |p|
12
- p.rubyforge_name = 'cachedvalues' # if different than lowercase project name
13
- p.remote_rdoc_dir = '' # Release to root
14
- p.developer('Jack Danger Canty', 'rubygems_cached_values@6brand.com')
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << '.'
26
+ test.pattern = 'test/*_test.rb'
27
+ test.verbose = true
15
28
  end
16
-
17
- # vim: syntax=Ruby
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.7.0
data/init.rb CHANGED
@@ -1,3 +1,2 @@
1
- require 'cached_value'
2
1
  require 'cached_values'
3
2
  ActiveRecord::Base.send :include, CachedValues
data/install.rb CHANGED
@@ -1 +1 @@
1
- puts IO.read(File.join(File.dirname(__FILE__), 'README.txt'))
1
+ puts IO.read(File.join(File.dirname(__FILE__), 'README.markdown'))
@@ -1,3 +1,4 @@
1
+ gem 'object_proxy'
1
2
  require 'object_proxy'
2
3
 
3
4
  module ActiveRecord
@@ -5,23 +6,16 @@ module ActiveRecord
5
6
 
6
7
  def initialize(owner, reflection)
7
8
  @owner, @reflection = owner, reflection
8
- reset
9
9
  end
10
10
 
11
- def reset
12
- @target = nil
13
- end
14
-
15
- def load
16
- reset
17
- @target = find_target(true)
11
+ def load(skip_cache = false)
12
+ @target = find_target skip_cache
18
13
  update_cache(@target)
14
+ self
19
15
  end
20
16
 
21
17
  def reload
22
- @owner.instance_variable_set("@#{@reflection.name}", nil)
23
- load
24
- @owner.send @reflection.name
18
+ @owner.send(@reflection.name).load(true)
25
19
  end
26
20
 
27
21
  alias update reload
@@ -29,6 +23,7 @@ module ActiveRecord
29
23
  def clear
30
24
  clear_cache
31
25
  @owner.instance_variable_set("@#{@reflection.name}", nil)
26
+ self
32
27
  end
33
28
 
34
29
  def target
@@ -82,10 +77,18 @@ module ActiveRecord
82
77
 
83
78
  def update_cache(value)
84
79
  return unless has_cache?
85
- unless @owner.new_record?
86
- @owner.class.update_all(["#{cache_column} = ?", value], ["id = ?", @owner.id])
87
- end
80
+
88
81
  @owner.send(:write_attribute, cache_column, value)
82
+
83
+ return if @owner.new_record? || !CachedValues.perform_save?
84
+
85
+ exist_conditions = value.nil? ?
86
+ [ "#{cache_column} IS NULL AND id = ?", @owner.id ] :
87
+ [ "(#{cache_column} = ? AND #{cache_column} IS NOT NULL) AND id = ?", value, @owner.id ]
88
+
89
+ return if @owner.class.exists? exist_conditions
90
+
91
+ @owner.class.update_all [ "#{cache_column} = ?", value], ["id = ?", @owner.id ]
89
92
  end
90
93
 
91
94
  def typecast_result(result)
data/lib/cached_values.rb CHANGED
@@ -1,9 +1,113 @@
1
1
  require 'active_record'
2
- require File.expand_path(File.dirname(__FILE__) + "/cached_values/cached_value")
3
- require File.expand_path(File.dirname(__FILE__) + "/cached_values/caches_value")
2
+ require File.expand_path(File.dirname(__FILE__) + "/cached_value")
4
3
 
5
4
  module CachedValues # :nodoc:
6
- VERSION = '1.0.1'
5
+ def self.perform_save?
6
+ @perform_save ||= true
7
+ end
8
+
9
+ def self.without_saving_record
10
+ @perform_save = false
11
+ yield
12
+ @perform_save = true
13
+ end
14
+
15
+ # USAGE:
16
+ #
17
+ # a very simple case in which cached_values works just like the .count method on a has_many association:
18
+ #
19
+ # class Company < ActiveRecord::Base
20
+ # caches_value :total_employees, :sql => 'select count(*) from employees where company_id = #{id}'
21
+ # end
22
+ #
23
+ # a more sophisticated example:
24
+ #
25
+ # class User < ActiveRecord::Base
26
+ # has_many :trinkets
27
+ # has_many :sales, :through => :trinkets
28
+ # caches_value :remaining_trinket_sales_allotted, :sql => '... very complicated sql here ...'
29
+ # end
30
+ #
31
+ # user = User.find(:first)
32
+ # user.remaining_trinket_sales_allotted # => 70
33
+ # Trinket.delete_all # <= any operation that would affect our value
34
+ # user.remaining_trinket_sales_allotted # => 70
35
+ # user.remaining_trinket_sales_allotted.reload # => 113
36
+ #
37
+ # You can also calculate the value in Ruby. This can be done by a string to be eval'ed or a Proc. Both are evaluated
38
+ # in the context of the record instance.
39
+ #
40
+ # class User < ActiveRecord::Base
41
+ # caches_value :expensive_calculation, :eval => "some_big_expensize_calculation(self.id)"
42
+ # caches_value :other_expensive_process, :eval => Proc.new {|record| record.other_expensize_process }
43
+ # end
44
+ #
45
+
46
+ def caches_value(name, options = {})
47
+ reflection = create_cached_value_reflection(name, options)
48
+
49
+ configure_dependency_for_cached_value(reflection)
50
+
51
+ reflection.options[:cache] ||= reflection.name unless false == options[:cache]
52
+
53
+ cached_value_accessor_method(reflection, ActiveRecord::CachedValue)
54
+ cached_value_callback_methods(reflection)
55
+ end
56
+
57
+ private
58
+
59
+ def configure_dependency_for_cached_value(reflection)
60
+
61
+ if !reflection.options[:sql] && !reflection.options[:eval]
62
+ raise ArgumentError, "You must specify either the :eval or :sql options for caches_value in #{self.name}"
63
+ end
64
+
65
+ if reflection.options[:sql] && reflection.options[:eval]
66
+ raise ArgumentError, ":eval and :sql are mutually exclusive options. You may specify one or the other for caches_value in #{self.name}"
67
+ end
68
+ end
69
+
70
+ def create_cached_value_reflection(name, options)
71
+ options.assert_valid_keys(:sql, :eval, :cache, :clear, :load, :reload)
72
+
73
+ reflection = ActiveRecord::Reflection::MacroReflection.new(:cached_value, name, options, self)
74
+ write_inheritable_hash :reflections, name => reflection
75
+ reflection
76
+ end
77
+
78
+ def cached_value_accessor_method(reflection, association_proxy_class)
79
+ define_method(reflection.name) do
80
+ association = instance_variable_get("@#{reflection.name}")
81
+
82
+ if association.nil?
83
+ association = association_proxy_class.new(self, reflection)
84
+ instance_variable_set("@#{reflection.name}", association)
85
+ association.load
86
+ end
87
+ association.target.nil? ? nil : association
88
+ end
89
+ end
90
+
91
+ def cached_value_callback_methods(reflection)
92
+ if events = reflection.options[:reload]
93
+ events = Array(events).map { |event| event.to_s }
94
+ ActiveRecord::Callbacks::CALLBACKS.each do |callback|
95
+ if events.include?(callback)
96
+ # before_save do |record|
97
+ send callback do |record|
98
+ if %{before_save before_create before_destroy}.include?(callback.to_s)
99
+ CachedValues.without_saving_record do
100
+ record.send(reflection.name).reload
101
+ end
102
+ else
103
+ record.send(reflection.name).reload
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
7
110
  end
8
111
 
9
- ActiveRecord::Base.send :include, CachesValues
112
+
113
+ ActiveRecord::Base.extend CachedValues
@@ -1,4 +1,3 @@
1
- require 'test/unit'
2
1
  require File.expand_path(File.dirname(__FILE__) + "/test_helper")
3
2
 
4
3
  class CachedValuesTest < Test::Unit::TestCase
@@ -97,13 +96,6 @@ class CachedValuesTest < Test::Unit::TestCase
97
96
  assert_not_equal value.to_i, @mc_nairn.reload_callback
98
97
  end
99
98
 
100
- def test_clear_callback_should_fire
101
- assert @mc_nairn.clear_callback
102
- assert @mc_nairn.instance_variable_get("@clear_callback")
103
- @mc_nairn.valid?
104
- assert_nil @mc_nairn.instance_variable_get("@clear_callback")
105
- end
106
-
107
99
  def test_sql_should_cast_to_integer
108
100
  assert @mc_nairn.integer_cast.is_a?(Fixnum)
109
101
  end
data/test/database.yml CHANGED
@@ -1,9 +1,9 @@
1
1
  sqlite:
2
2
  :adapter: sqlite
3
- :dbfile: plugin.sqlite.db
3
+ :database: plugin.sqlite.db
4
4
  sqlite3:
5
5
  :adapter: sqlite3
6
- :dbfile: ":memory:"
6
+ :database: ":memory:"
7
7
  postgresql:
8
8
  :adapter: postgresql
9
9
  :username: postgres
@@ -15,4 +15,4 @@ mysql:
15
15
  :host: localhost
16
16
  :username: rails
17
17
  :password:
18
- :database: plugin_test
18
+ :database: plugin_test
data/test/leprechaun.rb CHANGED
@@ -6,7 +6,6 @@ class Leprechaun < ActiveRecord::Base
6
6
  caches_value :favorite_color_in_rot_13_without_cache, :eval => Proc.new {|leprechaun| leprechaun.favorite_color.tr "A-Za-z", "N-ZA-Mn-za-m" }, :cache => false
7
7
  caches_value :favorite_color_turned_uppercase_with_explicit_cache, :eval => "favorite_color.upcase", :cache => 'some_other_cache_field'
8
8
  caches_value :reload_callback, :eval => "rand(1000)", :reload => [:before_save, :after_validation]
9
- caches_value :clear_callback, :eval => "rand(1000)", :clear => :before_validation
10
9
  caches_value :float_cast, :sql => "select 82343.222"
11
10
  caches_value :integer_cast, :sql => "select 19"
12
11
  caches_value :string_cast, :sql => 'select "Top \'o the mornin\' to ya"'
data/test/test_helper.rb CHANGED
@@ -4,7 +4,9 @@
4
4
  $:.unshift(File.dirname(__FILE__) + '/../lib')
5
5
 
6
6
  require 'rubygems'
7
+ gem 'test-unit'
7
8
  require 'test/unit'
9
+ require 'active_support'
8
10
  require 'active_record'
9
11
  require 'active_record/fixtures'
10
12
  require "cached_values"
@@ -16,7 +18,7 @@ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
16
18
 
17
19
  load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
18
20
 
19
- class Test::Unit::TestCase #:nodoc:
21
+ class ActiveSupport::TestCase #:nodoc:
20
22
 
21
23
  # def create_fixtures(*table_names)
22
24
  # if block_given?
@@ -27,10 +29,10 @@ class Test::Unit::TestCase #:nodoc:
27
29
  # end
28
30
 
29
31
  # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
30
- self.use_transactional_fixtures = true
32
+ # self.use_transactional_fixtures = true
31
33
 
32
34
  # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
33
- self.use_instantiated_fixtures = false
35
+ # self.use_instantiated_fixtures = false
34
36
 
35
37
  # Add more helper methods to be used by all tests here...
36
- end
38
+ end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cached_values
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 7
8
+ - 0
9
+ version: 1.7.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Jack Danger Canty
@@ -9,74 +14,89 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2008-03-12 00:00:00 -07:00
17
+ date: 2010-05-03 00:00:00 -07:00
13
18
  default_executable:
14
19
  dependencies:
15
20
  - !ruby/object:Gem::Dependency
16
- name: hoe
17
- version_requirement:
18
- version_requirements: !ruby/object:Gem::Requirement
21
+ name: object_proxy
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
19
24
  requirements:
20
25
  - - ">="
21
26
  - !ruby/object:Gem::Version
22
- version: 1.5.1
23
- version:
24
- description: ""
25
- email:
26
- - rubygems_cached_values@6brand.com
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: active_record
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
44
+ description: Speedup your ActiveRecord by storing and updating the results of SQL or Ruby expressions into record attributes
45
+ email: rubygems@6brand.com
27
46
  executables: []
28
47
 
29
48
  extensions: []
30
49
 
31
50
  extra_rdoc_files:
32
- - History.txt
33
- - Manifest.txt
34
- - README.txt
51
+ - README.markdown
35
52
  files:
36
- - History.txt
37
53
  - MIT-LICENSE
38
- - Manifest.txt
39
- - README.txt
54
+ - README.markdown
40
55
  - Rakefile
56
+ - VERSION
41
57
  - init.rb
42
58
  - install.rb
59
+ - lib/cached_value.rb
43
60
  - lib/cached_values.rb
44
- - lib/cached_values/cached_value.rb
45
- - lib/cached_values/caches_value.rb
46
61
  - tasks/cached_values_tasks.rake
62
+ - test/cached_values_test.rb
47
63
  - test/database.yml
48
64
  - test/leprechaun.rb
49
65
  - test/schema.rb
50
- - test/test_cached_values.rb
51
66
  - test/test_helper.rb
52
67
  - uninstall.rb
53
68
  has_rdoc: true
54
- homepage: http://github.com/JackDanger/cached_values/
69
+ homepage: http://github.com/JackDanger/cached_values
70
+ licenses: []
71
+
55
72
  post_install_message:
56
73
  rdoc_options:
57
- - --main
58
- - README.txt
74
+ - --charset=UTF-8
59
75
  require_paths:
60
76
  - lib
61
77
  required_ruby_version: !ruby/object:Gem::Requirement
62
78
  requirements:
63
79
  - - ">="
64
80
  - !ruby/object:Gem::Version
81
+ segments:
82
+ - 0
65
83
  version: "0"
66
- version:
67
84
  required_rubygems_version: !ruby/object:Gem::Requirement
68
85
  requirements:
69
86
  - - ">="
70
87
  - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
71
90
  version: "0"
72
- version:
73
91
  requirements: []
74
92
 
75
- rubyforge_project: cachedvalues
76
- rubygems_version: 1.0.1
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.6
77
95
  signing_key:
78
- specification_version: 2
79
- summary: ""
96
+ specification_version: 3
97
+ summary: Memoize and persist calculations into ActiveRecord attributes
80
98
  test_files:
81
- - test/test_cached_values.rb
99
+ - test/cached_values_test.rb
100
+ - test/leprechaun.rb
101
+ - test/schema.rb
82
102
  - test/test_helper.rb
data/History.txt DELETED
@@ -1,7 +0,0 @@
1
- === 1.0.1 / 2008-03-12
2
-
3
- * The find_target method is now public
4
-
5
- === 1.0.0 / 2008-03-06
6
-
7
- * Converted from a Rails plugin
data/Manifest.txt DELETED
@@ -1,17 +0,0 @@
1
- History.txt
2
- MIT-LICENSE
3
- Manifest.txt
4
- README.txt
5
- Rakefile
6
- init.rb
7
- install.rb
8
- lib/cached_values.rb
9
- lib/cached_values/cached_value.rb
10
- lib/cached_values/caches_value.rb
11
- tasks/cached_values_tasks.rake
12
- test/database.yml
13
- test/leprechaun.rb
14
- test/schema.rb
15
- test/test_cached_values.rb
16
- test/test_helper.rb
17
- uninstall.rb
data/README.txt DELETED
@@ -1,66 +0,0 @@
1
- = Cached Values
2
-
3
- * http://github.com/JackDanger/cached_values/
4
-
5
- A dead-simple way to calculate any value via Ruby or SQL and (optionally) have it saved in a database field.
6
-
7
- == REQUIREMENTS:
8
-
9
- * ObjectProxy
10
-
11
- == INSTALL:
12
-
13
- * as gem: sudo gem install cached_values
14
- * as plugin: ./script/plugin install git://github.com/JackDanger/cached_values.git
15
-
16
- == USAGE:
17
-
18
- You can calculate values with a single line in any ActiveRecord model. To actually save those values you'll need to create
19
- an attribute in your schema. By default the cached_value instance will look for an attribute with the same name as itself.
20
- You can override this by specifying a :cache => :some_attribute option
21
-
22
- A very simple case in which cached_values works just like the .count method on a has_many association:
23
-
24
- class Leprechaun < ActiveRecord::Base
25
- caches_value :total_gold_coins, :sql => 'select count(*) from gold_coins where leprechaun_id = #{id}'
26
- end
27
-
28
- Company.find(4).total_employees # => 45
29
-
30
- A more sophisticated example:
31
-
32
- class Leprechaun < ActiveRecord::Base
33
- has_many :lucky_charms
34
- has_many :deceived_children, :through => :lucky_charms
35
- caches_value :total_children_remaining_to_be_deceived, :sql => '... very complicated sql here ...'
36
- end
37
-
38
- Leprechaun.find(14).total_children_remaining_to_be_deceived # => 6,692,243,122
39
-
40
- The values can be of any type. The plugin will attempt to cast SQL results to the type corresponding with their database cache
41
- but calculations in Ruby are left alone.
42
-
43
- You can also calculate the value in Ruby using a string to be eval'ed or a Proc. Both are evaluated
44
- in the context of the record instance.
45
-
46
- class Leprechaun < ActiveRecord::Base
47
- caches_value :total_gold, :eval => "some_archaic_and_silly_calculation(self.gold_coins)"
48
- caches_value :total_lucky_charms, :eval => Proc.new {|record| record.calculate_total_lucky_charms }
49
- end
50
-
51
- The cache is customizable, you can specify which attribute should be used as a cache:
52
-
53
- caches_value :runic_formula, :sql => '...' # uses 'full_formula' column if it exists
54
- caches_value :standard_deviation_of_gold_over_time, # uses 'std' column if it exists
55
- :sql => '...', :cache => 'std'
56
- caches_value :id, :sql => '...', :cache => false # does NOT save any cache. This avoids overwriting an attribute
57
-
58
- ActiveRecord callbacks can be used to call load (make sure the value is in memory), clear (flush the cache and
59
- delete the instance), or reload (flush cache and reload instance) at certain times:
60
-
61
- caches_value :standard_deviation, :sql => '...', :reload => [:after_save, :before_validation]
62
- caches_value :full_formula, :sql => '...', :clear => :after_find
63
-
64
- Bugs can be filed at http://code.lighthouseapp.com/projects/4502-hoopla-rails-plugins/
65
-
66
- Copyright (c) 2007 Jack Danger Canty @ http://6brand.com, released under the MIT license
@@ -1,104 +0,0 @@
1
- require 'active_record'
2
- require File.expand_path(File.dirname(__FILE__) + '/cached_value')
3
-
4
- module CachesValues # :nodoc:
5
-
6
- def self.included(base)
7
- unless base.extended_by.include?(CachesValues::ClassMethods)
8
- base.extend(ClassMethods)
9
- end
10
- end
11
-
12
- module ClassMethods
13
- # USAGE:
14
- #
15
- # a very simple case in which cached_values works just like the .count method on a has_many association:
16
- #
17
- # class Company < ActiveRecord::Base
18
- # caches_value :total_employees, :sql => 'select count(*) from employees where company_id = #{id}'
19
- # end
20
- #
21
- # a more sophisticated example:
22
- #
23
- # class User < ActiveRecord::Base
24
- # has_many :trinkets
25
- # has_many :sales, :through => :trinkets
26
- # caches_value :remaining_trinket_sales_allotted, :sql => '... very complicated sql here ...'
27
- # end
28
- #
29
- # user = User.find(:first)
30
- # user.remaining_trinket_sales_allotted # => 70
31
- # Trinket.delete_all # <= any operation that would affect our value
32
- # user.remaining_trinket_sales_allotted # => 70
33
- # user.remaining_trinket_sales_allotted.reload # => 113
34
- #
35
- # You can also calculate the value in Ruby. This can be done by a string to be eval'ed or a Proc. Both are evaluated
36
- # in the context of the record instance.
37
- #
38
- # class User < ActiveRecord::Base
39
- # caches_value :expensive_calculation, :eval => "some_big_expensize_calculation(self.id)"
40
- # caches_value :other_expensive_process, :eval => Proc.new {|record| record.other_expensize_process }
41
- # end
42
- #
43
-
44
- def caches_value(name, options = {})
45
- reflection = create_cached_value_reflection(name, options)
46
-
47
- configure_dependency_for_cached_value(reflection)
48
-
49
- reflection.options[:cache] ||= reflection.name unless false == options[:cache]
50
-
51
- cached_value_accessor_method(reflection, ActiveRecord::CachedValue)
52
- cached_value_callback_methods(reflection)
53
- end
54
-
55
- private
56
-
57
- def configure_dependency_for_cached_value(reflection)
58
-
59
- if !reflection.options[:sql] && !reflection.options[:eval]
60
- raise ArgumentError, "You must specify either the :eval or :sql options for caches_value in #{self.name}"
61
- end
62
-
63
- if reflection.options[:sql] && reflection.options[:eval]
64
- raise ArgumentError, ":eval and :sql are mutually exclusive options. You may specify one or the other for caches_value in #{self.name}"
65
- end
66
- end
67
-
68
- def create_cached_value_reflection(name, options)
69
- options.assert_valid_keys(:sql, :eval, :cache, :clear, :load, :reload)
70
-
71
- reflection = ActiveRecord::Reflection::MacroReflection.new(:cached_value, name, options, self)
72
- write_inheritable_hash :reflections, name => reflection
73
- reflection
74
- end
75
-
76
- def cached_value_accessor_method(reflection, association_proxy_class)
77
- define_method(reflection.name) do |*params|
78
- force_reload = params.first unless params.empty?
79
- association = instance_variable_get("@#{reflection.name}")
80
-
81
- if association.nil? || force_reload
82
- association = association_proxy_class.new(self, reflection)
83
- instance_variable_set("@#{reflection.name}", association)
84
- force_reload ? association.reload : association.load
85
- end
86
- association.target.nil? ? nil : association
87
- end
88
- end
89
-
90
- def cached_value_callback_methods(reflection)
91
- %w{clear reload}.each do |operation|
92
- if events = reflection.options[operation.to_sym]
93
- events = [events] unless events.is_a?(Array)
94
- events.map! { |event| event.to_s }
95
- ActiveRecord::Callbacks::CALLBACKS.each do |callback|
96
- if events.include?(callback)
97
- send(callback, Proc.new {|record| record.send(reflection.name).send(operation)})
98
- end
99
- end
100
- end
101
- end
102
- end
103
- end
104
- end