From 8c512a1caffa11d1522cef5d2a35e2888c608581 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 3 Nov 2005 09:06:42 +0000 Subject: Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2861 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 21 +++++++++++++ activerecord/lib/active_record/associations.rb | 34 +++++++++++++++++++--- .../associations/association_proxy.rb | 7 +++-- activerecord/test/associations_extensions_test.rb | 15 ++++++++++ activerecord/test/fixtures/developer.rb | 8 ++++- activerecord/test/fixtures/post.rb | 16 ++++++++-- 6 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 activerecord/test/associations_extensions_test.rb diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index efec88c651..f0151360cf 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,26 @@ *SVN* +* Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH]. Example: + + class Account < ActiveRecord::Base + has_many :people, :extend => Module.new { + def find_or_create_by_name(name) + first_name, *last_name = name.split + last_name = last_name.join " " + + find_by_first_name_and_last_name(first_name, last_name) || + create({ :first_name => first_name, :last_name => last_name }) + end + } + end + + person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") + person.first_name # => "David" + person.last_name # => "Heinemeier Hansson" + + Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation). + + * A missing primary key column shouldn't raise an error when generating its error message. [Don Park ] * Changed :dbfile to :database for SQLite adapter for consistency (old key still works as an alias) #2644 [Dan Peterson] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 9b3296b3e4..5a9dc21a78 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -120,6 +120,30 @@ module ActiveRecord # Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with # the before_remove callbacks, if an exception is thrown the object doesn't get removed. # + # === Association extensions + # + # The proxy objects that controls the access to associations can be extended through anonymous modules. This is especially + # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this associatio. + # Example: + # + # class Account < ActiveRecord::Base + # has_many :people, :extend => Module.new { + # def find_or_create_by_name(name) + # first_name, *last_name = name.split + # last_name = last_name.join " " + # + # find_by_first_name_and_last_name(first_name, last_name) || + # create({ :first_name => first_name, :last_name => last_name }) + # end + # } + # end + # + # person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") + # person.first_name # => "David" + # person.last_name # => "Heinemeier Hansson" + # + # Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation). + # # == Caching # # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically @@ -282,6 +306,7 @@ module ActiveRecord # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. # * :counter_sql - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is # specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM. + # * :extend - anonymous module for extending the proxy, see "Association extensions". # # Option examples: # has_many :comments, :order => "posted_on" @@ -296,7 +321,7 @@ module ActiveRecord options.assert_valid_keys( :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove + :before_add, :after_add, :before_remove, :after_remove, :extend ) association_name, association_class_name, association_class_primary_key_name = @@ -380,7 +405,7 @@ module ActiveRecord # has_one :last_comment, :class_name => "Comment", :order => "posted_on" # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" def has_one(association_id, options = {}) - options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache) + options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache, :extend) association_name, association_class_name, association_class_primary_key_name = associate_identification(association_id, options[:class_name], options[:foreign_key], false) @@ -460,7 +485,7 @@ module ActiveRecord # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", # :conditions => 'discounts > #{payments_count}' def belongs_to(association_id, options = {}) - options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache) + options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache, :extend) association_name, association_class_name, class_primary_key_name = associate_identification(association_id, options[:class_name], options[:foreign_key], false) @@ -569,6 +594,7 @@ module ActiveRecord # classes with a manual one # * :insert_sql - overwrite the default generated SQL used to add links between the associated classes # with a manual one + # * :extend - anonymous module for extending the proxy, see "Association extensions". # # Option examples: # has_and_belongs_to_many :projects @@ -580,7 +606,7 @@ module ActiveRecord options.assert_valid_keys( :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add, - :before_remove, :after_remove + :before_remove, :after_remove, :extend ) association_name, association_class_name, association_class_primary_key_name = diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index c8d72c9f25..8245234854 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -2,7 +2,8 @@ module ActiveRecord module Associations class AssociationProxy #:nodoc: alias_method :proxy_respond_to?, :respond_to? - instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^send)/ } + alias_method :proxy_extend, :extend + instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ } def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) @owner = owner @@ -11,6 +12,8 @@ module ActiveRecord @association_class = eval(association_class_name, nil, __FILE__, __LINE__) @association_class_primary_key_name = association_class_primary_key_name + proxy_extend(options[:extend]) if options[:extend] + reset end @@ -95,4 +98,4 @@ module ActiveRecord end end end -end +end \ No newline at end of file diff --git a/activerecord/test/associations_extensions_test.rb b/activerecord/test/associations_extensions_test.rb new file mode 100644 index 0000000000..92bdb86eb5 --- /dev/null +++ b/activerecord/test/associations_extensions_test.rb @@ -0,0 +1,15 @@ +require 'abstract_unit' +require 'fixtures/project' +require 'fixtures/developer' + +class AssociationsExtensionsTest < Test::Unit::TestCase + fixtures :projects, :developers + + def test_extension_on_habtm + assert_equal projects(:action_controller), developers(:david).projects.find_most_recent + end + + def test_extension_on_has_many + assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent + end +end \ No newline at end of file diff --git a/activerecord/test/fixtures/developer.rb b/activerecord/test/fixtures/developer.rb index 1a4f0648bb..336b48f087 100644 --- a/activerecord/test/fixtures/developer.rb +++ b/activerecord/test/fixtures/developer.rb @@ -1,5 +1,11 @@ class Developer < ActiveRecord::Base - has_and_belongs_to_many :projects + has_and_belongs_to_many :projects, :extend => Module.new { + def find_most_recent + find(:first, :order => "id DESC") + end + } + + has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' validates_inclusion_of :salary, :in => 50000..200000 diff --git a/activerecord/test/fixtures/post.rb b/activerecord/test/fixtures/post.rb index f5adac41dc..6163ec90f8 100644 --- a/activerecord/test/fixtures/post.rb +++ b/activerecord/test/fixtures/post.rb @@ -1,7 +1,17 @@ class Post < ActiveRecord::Base - belongs_to :author - has_many :comments, :order => "body" - has_one :very_special_comment, :class_name => "VerySpecialComment" + belongs_to :author, :extend => Module.new { + def greeting + "hello" + end + } + + has_many :comments, :order => "body", :extend => Module.new { + def find_most_recent + find(:first, :order => "id DESC") + end + } + + has_many :special_comments, :class_name => "SpecialComment" has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts" -- cgit v1.2.3