cached_values 1.0.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +65 -0
- data/Rakefile +23 -12
- data/VERSION +1 -0
- data/init.rb +0 -1
- data/install.rb +1 -1
- data/lib/{cached_values/cached_value.rb → cached_value.rb} +17 -14
- data/lib/cached_values.rb +108 -4
- data/test/{test_cached_values.rb → cached_values_test.rb} +0 -8
- data/test/database.yml +3 -3
- data/test/leprechaun.rb +0 -1
- data/test/test_helper.rb +6 -4
- metadata +49 -29
- data/History.txt +0 -7
- data/Manifest.txt +0 -17
- data/README.txt +0 -66
- data/lib/cached_values/caches_value.rb +0 -104
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
|
-
|
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
|
-
|
8
|
-
require 'hoe'
|
9
|
-
require "cached_values"
|
21
|
+
task :default => :test
|
10
22
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
data/install.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
puts IO.read(File.join(File.dirname(__FILE__), 'README.
|
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
|
12
|
-
@target =
|
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.
|
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
|
-
|
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__) + "/
|
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
|
-
|
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
|
-
|
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
|
-
:
|
3
|
+
:database: plugin.sqlite.db
|
4
4
|
sqlite3:
|
5
5
|
:adapter: sqlite3
|
6
|
-
:
|
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
|
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
|
-
|
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:
|
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:
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
-
|
33
|
-
- Manifest.txt
|
34
|
-
- README.txt
|
51
|
+
- README.markdown
|
35
52
|
files:
|
36
|
-
- History.txt
|
37
53
|
- MIT-LICENSE
|
38
|
-
-
|
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
|
-
- --
|
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:
|
76
|
-
rubygems_version: 1.
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.3.6
|
77
95
|
signing_key:
|
78
|
-
specification_version:
|
79
|
-
summary:
|
96
|
+
specification_version: 3
|
97
|
+
summary: Memoize and persist calculations into ActiveRecord attributes
|
80
98
|
test_files:
|
81
|
-
- test/
|
99
|
+
- test/cached_values_test.rb
|
100
|
+
- test/leprechaun.rb
|
101
|
+
- test/schema.rb
|
82
102
|
- test/test_helper.rb
|
data/History.txt
DELETED
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
|