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.
Files changed (40) hide show
  1. data/CHANGELOG +142 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README +95 -0
  4. data/Rakefile +81 -0
  5. data/about.yml +8 -0
  6. data/cached-models.gemspec +18 -0
  7. data/init.rb +2 -0
  8. data/install.rb +1 -0
  9. data/lib/activerecord/lib/active_record.rb +4 -0
  10. data/lib/activerecord/lib/active_record/associations.rb +404 -0
  11. data/lib/activerecord/lib/active_record/associations/association_collection.rb +127 -0
  12. data/lib/activerecord/lib/active_record/associations/association_proxy.rb +39 -0
  13. data/lib/activerecord/lib/active_record/associations/has_many_association.rb +7 -0
  14. data/lib/activerecord/lib/active_record/base.rb +73 -0
  15. data/lib/cached-models.rb +1 -0
  16. data/lib/cached_models.rb +4 -0
  17. data/setup.rb +1585 -0
  18. data/tasks/cached_models_tasks.rake +117 -0
  19. data/test/active_record/associations/has_and_belongs_to_many_association_test.rb +12 -0
  20. data/test/active_record/associations/has_many_association_test.rb +355 -0
  21. data/test/active_record/associations/has_one_association_test.rb +12 -0
  22. data/test/active_record/base_test.rb +32 -0
  23. data/test/fixtures/addresses.yml +7 -0
  24. data/test/fixtures/authors.yml +13 -0
  25. data/test/fixtures/blogs.yml +7 -0
  26. data/test/fixtures/categories.yml +3 -0
  27. data/test/fixtures/categories_posts.yml +3 -0
  28. data/test/fixtures/comments.yml +19 -0
  29. data/test/fixtures/posts.yml +23 -0
  30. data/test/fixtures/tags.yml +14 -0
  31. data/test/models/address.rb +3 -0
  32. data/test/models/author.rb +12 -0
  33. data/test/models/blog.rb +4 -0
  34. data/test/models/category.rb +3 -0
  35. data/test/models/comment.rb +3 -0
  36. data/test/models/post.rb +8 -0
  37. data/test/models/tag.rb +3 -0
  38. data/test/test_helper.rb +88 -0
  39. data/uninstall.rb +1 -0
  40. 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
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require 'cached_models'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,4 @@
1
+ # FIXME load paths
2
+ path = File.dirname(__FILE__)
3
+ require "#{path}/active_record/base"
4
+ require "#{path}/active_record/associations"
@@ -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