cached-models 0.0.2
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/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
|