jodosha-cached-models 0.0.3
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 +142 -0
- data/MIT-LICENSE +20 -0
- data/README +95 -0
- data/Rakefile +81 -0
- data/about.yml +8 -0
- data/cached-models.gemspec +18 -0
- data/init.rb +2 -0
- data/install.rb +1 -0
- data/lib/activerecord/lib/active_record.rb +4 -0
- data/lib/activerecord/lib/active_record/associations.rb +404 -0
- data/lib/activerecord/lib/active_record/associations/association_collection.rb +127 -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/base.rb +73 -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 +117 -0
- data/test/active_record/associations/has_and_belongs_to_many_association_test.rb +12 -0
- data/test/active_record/associations/has_many_association_test.rb +355 -0
- data/test/active_record/associations/has_one_association_test.rb +12 -0
- data/test/active_record/base_test.rb +32 -0
- data/test/fixtures/addresses.yml +7 -0
- data/test/fixtures/authors.yml +13 -0
- data/test/fixtures/blogs.yml +7 -0
- data/test/fixtures/categories.yml +3 -0
- data/test/fixtures/categories_posts.yml +3 -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/address.rb +3 -0
- data/test/models/author.rb +12 -0
- data/test/models/blog.rb +4 -0
- data/test/models/category.rb +3 -0
- data/test/models/comment.rb +3 -0
- data/test/models/post.rb +8 -0
- data/test/models/tag.rb +3 -0
- data/test/test_helper.rb +88 -0
- data/uninstall.rb +1 -0
- metadata +112 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
*0.0.3 (October 22nd, 2008)*
|
2
|
+
|
3
|
+
* Tagged v0.0.3
|
4
|
+
|
5
|
+
* Test cases cleanup
|
6
|
+
|
7
|
+
* Sugar syntax for AssociationCollection#size
|
8
|
+
|
9
|
+
* Use loaded collection, instead of read from cache, when use scoped find form AssociationCollection
|
10
|
+
|
11
|
+
author.posts.find(1) # no cache read for #find
|
12
|
+
|
13
|
+
* Use loaded collection, instead of read from cache, when use #size, #empty? and #any? from AssociationCollection. Added support for :uniq option.
|
14
|
+
|
15
|
+
* Reduced cache overhead using read instead of fetch for access to AssociationCollection. Enhanced Mocha expectactions.
|
16
|
+
|
17
|
+
# BEFORE
|
18
|
+
author.posts # => cache fetch
|
19
|
+
|
20
|
+
# NOW
|
21
|
+
author.posts # => cache read
|
22
|
+
|
23
|
+
* Fixed typos in CHANGELOG and README
|
24
|
+
|
25
|
+
* Don't instantiate ivar when read from AssociationCollection if options[:cached] == true
|
26
|
+
|
27
|
+
* Reduced by half cache lookups when read from AssociationCollection
|
28
|
+
|
29
|
+
# BEFORE
|
30
|
+
author.posts # => cache read + cache fetch
|
31
|
+
|
32
|
+
# NOW
|
33
|
+
author.posts # => cache fetch
|
34
|
+
|
35
|
+
* Fixed Mocha expectations
|
36
|
+
|
37
|
+
* Fixed clear, delete and destroy cases for AssociationCollection
|
38
|
+
|
39
|
+
author.posts.delete(post)
|
40
|
+
author.posts.delete_all
|
41
|
+
author.posts.destroy
|
42
|
+
author.posts.destroy_all
|
43
|
+
author.posts.clear
|
44
|
+
|
45
|
+
* Fixed concurrency issues, using Thread#current to store cached_associations instead of ivar
|
46
|
+
|
47
|
+
* Make sure tests suite runs in 'test' environment. Introduced SKIP_MOCHA env variable, in order to run tests directly on cache
|
48
|
+
|
49
|
+
$ rake cached_models SKIP_MOCHA=true
|
50
|
+
|
51
|
+
* Bypass cache for will_paginate on association collection
|
52
|
+
|
53
|
+
author.posts.paginate(:all, :page => 1, :per_page => 10)
|
54
|
+
|
55
|
+
* Make sure habtm and has_one are safely used
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
*0.0.2 (October 10th, 2008)*
|
60
|
+
|
61
|
+
* Updated README with new installation instructions
|
62
|
+
|
63
|
+
* Created separated folder for ActiveRecord
|
64
|
+
|
65
|
+
* Added dist related Rake tasks
|
66
|
+
|
67
|
+
* Added gem related files
|
68
|
+
|
69
|
+
* Added Git related Rake tasks
|
70
|
+
|
71
|
+
* Removed default configuration for cache lookup
|
72
|
+
|
73
|
+
* Make sure cache is always used by all the instances which reference the same record
|
74
|
+
|
75
|
+
* Made independent of Rails
|
76
|
+
|
77
|
+
* Allow test suite to work without any active cache server
|
78
|
+
|
79
|
+
* Enhanced AssociationCollection test coverage
|
80
|
+
|
81
|
+
* ActiveRecord::Base#expire_cache_for now uses the new cache access API
|
82
|
+
|
83
|
+
* Abstracted ActiveRecord::Base#cache_fetch in order to normalize cache access for <reflection_name>_ids
|
84
|
+
|
85
|
+
* Reduced the amount of cache hits, caching the status of cached relations with ActiveRecord::Base#cached_associations
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
*0.0.1 (September 10th, 2008)*
|
90
|
+
|
91
|
+
* Updated README with project informations
|
92
|
+
|
93
|
+
* Make sure 'test' is the default Rake task
|
94
|
+
|
95
|
+
* Added project description to README. Added about.yml.
|
96
|
+
|
97
|
+
* Updated README with informations about required environment settings
|
98
|
+
|
99
|
+
* Only load the plugin if the current environment has the cache turned on
|
100
|
+
|
101
|
+
* Added support for cache expiration on after_save callback
|
102
|
+
|
103
|
+
* Make sure to use ActiveRecord cache proxy for test suite
|
104
|
+
|
105
|
+
* Make sure test suite will run using RAILS_ENV in test mode
|
106
|
+
|
107
|
+
* Added support for scoped finders in AssociationCollection. Fixed cache renewal for AssociationCollection#delete.
|
108
|
+
|
109
|
+
* Added support for cache renewal on AssociationCollection methods
|
110
|
+
|
111
|
+
* Added support for cache expiration on direct associated objects updates
|
112
|
+
|
113
|
+
* Updated README example
|
114
|
+
|
115
|
+
* Removed CacheObserver. Fixed cache expiration for has_many relation.
|
116
|
+
|
117
|
+
* Introducing CacheObserver in order to transparently handle cache expiring for has_many macro
|
118
|
+
|
119
|
+
* Test enhancements for AssociationCollection#<< on polymorphic associations
|
120
|
+
|
121
|
+
* Test enhancements for AssociationCollection#<<. Make sure to expire caches when an associated object changes owner.
|
122
|
+
|
123
|
+
class Author < ActiveRecord::Base
|
124
|
+
has_many :posts, :cached => true
|
125
|
+
end
|
126
|
+
|
127
|
+
post = author.posts.last
|
128
|
+
another_author.posts << post # => refresh both author and another_author caches
|
129
|
+
|
130
|
+
* AssociationCollection#<< support
|
131
|
+
|
132
|
+
class Author < ActiveRecord::Base
|
133
|
+
has_many :posts, :cached => true
|
134
|
+
end
|
135
|
+
|
136
|
+
author.posts << post # => causes a refresh of cached posts
|
137
|
+
|
138
|
+
* has_many association support
|
139
|
+
|
140
|
+
class Author < ActiveRecord::Base
|
141
|
+
has_many :posts, :cached => true
|
142
|
+
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.1.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.3'
|
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 ActiveRecord models a transparent approach to use ActiveSupport caching mechanism.
|
5
|
+
description: CachedModels provides to your ActiveRecord models a transparent approach to use ActiveSupport caching mechanism.
|
6
|
+
license: MIT
|
7
|
+
rails_version: 2.1.0+
|
8
|
+
version: 0.0.3
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "cached-models"
|
3
|
+
s.version = "0.0.3"
|
4
|
+
s.date = "2008-10-22"
|
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_and_belongs_to_many_association_test.rb", "test/active_record/associations/has_many_association_test.rb", "test/active_record/associations/has_one_association_test.rb", "test/active_record/base_test.rb", "test/fixtures/addresses.yml", "test/fixtures/authors.yml", "test/fixtures/blogs.yml", "test/fixtures/categories.yml", "test/fixtures/categories_posts.yml", "test/fixtures/comments.yml", "test/fixtures/posts.yml", "test/fixtures/tags.yml", "test/models/address.rb", "test/models/author.rb", "test/models/blog.rb", "test/models/category.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_and_belongs_to_many_association_test.rb", "test/active_record/associations/has_many_association_test.rb", "test/active_record/associations/has_one_association_test.rb", "test/active_record/base_test.rb"]
|
14
|
+
s.extra_rdoc_files = ['README', 'CHANGELOG']
|
15
|
+
|
16
|
+
s.add_dependency("activesupport", ["> 2.1.0"])
|
17
|
+
s.add_dependency("activerecord", ["> 2.1.0"])
|
18
|
+
end
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,404 @@
|
|
1
|
+
# FIXME load paths
|
2
|
+
require File.dirname(__FILE__) + '/associations/association_proxy'
|
3
|
+
require File.dirname(__FILE__) + '/associations/association_collection'
|
4
|
+
require File.dirname(__FILE__) + '/associations/has_many_association'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Associations
|
8
|
+
module ClassMethods
|
9
|
+
# Adds the following methods for retrieval and query of collections of associated objects:
|
10
|
+
# +collection+ is replaced with the symbol passed as the first argument, so
|
11
|
+
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
12
|
+
# * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
|
13
|
+
# An empty array is returned if none are found.
|
14
|
+
# * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
15
|
+
# * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
16
|
+
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
|
17
|
+
# * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
|
18
|
+
# * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
|
19
|
+
# * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
|
20
|
+
# * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
|
21
|
+
# are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
|
22
|
+
# otherwise sets their foreign keys to +NULL+.
|
23
|
+
# * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
|
24
|
+
# * <tt>collection.size</tt> - Returns the number of associated objects.
|
25
|
+
# * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
|
26
|
+
# * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
|
27
|
+
# with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
|
28
|
+
# associated object already exists, not if it's +nil+!
|
29
|
+
# * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
30
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
31
|
+
# *Note:* This only works if an associated object already exists, not if it's +nil+!
|
32
|
+
#
|
33
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
34
|
+
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
|
35
|
+
# * <tt>Firm#clients<<</tt>
|
36
|
+
# * <tt>Firm#clients.delete</tt>
|
37
|
+
# * <tt>Firm#clients=</tt>
|
38
|
+
# * <tt>Firm#client_ids</tt>
|
39
|
+
# * <tt>Firm#client_ids=</tt>
|
40
|
+
# * <tt>Firm#clients.clear</tt>
|
41
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
42
|
+
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
43
|
+
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
44
|
+
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
45
|
+
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
46
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
47
|
+
#
|
48
|
+
# Options are:
|
49
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
50
|
+
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
|
51
|
+
# if the real class name is SpecialProduct, you'll have to specify it with this option.
|
52
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
|
53
|
+
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
|
54
|
+
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
55
|
+
# or <tt>@blog.posts.build</tt>.
|
56
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
57
|
+
# such as <tt>last_name, first_name DESC</tt>.
|
58
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
59
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
|
60
|
+
# as the default <tt>:foreign_key</tt>.
|
61
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
|
62
|
+
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
63
|
+
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
64
|
+
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
|
65
|
+
# the <tt>:through</tt> option.
|
66
|
+
# * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
67
|
+
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
68
|
+
# * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
|
69
|
+
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
|
70
|
+
# * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
|
71
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
|
72
|
+
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
73
|
+
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
|
74
|
+
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
75
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
|
76
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will rise an error.
|
77
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
78
|
+
# * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
79
|
+
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
|
80
|
+
# or <tt>has_many</tt> association on the join model.
|
81
|
+
# * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
82
|
+
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
|
83
|
+
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
|
84
|
+
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
|
85
|
+
# association is a polymorphic +belongs_to+.
|
86
|
+
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
87
|
+
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
88
|
+
# * <tt>:cached</tt> - If true, all the associated objects will be cached.
|
89
|
+
#
|
90
|
+
# Option examples:
|
91
|
+
# has_many :comments, :order => "posted_on"
|
92
|
+
# has_many :comments, :include => :author
|
93
|
+
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
|
94
|
+
# has_many :tracks, :order => "position", :dependent => :destroy
|
95
|
+
# has_many :comments, :dependent => :nullify
|
96
|
+
# has_many :tags, :as => :taggable
|
97
|
+
# has_many :reports, :readonly => true
|
98
|
+
# has_many :posts, :cached => true
|
99
|
+
# has_many :subscribers, :through => :subscriptions, :source => :user
|
100
|
+
# has_many :subscribers, :class_name => "Person", :finder_sql =>
|
101
|
+
# 'SELECT DISTINCT people.* ' +
|
102
|
+
# 'FROM people p, post_subscriptions ps ' +
|
103
|
+
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
104
|
+
# 'ORDER BY p.first_name'
|
105
|
+
def has_many(association_id, options = {}, &extension)
|
106
|
+
reflection = create_has_many_reflection(association_id, options, &extension)
|
107
|
+
|
108
|
+
configure_dependency_for_has_many(reflection)
|
109
|
+
|
110
|
+
add_multiple_associated_save_callbacks(reflection.name)
|
111
|
+
add_association_callbacks(reflection.name, reflection.options)
|
112
|
+
|
113
|
+
if options[:through]
|
114
|
+
collection_accessor_methods(reflection, HasManyThroughAssociation, options)
|
115
|
+
else
|
116
|
+
collection_accessor_methods(reflection, HasManyAssociation, options)
|
117
|
+
end
|
118
|
+
|
119
|
+
add_cache_callbacks if options[:cached]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
|
123
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
124
|
+
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
|
125
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
126
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
127
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
128
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
129
|
+
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
|
130
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
131
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
132
|
+
#
|
133
|
+
# Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
|
134
|
+
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
135
|
+
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
136
|
+
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
137
|
+
# * <tt>Post#author.nil?</tt>
|
138
|
+
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
139
|
+
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
140
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
141
|
+
#
|
142
|
+
# Options are:
|
143
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
144
|
+
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
|
145
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
146
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
147
|
+
# SQL fragment, such as <tt>authorized = 1</tt>.
|
148
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
149
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
150
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
151
|
+
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
|
152
|
+
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
|
153
|
+
# will use a foreign key of "favorite_person_id".
|
154
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
155
|
+
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
|
156
|
+
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
|
157
|
+
# orphaned records behind.
|
158
|
+
# * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
|
159
|
+
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
|
160
|
+
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
161
|
+
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
|
162
|
+
# a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
163
|
+
# When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
|
164
|
+
# this results in a counter with +NULL+ value, which will never increment.
|
165
|
+
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
|
166
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
167
|
+
# * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
|
168
|
+
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
169
|
+
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
170
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
171
|
+
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
|
172
|
+
#
|
173
|
+
# Option examples:
|
174
|
+
# belongs_to :firm, :foreign_key => "client_of"
|
175
|
+
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
176
|
+
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
177
|
+
# :conditions => 'discounts > #{payments_count}'
|
178
|
+
# belongs_to :attachable, :polymorphic => true
|
179
|
+
# belongs_to :project, :readonly => true
|
180
|
+
# belongs_to :post, :counter_cache => true
|
181
|
+
# belongs_to :blog, :cached => true
|
182
|
+
def belongs_to(association_id, options = {})
|
183
|
+
reflection = create_belongs_to_reflection(association_id, options)
|
184
|
+
|
185
|
+
ivar = "@#{reflection.name}"
|
186
|
+
|
187
|
+
if reflection.options[:polymorphic]
|
188
|
+
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
189
|
+
|
190
|
+
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
|
191
|
+
define_method(method_name) do
|
192
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
193
|
+
|
194
|
+
if association && association.target
|
195
|
+
if association.new_record?
|
196
|
+
association.save(true)
|
197
|
+
end
|
198
|
+
|
199
|
+
if association.updated?
|
200
|
+
self["#{reflection.primary_key_name}"] = association.id
|
201
|
+
self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
before_save method_name
|
206
|
+
else
|
207
|
+
association_accessor_methods(reflection, BelongsToAssociation)
|
208
|
+
association_constructor_method(:build, reflection, BelongsToAssociation)
|
209
|
+
association_constructor_method(:create, reflection, BelongsToAssociation)
|
210
|
+
|
211
|
+
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
|
212
|
+
define_method(method_name) do
|
213
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
214
|
+
|
215
|
+
if !association.nil?
|
216
|
+
if association.new_record?
|
217
|
+
association.save(true)
|
218
|
+
end
|
219
|
+
|
220
|
+
if association.updated?
|
221
|
+
self["#{reflection.primary_key_name}"] = association.id
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
before_save method_name
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create the callbacks to update counter cache
|
229
|
+
if options[:counter_cache]
|
230
|
+
cache_column = options[:counter_cache] == true ?
|
231
|
+
"#{self.to_s.underscore.pluralize}_count" :
|
232
|
+
options[:counter_cache]
|
233
|
+
|
234
|
+
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
235
|
+
define_method(method_name) do
|
236
|
+
association = send("#{reflection.name}")
|
237
|
+
association.class.increment_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
238
|
+
end
|
239
|
+
after_create method_name
|
240
|
+
|
241
|
+
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
242
|
+
define_method(method_name) do
|
243
|
+
association = send("#{reflection.name}")
|
244
|
+
association.class.decrement_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
245
|
+
end
|
246
|
+
before_destroy method_name
|
247
|
+
|
248
|
+
module_eval(
|
249
|
+
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
if options[:cached]
|
254
|
+
after_save_method_name = "belongs_to_after_save_for_#{reflection.name}".to_sym
|
255
|
+
after_destroy_method_name = "belongs_to_after_destroy_for_#{reflection.name}".to_sym
|
256
|
+
define_method(after_save_method_name) do
|
257
|
+
send(reflection.name).expire_cache_for(self.class.name)
|
258
|
+
end
|
259
|
+
|
260
|
+
alias_method after_destroy_method_name, after_save_method_name
|
261
|
+
after_save after_save_method_name
|
262
|
+
after_destroy after_destroy_method_name
|
263
|
+
end
|
264
|
+
|
265
|
+
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
266
|
+
|
267
|
+
configure_dependency_for_belongs_to(reflection)
|
268
|
+
end
|
269
|
+
|
270
|
+
def collection_reader_method(reflection, association_proxy_class, options)
|
271
|
+
define_method(reflection.name) do |*params|
|
272
|
+
ivar = "@#{reflection.name}"
|
273
|
+
|
274
|
+
force_reload = params.first unless params.empty?
|
275
|
+
|
276
|
+
association = if options[:cached]
|
277
|
+
cache_read(reflection)
|
278
|
+
else
|
279
|
+
instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
280
|
+
end
|
281
|
+
|
282
|
+
unless association.respond_to?(:loaded?)
|
283
|
+
association = association_proxy_class.new(self, reflection)
|
284
|
+
if options[:cached]
|
285
|
+
cache_write(reflection, association)
|
286
|
+
else
|
287
|
+
instance_variable_set(ivar, association)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
if force_reload
|
292
|
+
association.reload
|
293
|
+
cache_write(reflection, association) if options[:cached]
|
294
|
+
end
|
295
|
+
|
296
|
+
association
|
297
|
+
end
|
298
|
+
|
299
|
+
method_name = "#{reflection.name.to_s.singularize}_ids"
|
300
|
+
define_method(method_name) do
|
301
|
+
if options[:cached]
|
302
|
+
cache_fetch("#{cache_key}/#{method_name}", send("calculate_#{method_name}"))
|
303
|
+
else
|
304
|
+
send("calculate_#{method_name}")
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
define_method("calculate_#{method_name}") do
|
309
|
+
send(reflection.name).map { |record| record.id }
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def has_and_belongs_to_many(association_id, options = {}, &extension)
|
314
|
+
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
315
|
+
|
316
|
+
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
|
317
|
+
add_multiple_associated_save_callbacks(reflection.name)
|
318
|
+
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation, options)
|
319
|
+
|
320
|
+
# Don't use a before_destroy callback since users' before_destroy
|
321
|
+
# callbacks will be executed after the association is wiped out.
|
322
|
+
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
|
323
|
+
class_eval <<-end_eval unless method_defined?(old_method)
|
324
|
+
alias_method :#{old_method}, :destroy_without_callbacks
|
325
|
+
def destroy_without_callbacks
|
326
|
+
#{reflection.name}.clear
|
327
|
+
#{old_method}
|
328
|
+
end
|
329
|
+
end_eval
|
330
|
+
|
331
|
+
add_association_callbacks(reflection.name, options)
|
332
|
+
end
|
333
|
+
|
334
|
+
def collection_accessor_methods(reflection, association_proxy_class, options, writer = true)
|
335
|
+
collection_reader_method(reflection, association_proxy_class, options)
|
336
|
+
|
337
|
+
if writer
|
338
|
+
define_method("#{reflection.name}=") do |new_value|
|
339
|
+
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
|
340
|
+
association = send(reflection.name)
|
341
|
+
association.replace(new_value)
|
342
|
+
|
343
|
+
cache_write(reflection, association) if options[:cached]
|
344
|
+
|
345
|
+
association
|
346
|
+
end
|
347
|
+
|
348
|
+
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
349
|
+
ids = (new_value || []).reject { |nid| nid.blank? }
|
350
|
+
send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def create_has_many_reflection(association_id, options, &extension)
|
356
|
+
options.assert_valid_keys(
|
357
|
+
:class_name, :table_name, :foreign_key, :primary_key,
|
358
|
+
:dependent,
|
359
|
+
:select, :conditions, :include, :order, :group, :limit, :offset,
|
360
|
+
:as, :through, :source, :source_type,
|
361
|
+
:uniq,
|
362
|
+
:finder_sql, :counter_sql,
|
363
|
+
:before_add, :after_add, :before_remove, :after_remove,
|
364
|
+
:extend, :readonly,
|
365
|
+
:validate, :accessible,
|
366
|
+
:cached
|
367
|
+
)
|
368
|
+
|
369
|
+
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
370
|
+
|
371
|
+
create_reflection(:has_many, association_id, options, self)
|
372
|
+
end
|
373
|
+
|
374
|
+
def create_belongs_to_reflection(association_id, options)
|
375
|
+
options.assert_valid_keys(
|
376
|
+
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
|
377
|
+
:counter_cache, :extend, :polymorphic, :readonly, :validate, :cached
|
378
|
+
)
|
379
|
+
|
380
|
+
reflection = create_reflection(:belongs_to, association_id, options, self)
|
381
|
+
|
382
|
+
if options[:polymorphic]
|
383
|
+
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
|
384
|
+
end
|
385
|
+
|
386
|
+
reflection
|
387
|
+
end
|
388
|
+
|
389
|
+
def add_cache_callbacks
|
390
|
+
method_name = :after_save_cache_expire
|
391
|
+
return if respond_to? method_name
|
392
|
+
|
393
|
+
define_method(method_name) do
|
394
|
+
return unless self[:updated_at]
|
395
|
+
|
396
|
+
self.class.reflections.each do |name, reflection|
|
397
|
+
cache_delete(reflection) if reflection.options[:cached]
|
398
|
+
end
|
399
|
+
end
|
400
|
+
after_save method_name
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|