Skip to content
This repository
Browse code

Added preliminary support for join models [DHH] Added preliminary sup…

…port for polymorphic associations [DHH] Refactored associations to use reflections to get DRYer, beware, major refactoring -- double check before deploying anything with this (all tests pass, but..)

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3213 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 6abda696b5df14a9ab132c34311daaabe12030e6 1 parent 57b7532
David Heinemeier Hansson dhh authored
4 activerecord/CHANGELOG
... ... @@ -1,5 +1,9 @@
1 1 *SVN*
2 2
  3 +* Added preliminary support for polymorphic associations [DHH]
  4 +
  5 +* Added preliminary support for join models [DHH]
  6 +
3 7 * Allow validate_uniqueness_of to be scoped by more than just one column. #1559. [jeremy@jthopple.com, Marcel Molina Jr.]
4 8
5 9 * Firebird: active? and reconnect! methods for handling stale connections. #428 [Ken Kunz <kennethkunz@gmail.com>]
2  activerecord/lib/active_record.rb
@@ -38,10 +38,10 @@
38 38 require 'active_record/observer'
39 39 require 'active_record/validations'
40 40 require 'active_record/callbacks'
  41 +require 'active_record/reflection'
41 42 require 'active_record/associations'
42 43 require 'active_record/aggregations'
43 44 require 'active_record/transactions'
44   -require 'active_record/reflection'
45 45 require 'active_record/timestamp'
46 46 require 'active_record/acts/list'
47 47 require 'active_record/acts/tree'
5 activerecord/lib/active_record/aggregations.rb
... ... @@ -1,7 +1,6 @@
1 1 module ActiveRecord
2 2 module Aggregations # :nodoc:
3   - def self.append_features(base)
4   - super
  3 + def self.included(base)
5 4 base.extend(ClassMethods)
6 5 end
7 6
@@ -128,6 +127,8 @@ def composed_of(part_id, options = {})
128 127
129 128 reader_method(name, class_name, mapping)
130 129 writer_method(name, class_name, mapping)
  130 +
  131 + create_reflection(:composed_of, part_id, options, self)
131 132 end
132 133
133 134 private
383 activerecord/lib/active_record/associations.rb
@@ -4,6 +4,7 @@
4 4 require 'active_record/associations/belongs_to_polymorphic_association'
5 5 require 'active_record/associations/has_one_association'
6 6 require 'active_record/associations/has_many_association'
  7 +require 'active_record/associations/has_many_through_association'
7 8 require 'active_record/associations/has_and_belongs_to_many_association'
8 9 require 'active_record/deprecated_associations'
9 10
@@ -341,57 +342,19 @@ module ClassMethods
341 342 # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
342 343 # 'ORDER BY p.first_name'
343 344 def has_many(association_id, options = {}, &extension)
344   - options.assert_valid_keys(
345   - :foreign_key, :class_name, :exclusively_dependent, :dependent,
346   - :conditions, :order, :include, :finder_sql, :counter_sql,
347   - :before_add, :after_add, :before_remove, :after_remove, :extend,
348   - :group, :as
349   - )
  345 + reflection = create_has_many_reflection(association_id, options, &extension)
350 346
351   - options[:extend] = create_extension_module(association_id, extension) if block_given?
  347 + configure_dependency_for_has_many(reflection)
352 348
353   - association_name, association_class_name, association_class_primary_key_name =
354   - associate_identification(association_id, options[:class_name], options[:foreign_key])
355   -
356   - require_association_class(association_class_name)
357   -
358   - raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' if options[:dependent] and options[:exclusively_dependent]
359   -
360   - if options[:exclusively_dependent]
361   - options[:dependent] = :delete_all
362   - #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
363   - end
364   -
365   - # See HasManyAssociation#delete_records. Dependent associations
366   - # delete children, otherwise foreign key is set to NULL.
367   - case options[:dependent]
368   - when :destroy, true
369   - module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
370   - when :delete_all
371   - module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
372   - when :nullify
373   - module_eval "before_destroy { |record| #{association_class_name}.update_all(%(#{association_class_primary_key_name} = NULL), %(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
374   - when nil, false
375   - # pass
376   - else
377   - raise ArgumentError, 'The :dependent option expects either true, :destroy, :delete_all, or :nullify'
  349 + if options[:through]
  350 + collection_reader_method(reflection, HasManyThroughAssociation)
  351 + else
  352 + add_multiple_associated_save_callbacks(reflection.name)
  353 + add_association_callbacks(reflection.name, reflection.options)
  354 + collection_accessor_methods(reflection, HasManyAssociation)
378 355 end
379 356
380   -
381   - add_multiple_associated_save_callbacks(association_name)
382   - add_association_callbacks(association_name, options)
383   -
384   - collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
385   -
386   - # deprecated api
387   - deprecated_collection_count_method(association_name)
388   - deprecated_add_association_relation(association_name)
389   - deprecated_remove_association_relation(association_name)
390   - deprecated_has_collection_method(association_name)
391   - deprecated_find_in_collection_method(association_name)
392   - deprecated_find_all_in_collection_method(association_name)
393   - deprecated_collection_create_method(association_name)
394   - deprecated_collection_build_method(association_name)
  357 + add_deprecated_api_for_has_many(reflection.name)
395 358 end
396 359
397 360 # Adds the following methods for retrieval and query of a single associated object.
@@ -436,42 +399,27 @@ def has_many(association_id, options = {}, &extension)
436 399 # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
437 400 # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
438 401 def has_one(association_id, options = {})
439   - options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend)
440   -
441   - association_name, association_class_name, association_class_primary_key_name =
442   - associate_identification(association_id, options[:class_name], options[:foreign_key], false)
443   -
444   - require_association_class(association_class_name)
  402 + reflection = create_has_one_reflection(association_id, options)
445 403
446 404 module_eval do
447 405 after_save <<-EOF
448   - association = instance_variable_get("@#{association_name}")
  406 + association = instance_variable_get("@#{reflection.name}")
449 407 unless association.nil?
450   - association["#{association_class_primary_key_name}"] = id
  408 + association["#{reflection.primary_key_name}"] = id
451 409 association.save(true)
452   - association.send(:construct_sql)
453 410 end
454 411 EOF
455 412 end
456 413
457   - association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
458   - association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
459   - association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
  414 + association_accessor_methods(reflection, HasOneAssociation)
  415 + association_constructor_method(:build, reflection, HasOneAssociation)
  416 + association_constructor_method(:create, reflection, HasOneAssociation)
460 417
461   - case options[:dependent]
462   - when :destroy, true
463   - module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'"
464   - when :nullify
465   - module_eval "before_destroy '#{association_name}.update_attribute(\"#{association_class_primary_key_name}\", nil)'"
466   - when nil, false
467   - # pass
468   - else
469   - raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
470   - end
  418 + configure_dependency_for_has_one(reflection)
471 419
472 420 # deprecated api
473   - deprecated_has_association_method(association_name)
474   - deprecated_association_comparison_method(association_name, association_class_name)
  421 + deprecated_has_association_method(reflection.name)
  422 + deprecated_association_comparison_method(reflection.name, reflection.class_name)
475 423 end
476 424
477 425 # Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
@@ -517,52 +465,41 @@ def has_one(association_id, options = {})
517 465 # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
518 466 # :conditions => 'discounts > #{payments_count}'
519 467 def belongs_to(association_id, options = {})
520   - options.assert_valid_keys(:class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :polymorphic)
521   -
522   - association_name, association_class_name, class_primary_key_name =
523   - associate_identification(association_id, options[:class_name], options[:foreign_key], false)
524   -
525   - association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key
526   -
527   - if options[:polymorphic]
528   - options[:foreign_type] ||= association_class_name.underscore + "_type"
529   -
530   - association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToPolymorphicAssociation)
  468 + reflection = create_belongs_to_reflection(association_id, options)
  469 +
  470 + if reflection.options[:polymorphic]
  471 + association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
531 472
532 473 module_eval do
533 474 before_save <<-EOF
534   - association = instance_variable_get("@#{association_name}")
  475 + association = instance_variable_get("@#{reflection.name}")
535 476 if !association.nil?
536 477 if association.new_record?
537 478 association.save(true)
538   - association.send(:construct_sql)
539 479 end
540 480
541 481 if association.updated?
542   - self["#{association_class_primary_key_name}"] = association.id
543   - self["#{options[:foreign_type]}"] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, association.class).to_s
  482 + self["#{reflection.primary_key_name}"] = association.id
  483 + self["#{reflection.options[:foreign_type]}"] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, association.class).to_s
544 484 end
545 485 end
546 486 EOF
547 487 end
548 488 else
549   - require_association_class(association_class_name)
550   -
551   - association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
552   - association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
553   - association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
  489 + association_accessor_methods(reflection, BelongsToAssociation)
  490 + association_constructor_method(:build, reflection, BelongsToAssociation)
  491 + association_constructor_method(:create, reflection, BelongsToAssociation)
554 492
555 493 module_eval do
556 494 before_save <<-EOF
557   - association = instance_variable_get("@#{association_name}")
  495 + association = instance_variable_get("@#{reflection.name}")
558 496 if !association.nil?
559 497 if association.new_record?
560 498 association.save(true)
561   - association.send(:construct_sql)
562 499 end
563 500
564 501 if association.updated?
565   - self["#{association_class_primary_key_name}"] = association.id
  502 + self["#{reflection.primary_key_name}"] = association.id
566 503 end
567 504 end
568 505 EOF
@@ -570,19 +507,19 @@ def belongs_to(association_id, options = {})
570 507
571 508 if options[:counter_cache]
572 509 module_eval(
573   - "after_create '#{association_class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" +
574   - " unless #{association_name}.nil?'"
  510 + "after_create '#{reflection.class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{reflection.primary_key_name})" +
  511 + " unless #{reflection.name}.nil?'"
575 512 )
576 513
577 514 module_eval(
578   - "before_destroy '#{association_class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" +
579   - " unless #{association_name}.nil?'"
  515 + "before_destroy '#{reflection.class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{reflection.primary_key_name})" +
  516 + " unless #{reflection.name}.nil?'"
580 517 )
581 518 end
582 519
583 520 # deprecated api
584   - deprecated_has_association_method(association_name)
585   - deprecated_association_comparison_method(association_name, association_class_name)
  521 + deprecated_has_association_method(reflection.name)
  522 + deprecated_association_comparison_method(reflection.name, reflection.class_name)
586 523 end
587 524 end
588 525
@@ -663,43 +600,29 @@ def belongs_to(association_id, options = {})
663 600 # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
664 601 # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
665 602 def has_and_belongs_to_many(association_id, options = {}, &extension)
666   - options.assert_valid_keys(
667   - :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :include,
668   - :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
669   - :before_remove, :after_remove, :extend
670   - )
671   -
672   - options[:extend] = create_extension_module(association_id, extension) if block_given?
673   -
674   - association_name, association_class_name, association_class_primary_key_name =
675   - associate_identification(association_id, options[:class_name], options[:foreign_key])
676   -
677   - require_association_class(association_class_name)
678   -
679   - options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
680   -
681   - add_multiple_associated_save_callbacks(association_name)
682   -
683   - collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
  603 + reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
  604 +
  605 + add_multiple_associated_save_callbacks(reflection.name)
  606 + collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
684 607
685 608 # Don't use a before_destroy callback since users' before_destroy
686 609 # callbacks will be executed after the association is wiped out.
687   - old_method = "destroy_without_habtm_shim_for_#{association_name}"
  610 + old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
688 611 class_eval <<-end_eval
689 612 alias_method :#{old_method}, :destroy_without_callbacks
690 613 def destroy_without_callbacks
691   - #{association_name}.clear
  614 + #{reflection.name}.clear
692 615 #{old_method}
693 616 end
694 617 end_eval
695 618
696   - add_association_callbacks(association_name, options)
  619 + add_association_callbacks(reflection.name, options)
697 620
698 621 # deprecated api
699   - deprecated_collection_count_method(association_name)
700   - deprecated_add_association_relation(association_name)
701   - deprecated_remove_association_relation(association_name)
702   - deprecated_has_collection_method(association_name)
  622 + deprecated_collection_count_method(reflection.name)
  623 + deprecated_add_association_relation(reflection.name)
  624 + deprecated_remove_association_relation(reflection.name)
  625 + deprecated_has_collection_method(reflection.name)
703 626 end
704 627
705 628 private
@@ -713,93 +636,81 @@ def join_table_name(first_table_name, second_table_name)
713 636 table_name_prefix + join_table + table_name_suffix
714 637 end
715 638
716   - def associate_identification(association_id, association_class_name, foreign_key, plural = true)
717   - if association_class_name !~ /::/
718   - association_class_name = type_name_with_module(
719   - association_class_name ||
720   - Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
721   - )
722   - end
723   -
724   - primary_key_name = foreign_key || name.foreign_key
725   -
726   - return association_id.id2name, association_class_name, primary_key_name
727   - end
728   -
729   - def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
730   - define_method(association_name) do |*params|
  639 + def association_accessor_methods(reflection, association_proxy_class)
  640 + define_method(reflection.name) do |*params|
731 641 force_reload = params.first unless params.empty?
732   - association = instance_variable_get("@#{association_name}")
733   - if association.nil? or force_reload
734   - association = association_proxy_class.new(self,
735   - association_name, association_class_name,
736   - association_class_primary_key_name, options)
  642 + association = instance_variable_get("@#{reflection.name}")
  643 +
  644 + if association.nil? || force_reload
  645 + association = association_proxy_class.new(self, reflection)
737 646 retval = association.reload
738 647 unless retval.nil?
739   - instance_variable_set("@#{association_name}", association)
  648 + instance_variable_set("@#{reflection.name}", association)
740 649 else
741   - instance_variable_set("@#{association_name}", nil)
  650 + instance_variable_set("@#{reflection.name}", nil)
742 651 return nil
743 652 end
744 653 end
745 654 association
746 655 end
747 656
748   - define_method("#{association_name}=") do |new_value|
749   - association = instance_variable_get("@#{association_name}")
  657 + define_method("#{reflection.name}=") do |new_value|
  658 + association = instance_variable_get("@#{reflection.name}")
750 659 if association.nil?
751   - association = association_proxy_class.new(self,
752   - association_name, association_class_name,
753   - association_class_primary_key_name, options)
  660 + association = association_proxy_class.new(self, reflection)
754 661 end
  662 +
755 663 association.replace(new_value)
  664 +
756 665 unless new_value.nil?
757   - instance_variable_set("@#{association_name}", association)
  666 + instance_variable_set("@#{reflection.name}", association)
758 667 else
759   - instance_variable_set("@#{association_name}", nil)
  668 + instance_variable_set("@#{reflection.name}", nil)
760 669 return nil
761 670 end
  671 +
762 672 association
763 673 end
764 674
765   - define_method("set_#{association_name}_target") do |target|
  675 + define_method("set_#{reflection.name}_target") do |target|
766 676 return if target.nil?
767   - association = association_proxy_class.new(self,
768   - association_name, association_class_name,
769   - association_class_primary_key_name, options)
  677 + association = association_proxy_class.new(self, reflection)
770 678 association.target = target
771   - instance_variable_set("@#{association_name}", association)
  679 + instance_variable_set("@#{reflection.name}", association)
772 680 end
773 681 end
774 682
775   - def collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
776   - define_method(association_name) do |*params|
  683 + def collection_reader_method(reflection, association_proxy_class)
  684 + define_method(reflection.name) do |*params|
777 685 force_reload = params.first unless params.empty?
778   - association = instance_variable_get("@#{association_name}")
  686 + association = instance_variable_get("@#{reflection.name}")
  687 +
779 688 unless association.respond_to?(:loaded?)
780   - association = association_proxy_class.new(self,
781   - association_name, association_class_name,
782   - association_class_primary_key_name, options)
783   - instance_variable_set("@#{association_name}", association)
  689 + association = association_proxy_class.new(self, reflection)
  690 + instance_variable_set("@#{reflection.name}", association)
784 691 end
  692 +
785 693 association.reload if force_reload
  694 +
786 695 association
787 696 end
  697 + end
788 698
789   - define_method("#{association_name}=") do |new_value|
790   - association = instance_variable_get("@#{association_name}")
  699 + def collection_accessor_methods(reflection, association_proxy_class)
  700 + collection_reader_method(reflection, association_proxy_class)
  701 +
  702 + define_method("#{reflection.name}=") do |new_value|
  703 + association = instance_variable_get("@#{reflection.name}")
791 704 unless association.respond_to?(:loaded?)
792   - association = association_proxy_class.new(self,
793   - association_name, association_class_name,
794   - association_class_primary_key_name, options)
795   - instance_variable_set("@#{association_name}", association)
  705 + association = association_proxy_class.new(self, reflection)
  706 + instance_variable_set("@#{reflection.name}", association)
796 707 end
797 708 association.replace(new_value)
798 709 association
799 710 end
800 711
801   - define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
802   - send("#{association_name}=", association_class_name.constantize.find(new_value))
  712 + define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
  713 + send("#{reflection.name}=", reflection.class_name.constantize.find(new_value))
803 714 end
804 715 end
805 716
@@ -847,17 +758,15 @@ def add_multiple_associated_save_callbacks(association_name)
847 758 after_update(after_callback)
848 759 end
849 760
850   - def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
851   - define_method("#{constructor}_#{association_name}") do |*params|
  761 + def association_constructor_method(constructor, reflection, association_proxy_class)
  762 + define_method("#{constructor}_#{reflection.name}") do |*params|
852 763 attributees = params.first unless params.empty?
853 764 replace_existing = params[1].nil? ? true : params[1]
854   - association = instance_variable_get("@#{association_name}")
  765 + association = instance_variable_get("@#{reflection.name}")
855 766
856 767 if association.nil?
857   - association = association_proxy_class.new(self,
858   - association_name, association_class_name,
859   - association_class_primary_key_name, options)
860   - instance_variable_set("@#{association_name}", association)
  768 + association = association_proxy_class.new(self, reflection)
  769 + instance_variable_set("@#{reflection.name}", association)
861 770 end
862 771
863 772 if association_proxy_class == HasOneAssociation
@@ -910,6 +819,118 @@ def find_with_associations(options = {})
910 819 end
911 820
912 821
  822 + def configure_dependency_for_has_many(reflection)
  823 + if reflection.options[:dependent] && reflection.options[:exclusively_dependent]
  824 + raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
  825 + end
  826 +
  827 + if reflection.options[:exclusively_dependent]
  828 + reflection.options[:dependent] = :delete_all
  829 + #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
  830 + end
  831 +
  832 + # See HasManyAssociation#delete_records. Dependent associations
  833 + # delete children, otherwise foreign key is set to NULL.
  834 + case reflection.options[:dependent]
  835 + when :destroy, true
  836 + module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
  837 + when :delete_all
  838 + module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
  839 + when :nullify
  840 + module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
  841 + when nil, false
  842 + # pass
  843 + else
  844 + raise ArgumentError, 'The :dependent option expects either true, :destroy, :delete_all, or :nullify'
  845 + end
  846 + end
  847 +
  848 + def configure_dependency_for_has_one(reflection)
  849 + case reflection.options[:dependent]
  850 + when :destroy, true
  851 + module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
  852 + when :nullify
  853 + module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'"
  854 + when nil, false
  855 + # pass
  856 + else
  857 + raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
  858 + end
  859 + end
  860 +
  861 +
  862 + def add_deprecated_api_for_has_many(association_name)
  863 + deprecated_collection_count_method(association_name)
  864 + deprecated_add_association_relation(association_name)
  865 + deprecated_remove_association_relation(association_name)
  866 + deprecated_has_collection_method(association_name)
  867 + deprecated_find_in_collection_method(association_name)
  868 + deprecated_find_all_in_collection_method(association_name)
  869 + deprecated_collection_create_method(association_name)
  870 + deprecated_collection_build_method(association_name)
  871 + end
  872 +
  873 + def create_has_many_reflection(association_id, options, &extension)
  874 + options.assert_valid_keys(
  875 + :foreign_key, :class_name, :exclusively_dependent, :dependent,
  876 + :conditions, :order, :include, :finder_sql, :counter_sql,
  877 + :before_add, :after_add, :before_remove, :after_remove, :extend,
  878 + :group, :as, :through
  879 + )
  880 +
  881 + options[:extend] = create_extension_module(association_id, extension) if block_given?
  882 +
  883 + reflection = create_reflection(:has_many, association_id, options, self)
  884 + reflection.require_class
  885 +
  886 + reflection
  887 + end
  888 +
  889 + def create_has_one_reflection(association_id, options)
  890 + options.assert_valid_keys(
  891 + :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend
  892 + )
  893 +
  894 + reflection = create_reflection(:has_one, association_id, options, self)
  895 + reflection.require_class
  896 +
  897 + reflection
  898 + end
  899 +
  900 + def create_belongs_to_reflection(association_id, options)
  901 + options.assert_valid_keys(
  902 + :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
  903 + :counter_cache, :extend, :polymorphic
  904 + )
  905 +
  906 + reflection = create_reflection(:belongs_to, association_id, options, self)
  907 +
  908 + if options[:polymorphic]
  909 + reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
  910 + else
  911 + reflection.require_class
  912 + end
  913 +
  914 + reflection
  915 + end
  916 +
  917 + def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
  918 + options.assert_valid_keys(
  919 + :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :include,
  920 + :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
  921 + :before_remove, :after_remove, :extend
  922 + )
  923 +
  924 + options[:extend] = create_extension_module(association_id, extension) if block_given?
  925 +
  926 + reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
  927 + reflection.require_class
  928 +
  929 + reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
  930 +
  931 + reflection
  932 + end
  933 +
913 934 def reflect_on_included_associations(associations)
914 935 [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
915 936 end
19 activerecord/lib/active_record/associations/association_collection.rb
@@ -18,6 +18,7 @@ def reset
18 18 def <<(*records)
19 19 result = true
20 20 load_target
  21 +
21 22 @owner.transaction do
22 23 flatten_deeper(records).each do |record|
23 24 raise_on_type_mismatch(record)
@@ -28,7 +29,7 @@ def <<(*records)
28 29 end
29 30 end
30 31
31   - result and self
  32 + result && self
32 33 end
33 34
34 35 alias_method :push, :<<
@@ -60,11 +61,13 @@ def delete(*records)
60 61 # Removes all records from this association. Returns +self+ so method calls may be chained.
61 62 def clear
62 63 return self if length.zero? # forces load_target if hasn't happened already
63   - if @options[:exclusively_dependent]
  64 +
  65 + if @reflection.options[:exclusively_dependent]
64 66 destroy_all
65 67 else
66 68 delete_all
67 69 end
  70 +
68 71 self
69 72 end
70 73
@@ -124,14 +127,6 @@ def replace(other_array)
124 127 end
125 128
126 129 private
127   - def raise_on_type_mismatch(record)
128   - raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
129   - end
130   -
131   - def target_obsolete?
132   - false
133   - end
134   -
135 130 # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
136 131 def flatten_deeper(array)
137 132 array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
@@ -155,8 +150,8 @@ def callback(method, record)
155 150 end
156 151
157 152 def callbacks_for(callback_name)
158   - full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}"
159   - @owner.class.read_inheritable_attribute(full_callback_name.to_sym) or []
  153 + full_callback_name = "#{callback_name}_for_#{@reflection.name}"
  154 + @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
160 155 end
161 156
162 157 end
38 activerecord/lib/active_record/associations/association_proxy.rb
@@ -5,15 +5,9 @@ class AssociationProxy #:nodoc:
5 5 alias_method :proxy_extend, :extend
6 6 instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ }
7 7
8   - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
9   - @owner = owner
10   - @options = options
11   - @association_name = association_name
12   - @association_class = eval(association_class_name, nil, __FILE__, __LINE__)
13   - @association_class_primary_key_name = association_class_primary_key_name
14   -
15   - proxy_extend(options[:extend]) if options[:extend]
16   -
  8 + def initialize(owner, reflection)
  9 + @owner, @reflection = owner, reflection
  10 + proxy_extend(reflection.options[:extend]) if reflection.options[:extend]
17 11 reset
18 12 end
19 13
@@ -28,6 +22,11 @@ def ===(other)
28 22 other === @target
29 23 end
30 24
  25 + def reset
  26 + @target = nil
  27 + @loaded = false
  28 + end
  29 +
31 30 def reload
32 31 reset
33 32 load_target
@@ -45,14 +44,14 @@ def target
45 44 @target
46 45 end
47 46
48   - def target=(t)
49   - @target = t
50   - @loaded = true
  47 + def target=(target)
  48 + @target = target
  49 + loaded
51 50 end
52 51
53 52 protected
54 53 def dependent?
55   - @options[:dependent] || false
  54 + @reflection.options[:dependent] || false
56 55 end
57 56
58 57 def quoted_record_ids(records)
@@ -68,7 +67,7 @@ def interpolate_sql(sql, record = nil)
68 67 end
69 68
70 69 def sanitize_sql(sql)
71   - @association_class.send(:sanitize_sql, sql)
  70 + @reflection.klass.send(:sanitize_sql, sql)
72 71 end
73 72
74 73 def extract_options_from_args!(args)
@@ -84,13 +83,14 @@ def method_missing(method, *args, &block)
84 83 def load_target
85 84 if !@owner.new_record? || foreign_key_present
86 85 begin
87   - @target = find_target if not loaded?
  86 + @target = find_target if !loaded?
88 87 rescue ActiveRecord::RecordNotFound
89 88 reset
90 89 end
91 90 end
92   - @loaded = true if @target
93   - @target
  91 +
  92 + loaded if target
  93 + target
94 94 end
95 95
96 96 # Can be overwritten by associations that might have the foreign key available for an association without
@@ -100,7 +100,9 @@ def foreign_key_present
100 100 end
101 101
102 102 def raise_on_type_mismatch(record)
103   - raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
  103 + unless record.is_a?(@reflection.klass)
  104 + raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
  105 + end
104 106 end
105 107 end
106 108 end
58 activerecord/lib/active_record/associations/belongs_to_association.rb
... ... @@ -1,41 +1,27 @@
1 1 module ActiveRecord
2 2 module Associations
3 3 class BelongsToAssociation < AssociationProxy #:nodoc:
4   - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5   - super
6   - construct_sql
7   - end
8   -
9   - def reset
10   - @target = nil
11   - @loaded = false
12   - end
13   -
14 4 def create(attributes = {})
15   - record = @association_class.create(attributes)
16   - replace(record, true)
17   - record
  5 + replace(@reflection.klass.create(attributes))
18 6 end
19 7
20 8 def build(attributes = {})
21   - record = @association_class.new(attributes)
22   - replace(record, true)
23   - record
  9 + replace(@reflection.klass.new(attributes))
24 10 end
25 11
26   - def replace(obj, dont_save = false)
27   - if obj.nil?
28   - @target = @owner[@association_class_primary_key_name] = nil
  12 + def replace(record)
  13 + if record.nil?
  14 + @target = @owner[@reflection.primary_key_name] = nil
29 15 else
30   - raise_on_type_mismatch(obj) unless obj.nil?
  16 + raise_on_type_mismatch(record)
31 17
32   - @target = (AssociationProxy === obj ? obj.target : obj)
33   - @owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
  18 + @target = (AssociationProxy === record ? record.target : record)
  19 + @owner[@reflection.primary_key_name] = record.id unless record.new_record?
34 20 @updated = true
35 21 end
36   - @loaded = true
37 22
38   - return (@target.nil? ? nil : self)
  23 + loaded
  24 + record
39 25 end
40 26
41 27 def updated?
@@ -44,27 +30,15 @@ def updated?
44 30
45 31 private
46 32 def find_target
47   - if @options[:conditions]
48   - @association_class.find(
49   - @owner[@association_class_primary_key_name],
50   - :conditions => interpolate_sql(@options[:conditions]),
51   - :include => @options[:include]
52   - )
53   - else
54   - @association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include])
55   - end
  33 + @reflection.klass.find(
  34 + @owner[@reflection.primary_key_name],
  35 + :conditions => @reflection.options[:conditions] ? interpolate_sql(@reflection.options[:conditions]) : nil,
  36 + :include => @reflection.options[:include]
  37 + )
56 38 end
57 39
58 40 def foreign_key_present
59   - !@owner[@association_class_primary_key_name].nil?
60   - end
61   -
62   - def target_obsolete?
63   - @owner[@association_class_primary_key_name] != @target.id
64   - end
65   -
66   - def construct_sql
67   - @finder_sql = "#{@association_class.table_name}.#{@association_class.primary_key} = #{@owner.id}"
  41 + !@owner[@reflection.primary_key_name].nil?
68 42 end
69 43 end
70 44 end
62 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
... ... @@ -1,69 +1,49 @@
1 1 module ActiveRecord
2 2 module Associations
3   - class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
4   - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5   - @owner = owner
6   - @options = options
7   - @association_name = association_name
8   - @association_class_primary_key_name = association_class_primary_key_name
9   -
10   - proxy_extend(options[:extend]) if options[:extend]
11   -
12   - reset
13   - end
14   -
15   - def create(attributes = {})
16   - raise ActiveRecord::ActiveRecordError, "Can't create an abstract polymorphic object"
17   - end
18   -
19   - def build(attributes = {})
20   - raise ActiveRecord::ActiveRecordError, "Can't build an abstract polymorphic object"
21   - end
22   -
23   - def replace(obj, dont_save = false)
24   - if obj.nil?
25   - @target = @owner[@association_class_primary_key_name] = @owner[@options[:foreign_type]] = nil
  3 + class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
  4 + def replace(record)
  5 + if record.nil?
  6 + @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
26 7 else
27   - @target = (AssociationProxy === obj ? obj.target : obj)
  8 + @target = (AssociationProxy === record ? record.target : record)
28 9
29   - unless obj.new_record?
30   - @owner[@association_class_primary_key_name] = obj.id
31   - @owner[@options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, obj.class).to_s
  10 + unless record.new_record?
  11 + @owner[@reflection.primary_key_name] = record.id
  12 + @owner[@reflection.options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, record.class).to_s
32 13 end
33 14
34 15 @updated = true
35 16 end
36 17
37   - @loaded = true
  18 + loaded
  19 + record
  20 + end
38 21
39   - return (@target.nil? ? nil : self)
  22 + def updated?
  23 + @updated
40 24 end
41   -
  25 +
42 26 private
43 27 def find_target
44 28 return nil if association_class.nil?
45 29
46   - if @options[:conditions]
  30 + if @reflection.options[:conditions]
47 31 association_class.find(
48   - @owner[@association_class_primary_key_name],
49   - :conditions => interpolate_sql(@options[:conditions]),
50   - :include => @options[:include]
  32 + @owner[@reflection.primary_key_name],
  33 + :conditions => interpolate_sql(@reflection.options[:conditions]),
  34 + :include => @reflection.options[:include]
51 35 )
52 36 else
53   - association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include])
  37 + association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include])
54 38 end
55 39 end
56 40
57 41 def foreign_key_present
58   - !@owner[@association_class_primary_key_name].nil?
  42 + !@owner[@reflection.primary_key_name].nil?
59 43 end
60 44
61   - def target_obsolete?
62   - @owner[@association_class_primary_key_name] != @target.id
63   - end
64   -
65 45 def association_class
66   - @owner[@options[:foreign_type]] ? @owner[@options[:foreign_type]].constantize : nil
  46 + @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
67 47 end
68 48 end
69 49 end
70 activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
... ... @@ -1,20 +1,14 @@
1 1 module ActiveRecord
2 2 module Associations
3 3 class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4   - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
  4 + def initialize(owner, reflection)
5 5 super
6   -
7   - @association_foreign_key = options[:association_foreign_key] || association_class_name.foreign_key
8   - @association_table_name = options[:table_name] || @association_class.table_name
9   - @join_table = options[:join_table]
10   - @order = options[:order]
11   -
12 6 construct_sql
13 7 end
14 8
15 9 def build(attributes = {})
16 10 load_target
17   - record = @association_class.new(attributes)
  11 + record = @reflection.klass.new(attributes)
18 12 @target << record
19 13 record
20 14 end
@@ -27,7 +21,7 @@ def find(*args)
27 21 options = Base.send(:extract_options_from_args!, args)
28 22
29 23 # If using a custom finder_sql, scan the entire collection.
30   - if @options[:finder_sql]
  24 + if @reflection.options[:finder_sql]
31 25 expects_array = args.first.kind_of?(Array)
32 26 ids = args.flatten.compact.uniq
33 27
@@ -40,60 +34,64 @@ def find(*args)
40 34 end
41 35 else
42 36 conditions = "#{@finder_sql}"
  37 +
43 38 if sanitized_conditions = sanitize_sql(options[:conditions])
44 39 conditions << " AND (#{sanitized_conditions})"
45 40 end
  41 +
46 42 options[:conditions] = conditions
47 43 options[:joins] = @join_sql
48 44 options[:readonly] ||= false
49 45
50   - if options[:order] && @options[:order]
51   - options[:order] = "#{options[:order]}, #{@options[:order]}"
52   - elsif @options[:order]
53   - options[:order] = @options[:order]
  46 + if options[:order] && @reflection.options[:order]
  47 + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
  48 + elsif @reflection.options[:order]
  49 + options[:order] = @reflection.options[:order]
54 50 end
55 51
56 52 # Pass through args exactly as we received them.
57 53 args << options
58   - @association_class.find(*args)
  54 + @reflection.klass.find(*args)
59 55 end
60 56 end
61 57
62 58 def push_with_attributes(record, join_attributes = {})
63 59 raise_on_type_mismatch(record)
64 60 join_attributes.each { |key, value| record[key.to_s] = value }
  61 +
65 62 callback(:before_add, record)
66 63 insert_record(record) unless @owner.new_record?
67 64 @target << record
68 65 callback(:after_add, record)
  66 +
69 67 self
70 68 end
71 69
72 70 alias :concat_with_attributes :push_with_attributes
73 71
74 72 def size
75   - @options[:uniq] ? count_records : super
  73 + @reflection.options[:uniq] ? count_records : super
76 74 end
77 75
78 76 protected
79 77 def method_missing(method, *args, &block)
80   - if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
  78 + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
81 79 super
82 80 else
83   - @association_class.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
84   - @association_class.send(method, *args, &block)
  81 + @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
  82 + @reflection.klass.send(method, *args, &block)
85 83 end
86 84 end
87 85 end
88 86
89 87 def find_target
90   - if @options[:finder_sql]
91   - records = @association_class.find_by_sql(@finder_sql)
  88 + if @reflection.options[:finder_sql]
  89 + records = @reflection.klass.find_by_sql(@finder_sql)
92 90 else
93   - records = find(:all, :include => @options[:include])
  91 + records = find(:all, :include => @reflection.options[:include])
94 92 end
95 93
96   - @options[:uniq] ? uniq(records) : records
  94 + @reflection.options[:uniq] ? uniq(records) : records
97 95 end
98 96
99 97 def count_records
@@ -105,16 +103,16 @@ def insert_record(record)
105 103 return false unless record.save
106 104 end
107 105
108   - if @options[:insert_sql]
109   - @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
  106 + if @reflection.options[:insert_sql]
  107 + @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
110 108 else
111   - columns = @owner.connection.columns(@join_table, "#{@join_table} Columns")
  109 + columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
112 110
113 111 attributes = columns.inject({}) do |attributes, column|
114 112 case column.name