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 +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
|