cached-models 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +84 -0
- data/MIT-LICENSE +20 -0
- data/README +95 -0
- data/Rakefile +81 -0
- data/about.yml +8 -0
- data/cached-models.gemspec +19 -0
- data/init.rb +2 -0
- data/install.rb +1 -0
- data/lib/activerecord/lib/active_record/associations/association_collection.rb +95 -0
- data/lib/activerecord/lib/active_record/associations/association_proxy.rb +39 -0
- data/lib/activerecord/lib/active_record/associations/has_many_association.rb +7 -0
- data/lib/activerecord/lib/active_record/associations.rb +379 -0
- data/lib/activerecord/lib/active_record/base.rb +69 -0
- data/lib/activerecord/lib/active_record.rb +4 -0
- data/lib/cached-models.rb +1 -0
- data/lib/cached_models.rb +4 -0
- data/setup.rb +1585 -0
- data/tasks/cached_models_tasks.rake +90 -0
- data/test/active_record/associations/has_many_association_test.rb +401 -0
- data/test/active_record/base_test.rb +32 -0
- data/test/fixtures/authors.yml +13 -0
- data/test/fixtures/blogs.yml +7 -0
- data/test/fixtures/comments.yml +19 -0
- data/test/fixtures/posts.yml +23 -0
- data/test/fixtures/tags.yml +14 -0
- data/test/models/author.rb +10 -0
- data/test/models/blog.rb +4 -0
- data/test/models/comment.rb +3 -0
- data/test/models/post.rb +7 -0
- data/test/models/tag.rb +3 -0
- data/test/test_helper.rb +42 -0
- data/uninstall.rb +1 -0
- metadata +105 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
*0.0.2 (October 10th, 2008)*
|
2
|
+
|
3
|
+
* Updated README with new installation instructions
|
4
|
+
|
5
|
+
* Created separated folder for ActiveRecord
|
6
|
+
|
7
|
+
* Added dist related Rake tasks
|
8
|
+
|
9
|
+
* Added gem related files
|
10
|
+
|
11
|
+
* Added Git related Rake tasks
|
12
|
+
|
13
|
+
* Removed default configuration for cache lookup
|
14
|
+
|
15
|
+
* Make sure cache is always used by all the instances which reference the same record
|
16
|
+
|
17
|
+
* Made independent of Rails
|
18
|
+
|
19
|
+
* Allow test suite to work without any active cache server
|
20
|
+
|
21
|
+
* Enhanced AssociationCollection test coverage
|
22
|
+
|
23
|
+
* ActiveRecord::Base#expire_cache_for now uses the new cache access API
|
24
|
+
|
25
|
+
* Abstracted ActiveRecord::Base#cache_fetch in order to normalize cache access for <reflection_name>_ids
|
26
|
+
|
27
|
+
* Reduced the amount of cache hits, caching the status of cached relations with ActiveRecord::Base#cached_associations
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
*0.0.1 (September 10th, 2008)*
|
32
|
+
|
33
|
+
* Updated README with project informations
|
34
|
+
|
35
|
+
* Make sure 'test' is the default Rake task
|
36
|
+
|
37
|
+
* Added project description to README. Added about.yml.
|
38
|
+
|
39
|
+
* Updated README with informations about required environment settings
|
40
|
+
|
41
|
+
* Only load the plugin if the current environment has the cache turned on
|
42
|
+
|
43
|
+
* Added support for cache expiration on after_save callback
|
44
|
+
|
45
|
+
* Make sure to use ActiveRecord cache proxy for test suite
|
46
|
+
|
47
|
+
* Make sure test suite will run using RAILS_ENV in test mode
|
48
|
+
|
49
|
+
* Added support for scoped finders in AssociationCollection. Fixed cache renewal for AssociationCollection#delete.
|
50
|
+
|
51
|
+
* Added support for cache renewal on AssociationCollection methods
|
52
|
+
|
53
|
+
* Added support for cache expiration on direct associated objects updates
|
54
|
+
|
55
|
+
* Updated README example
|
56
|
+
|
57
|
+
* Removed CacheObserver. Fixed cache expiration for has_many relation.
|
58
|
+
|
59
|
+
* Introducing CacheObserver in order to transparently handle cache expiring for has_many macro
|
60
|
+
|
61
|
+
* Test enhancements for AssociationCollection#<< on polymorphic associations
|
62
|
+
|
63
|
+
* Test enhancements for AssociationCollection#<<. Make sure to expire caches when an associated object changes owner.
|
64
|
+
|
65
|
+
class Author < ActiveRecord::Base
|
66
|
+
has_many :posts, :cached => true
|
67
|
+
end
|
68
|
+
|
69
|
+
post = author.posts.last
|
70
|
+
another_author.posts << post # => refresh both author and another_author caches
|
71
|
+
|
72
|
+
* AssociationCollection#<< support
|
73
|
+
|
74
|
+
class Author < ActiveRecord::Base
|
75
|
+
has_many :posts, :cached => true
|
76
|
+
end
|
77
|
+
|
78
|
+
author.posts << post # => causes a refresh of cached posts
|
79
|
+
|
80
|
+
* has_many association support
|
81
|
+
|
82
|
+
class Author < ActiveRecord::Base
|
83
|
+
has_many :posts, :cached => true
|
84
|
+
end
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Luca Guidi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
= CachedModels
|
2
|
+
|
3
|
+
CachedModels provides to your models a transparent approach to use Rails internal caching mechanism.
|
4
|
+
|
5
|
+
Check for news and tutorials at the {project home page}[http://www.lucaguidi.com/pages/cached_models].
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
= Usage
|
10
|
+
|
11
|
+
Using Memcached and Rails 2.2.1
|
12
|
+
|
13
|
+
Make sure to configure your current environment with:
|
14
|
+
|
15
|
+
config.cache_classes = true
|
16
|
+
config.action_controller.perform_caching = true
|
17
|
+
config.cache_store = :mem_cache_store
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
class Project < ActiveRecord::Base
|
22
|
+
has_many :developers, :cached => true
|
23
|
+
has_many :tickets, :cached => true
|
24
|
+
has_many :recent_tickets, :limit => 5,
|
25
|
+
:order => 'id DESC', :cached => true
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class Developer < ActiveRecord::Base
|
30
|
+
belongs_to :project, :cached => true
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
Example 1
|
35
|
+
project.developers # Database fetch and automatic cache storing
|
36
|
+
|
37
|
+
developer = project.developers.last
|
38
|
+
developer.update_attributes :first_name => 'Luca' # Database update and cache expiration for project cache
|
39
|
+
|
40
|
+
Example 2
|
41
|
+
project2.developers # Database fetch and automatic cache storing
|
42
|
+
project2.developers << developer # Database update and cache renewal for both project and project2 caches
|
43
|
+
|
44
|
+
Example 3
|
45
|
+
project.tickets # Database fetch and automatic cache storing
|
46
|
+
ticket = project.recent_tickets.first
|
47
|
+
ticket.update_attributes :state => 'solved' # Database update and cache expiration for both tickets and recent_tickets entries
|
48
|
+
|
49
|
+
|
50
|
+
= Install
|
51
|
+
|
52
|
+
There are three ways to install CachedModels
|
53
|
+
|
54
|
+
Gemified plugin:
|
55
|
+
|
56
|
+
environment.rb
|
57
|
+
|
58
|
+
Rails::Initializer.run do |config|
|
59
|
+
config.gem 'cached-models'
|
60
|
+
end
|
61
|
+
|
62
|
+
$ (sudo) rake gems:install
|
63
|
+
$ rake gems:unpack
|
64
|
+
|
65
|
+
Rails plugin:
|
66
|
+
|
67
|
+
$ ./script/plugin install git://github.com/jodosha/cached-models.git
|
68
|
+
|
69
|
+
Standalone:
|
70
|
+
|
71
|
+
$ (sudo) gem install cached-models
|
72
|
+
|
73
|
+
in your project:
|
74
|
+
|
75
|
+
require 'rubygems'
|
76
|
+
require 'activerecord'
|
77
|
+
require 'cached-models'
|
78
|
+
|
79
|
+
ActiveRecord::Base.rails_cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, 'localhost')
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
= Contribute
|
84
|
+
|
85
|
+
* Check out the code and test it:
|
86
|
+
$ git clone git://github.com/jodosha/cached-models.git
|
87
|
+
$ rake cached_models
|
88
|
+
|
89
|
+
* Create a ticket to the {Sushistar Lighthouse page}[http://sushistar.lighthouseapp.com]
|
90
|
+
|
91
|
+
* Create a patch and add as attachment to the ticket.
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
Copyright (c) 2008 Luca Guidi, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
version = '0.0.2'
|
6
|
+
repositories = %w( origin rubyforge )
|
7
|
+
|
8
|
+
desc 'Default: run unit tests.'
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
desc 'Test the cached_models plugin.'
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'lib'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Generate documentation for the cached_models plugin.'
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = 'CachedModels'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Build and install the gem (useful for development purposes).'
|
28
|
+
task :install do
|
29
|
+
system "gem build cached-models.gemspec"
|
30
|
+
system "sudo gem uninstall cached-models"
|
31
|
+
system "sudo gem install --local --no-rdoc --no-ri cached-models-#{version}.gem"
|
32
|
+
system "rm cached-models-*.gem"
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Build and prepare files for release.'
|
36
|
+
task :dist => :clean do
|
37
|
+
require 'cached-models'
|
38
|
+
system "gem build cached-models.gemspec"
|
39
|
+
system "cd .. && tar -czf cached-models-#{version}.tar.gz cached_models"
|
40
|
+
system "cd .. && tar -cjf cached-models-#{version}.tar.bz2 cached_models"
|
41
|
+
system "cd .. && mv cached-models-* cached_models"
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Clean the working copy from release files.'
|
45
|
+
task :clean do
|
46
|
+
system "rm cached-models-#{version}.gem" if File.exist? "cached-models-#{version}.gem"
|
47
|
+
system "rm cached-models-#{version}.tar.gz" if File.exist? "cached-models-#{version}.tar.gz"
|
48
|
+
system "rm cached-models-#{version}.tar.bz2" if File.exist? "cached-models-#{version}.tar.bz2"
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'Show the file list for the gemspec file'
|
52
|
+
task :files do
|
53
|
+
puts "Files:\n #{Dir['**/*'].reject {|f| File.directory?(f)}.sort.inspect}"
|
54
|
+
puts "Test files:\n #{Dir['test/**/*_test.rb'].reject {|f| File.directory?(f)}.sort.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
namespace :git do
|
58
|
+
desc 'Push local Git commits to all remote centralized repositories.'
|
59
|
+
task :push do
|
60
|
+
repositories.each do |repository|
|
61
|
+
puts "Pushing #{repository}...\n"
|
62
|
+
system "git push #{repository} master"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
desc 'Perform a git-tag'
|
67
|
+
task :tag do
|
68
|
+
puts "Please enter the tag name: "
|
69
|
+
tag_name = STDIN.gets.chomp
|
70
|
+
exit(1) if tag_name.nil? or tag_name.empty?
|
71
|
+
system %(git tag -s #{tag_name} -m "Tagged #{tag_name}")
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'Push all the tags to remote centralized repositories.'
|
75
|
+
task :push_tags do
|
76
|
+
repositories.each do |repository|
|
77
|
+
puts "Pushing tags to #{repository}...\n"
|
78
|
+
system "git push --tags #{repository}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/about.yml
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
author: Luca Guidi
|
2
|
+
email: guidi.luca@gmail.com
|
3
|
+
homepage: http://lucaguidi.com/pages/cached_models
|
4
|
+
summary: CachedModels provides to your models a transparent approach to use Rails internal caching mechanism.
|
5
|
+
description: CachedModels provides to your models a transparent approach to use Rails internal caching mechanism.
|
6
|
+
license: MIT
|
7
|
+
rails_version: 2.1.1+
|
8
|
+
version: 0.0.1
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "cached-models"
|
3
|
+
s.version = "0.0.2"
|
4
|
+
s.date = "2008-10-10"
|
5
|
+
s.summary = "Transparent caching policy for your models"
|
6
|
+
s.author = "Luca Guidi"
|
7
|
+
s.email = "guidi.luca@gmail.com"
|
8
|
+
s.homepage = "http://lucaguidi.com/pages/cached_models"
|
9
|
+
s.description = "CachedModels provides to your ActiveRecord models a transparent approach to use ActiveSupport caching mechanism."
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.rubyforge_project = %q{cached-models}
|
12
|
+
s.files = ["CHANGELOG", "MIT-LICENSE", "README", "Rakefile", "about.yml", "cached-models.gemspec", "init.rb", "install.rb", "lib/activerecord/lib/active_record.rb", "lib/activerecord/lib/active_record/associations.rb", "lib/activerecord/lib/active_record/associations/association_collection.rb", "lib/activerecord/lib/active_record/associations/association_proxy.rb", "lib/activerecord/lib/active_record/associations/has_many_association.rb", "lib/activerecord/lib/active_record/base.rb", "lib/cached-models.rb", "lib/cached_models.rb", "setup.rb", "tasks/cached_models_tasks.rake", "test/active_record/associations/has_many_association_test.rb", "test/active_record/base_test.rb", "test/fixtures/authors.yml", "test/fixtures/blogs.yml", "test/fixtures/comments.yml", "test/fixtures/posts.yml", "test/fixtures/tags.yml", "test/models/author.rb", "test/models/blog.rb", "test/models/comment.rb", "test/models/post.rb", "test/models/tag.rb", "test/test_helper.rb", "uninstall.rb"]
|
13
|
+
s.test_files = ["test/active_record/associations/has_many_association_test.rb",
|
14
|
+
"test/active_record/base_test.rb"]
|
15
|
+
s.extra_rdoc_files = ['README', 'CHANGELOG']
|
16
|
+
|
17
|
+
s.add_dependency("activesupport", ["> 2.1.0"])
|
18
|
+
s.add_dependency("activerecord", ["> 2.1.0"])
|
19
|
+
end
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Associations
|
5
|
+
class AssociationCollection < AssociationProxy #:nodoc:
|
6
|
+
def find(*args)
|
7
|
+
expects_array = args.first.kind_of?(Array)
|
8
|
+
ids = args.flatten.compact.uniq.map(&:to_i)
|
9
|
+
|
10
|
+
if @reflection.options[:cached]
|
11
|
+
result = @owner.send(:cache_read, @reflection)
|
12
|
+
if result
|
13
|
+
result = result.select { |record| ids.include? record.id }
|
14
|
+
result = expects_array ? result : result.first
|
15
|
+
return result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
options = args.extract_options!
|
20
|
+
|
21
|
+
# If using a custom finder_sql, scan the entire collection.
|
22
|
+
if @reflection.options[:finder_sql]
|
23
|
+
if ids.size == 1
|
24
|
+
id = ids.first
|
25
|
+
record = load_target.detect { |r| id == r.id }
|
26
|
+
expects_array ? [ record ] : record
|
27
|
+
else
|
28
|
+
load_target.select { |r| ids.include?(r.id) }
|
29
|
+
end
|
30
|
+
else
|
31
|
+
conditions = "#{@finder_sql}"
|
32
|
+
if sanitized_conditions = sanitize_sql(options[:conditions])
|
33
|
+
conditions << " AND (#{sanitized_conditions})"
|
34
|
+
end
|
35
|
+
|
36
|
+
options[:conditions] = conditions
|
37
|
+
|
38
|
+
if options[:order] && @reflection.options[:order]
|
39
|
+
options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
|
40
|
+
elsif @reflection.options[:order]
|
41
|
+
options[:order] = @reflection.options[:order]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Build options specific to association
|
45
|
+
construct_find_options!(options)
|
46
|
+
|
47
|
+
merge_options_from_reflection!(options)
|
48
|
+
|
49
|
+
# Pass through args exactly as we received them.
|
50
|
+
args << options
|
51
|
+
@reflection.klass.find(*args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
56
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
57
|
+
def <<(*records)
|
58
|
+
result = true
|
59
|
+
load_target if @owner.new_record?
|
60
|
+
|
61
|
+
@owner.transaction do
|
62
|
+
flatten_deeper(records).each do |record|
|
63
|
+
raise_on_type_mismatch(record)
|
64
|
+
add_record_to_target_with_callbacks(record) do |r|
|
65
|
+
result &&= insert_record(record) unless @owner.new_record?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@owner.send(:cache_write, @reflection, self) if @reflection.options[:cached]
|
71
|
+
|
72
|
+
result && self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
76
|
+
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
77
|
+
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
78
|
+
def size
|
79
|
+
if @reflection.options[:cached]
|
80
|
+
result = @owner.send(:cache_read, @reflection)
|
81
|
+
return result.to_ary.size if result
|
82
|
+
end
|
83
|
+
|
84
|
+
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
85
|
+
@target.size
|
86
|
+
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
87
|
+
unsaved_records = @target.select { |r| r.new_record? }
|
88
|
+
unsaved_records.size + count_records
|
89
|
+
else
|
90
|
+
count_records
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Associations
|
5
|
+
class AssociationProxy
|
6
|
+
protected
|
7
|
+
def set_belongs_to_association_for(record)
|
8
|
+
reset_association_cache(record) if @reflection.options[:cached]
|
9
|
+
|
10
|
+
if @reflection.options[:as]
|
11
|
+
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
12
|
+
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
|
13
|
+
else
|
14
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def reset_association_cache(record)
|
20
|
+
current_owner = current_owner(record)
|
21
|
+
return unless current_owner
|
22
|
+
current_owner.send(:cache_delete, @reflection)
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_owner(record)
|
26
|
+
current_owner_id, current_owner_type = if @reflection.options[:as]
|
27
|
+
[ record["#{@reflection.options[:as]}_id"],
|
28
|
+
record["#{@reflection.options[:as]}_type"] ]
|
29
|
+
else
|
30
|
+
[ record[@reflection.primary_key_name],
|
31
|
+
@owner.class.base_class.name.to_s ]
|
32
|
+
end
|
33
|
+
|
34
|
+
return unless current_owner_id
|
35
|
+
current_owner_type.constantize.find(current_owner_id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|