Skip to content
This repository

association methods are now generated in modules #3636

Merged
merged 6 commits into from over 2 years ago

9 participants

Josh Susser Justin Ko Maxim Chernyak aka hakunin C o u r t e n a y Piotr Solnica Jon Leighton José Valim James Edward Gray II Emmanuel Gomez
Josh Susser

Instead of generating association methods directly in the model
class, they are generated in an anonymous module which
is then included in the model class. There is one such module
for each association. The only subtlety is that the
generated_attributes_methods module (from ActiveModel) must
be forced to be included before association methods are created
so that attribute methods will not shadow association methods.

The advantage to this approach is that it is now possible to override
a generated method and still call the original generated method via super.

This change is relatively straightforward from a code organization
perspective, but it has a high potential for causing grief in applications
that make assumptions about how association methods are generated
or ordering of method definitions. Therefore it probably needs some
stress-testing in real applications to see how it goes.

Justin Ko

Oh man this is much cleaner. Nice stuff.

Maxim Chernyak aka hakunin

I agree, this always felt awkward from practical perspective. Although depends how you look at it. attr_accessor (and the like) don't include modules either. Wouldn't it be less surprising to let associations behave as accessor declarations? Or why not go further and make accessor methods into modules as well? Just curious where would the line be.

Josh Susser

@maxim: Surprise! Attribute accessor methods are already generated in their own module. I wasn't aware of that either until I dug into the code for this change. With this change, association methods will be consistent with that.

C o u r t e n a y

Any time you can get rid of code that looks like class_eval <<-RUBY, __FILE__, __LINE__ + 1 you're having a good day.

Maxim Chernyak aka hakunin

@joshsusser Weird, doesn't seem to work for me. Unless it's something on the master?

(Trip is an existing ActiveRecord model)

Loading development environment (Rails 3.1.1)
>> class Trip
>> attr_accessor :foo
>> def foo
>> super
>> end
>> end
=> nil
>> Trip.new.foo
NoMethodError: super: no superclass method `foo' for #<Trip:0x007ff54ef8c8c0>
Josh Susser

@maxim: Maybe attr_accessor doesn't work the same way as accessor methods for actual attributes in the database. I'd have to think about whether it makes sense to modularize attr_accessor methods too.

Maxim Chernyak aka hakunin

@joshsusser not arguing against it though, just curious. It's "simple vs easy" type of thing.

Piotr Solnica

@joshsusser It's better to create one such module for a model and then define new readers/writers every time an association is created and include it again. This way you will avoid having a lot of anonymous modules.

Jon Leighton
Owner

Thanks for doing this - have been meaning to for a while and never got around to it. Will review within the next week.

Josh Susser

Since @solnic and others have asked... the reason for using a module for each association instead of one module for all of them is that it was the simplest way to code it, and there doesn't seem to be any cost for having lots of modules except for a bit of memory used. I believe all current Ruby implementations optimize method lookup so that having a deep inheritance hierarchy costs little to nothing at runtime.

Piotr Solnica

@joshsusser the reason I pointed this out was that we did this optimization in DataMapper project and as far as I remember it had a noticeable impact on specs performance. It probably doesn't really matter until you create A LOT of objects. Nevertheless it's a fantastic improvement. I hope it'll get merged in soon.

José Valim
Owner
James Edward Gray II

It's worth noting that things like attr_accessor and this pull request are different use cases.

For example, if you wanted to use attr_reader (a cousin of attr_accessor, which are both provided by Ruby, not Rails) it is because you are defining a class and want to take a shortcut instead of writing out a rote method definition. This shortcut works when you just want to access the instance variable. If you wanted to do more, say cast the value on the way out, you would just skip the shortcut and define the method normally. It doesn't make sense to have Ruby define the wrong version and you override it in the same class definition.

Now, the code Josh changed is a different scenario. Rails is giving you ways to describe your schema. It will then use that description to build the needed interface methods. It does make perfect sense that you could then need to override those, adding additional behavior to the raw database access Rails is providing.

That's why this change from Josh is such a great idea. I'm all for it.

José Valim
Owner
Josh Susser

Doing an optimization to bundle all accessor methods in a single module wouldn't be too terrible, but I'd like to prove that this change won't cause problems first before investing that effort. I already have some ideas how to do that optimization, shouldn't be too hard.

Maxim Chernyak aka hakunin

@JEG2 I like this perspective. No more concerns here. : )

Jon Leighton
Owner

@joshsusser

I agree with what others have said about using only one module. Ideally we'd have a single module that we can just throw methods into wherever in Active Record they get generated. As you said, the attributes stuff already generates methods in its own module. There's also stuff like nested attributes, composed_of, etc, that all generate methods, and would all benefit from this sort of change in the future.

So I wonder if you could incorporate into your patch a change that defines a method on ActiveRecord::Base which generates/includes this module (like the generated_attribute_methods method in ActiveModel::AttributeMethods). Then, we could redefine generated_attribute_methods in AR to reference this module also.

Perhaps you can generate this module in such a way that it is a named constant, so that it's obvious what it is when people inspect Model.ancestors.

Please also add a CHANGELOG entry for this.

It would also be great to add to the docs to indicate to users that they can extend the generated methods using super.

activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
... ...
@@ -15,14 +15,10 @@ module ActiveRecord::Associations::Builder
15 15
 
16 16
       def define_destroy_hook
17 17
         name = self.name
18  
-        model.send(:include, Module.new {
19  
-          class_eval <<-RUBY, __FILE__, __LINE__ + 1
20  
-            def destroy_associations
21  
-              association(#{name.to_sym.inspect}).delete_all
22  
-              super
23  
-            end
24  
-          RUBY
25  
-        })
  18
+        mixin.send(:define_method, :destroy_associations) do
  19
+          association(name).delete_all
  20
+          super()
3
Jon Leighton Owner

no need for parens here

Actually, the parens on super() are required to make Ruby 1.9 happy. You can't do super with implicit args in a method created in define_method like that. Ruby 1.9 gets confused about arity - is it the arity of the block or the containing method?

Jon Leighton Owner

successfully out-geeked ;)

(I created a test to play with this if anyone is reading and interested: https://gist.github.com/1368727)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jon Leighton
Owner

Another thing: please could you add an explicit test that extending one of these methods via super actually works.

Josh Susser

@jonleighton: Good feedback. I think putting all the association methods in one module is a good way to go. If we're comfortable with this level of change and the potential for disruption I can go ahead and work on that. But I do not think it's a good idea to combine all AR generated methods into a single module for a model class. That will force a particular order for generating methods that may have name clashes. The example in the Rails test fixtures is computers(:workstation).developer - "developer" is the name of both the foreign key field and the belongs_to association. The current order in which those methods get generated breaks the association unless the association methods model/class inherits from the attribute methods module. I think it's better to have a module for attribute methods and a different module for association methods. Offhand I'm not sure what to do for the other kinds of generated methods you mentioned - they might work in one of those modules, or might want to go in another. But I think some kind of precedence ordering should be used, not just the temporal order of method generation.

Jon Leighton
Owner

Ok, yes, I agree with you about having the attributes module higher up the ancestor chain than the associations module, because we always want attributes to get overridden by other stuff if necessary.

But I think it's okay in theory to use the same module for associations, nested attributes, composed of etc. If those things are clashing with each other I actively want us to be redefining methods in such a way as to trigger a warning in ruby. So maybe you can name you single-module thinger in a generic way in anticipation of the flood of pull requests we are imminently going to receive for those other things.

added some commits November 14, 2011
Josh Susser association methods are now generated in modules
Instead of generating association methods directly in the model
class, they are generated in an anonymous module which
is then included in the model class. There is one such module
for each association. The only subtlety is that the
generated_attributes_methods module (from ActiveModel) must
be forced to be included before association methods are created
so that attribute methods will not shadow association methods.
7cba6a3
Josh Susser add test for super-ing to association methods 9cdf33a
Josh Susser

I took a crack at generating all association methods in a single module per model class. It was pretty easy, right up until I ran into the has_and_belongs_to_many destroy_associations hook. HABTM associations have been generating a module per association for quite a while. They do it so that multiple HABTMs in a class can all have their destroy_associations hooks run in series. Note that all those methods have the same name - they are all in separate modules that are ancestors of the same class, and each one calls the next using super. So to change this patch to use one module per class, I'll have to come up with another way to handle this case. Either that or we can punt on this one case - since HABTM isn't used very often, maybe multiple modules won't offend anyone.

Also, I wanted to respond to a few comments above.

1) Programmatically creating modules is not the same as loading a bunch of helper modules from the file system so there shouldn't be any significant impact to startup time. I haven't noticed any running tests.

2) As I mentioned before, having a bunch of modules should have negligible impact on performance. All current Ruby VMs optimize method dispatch so inheritance depth doesn't matter, except maybe while you have cold caches the first time you run a piece of code. That might have a tiny impact on how long tests take to run, but I doubt it would be statistically significant. If you think you are measuring a performance impact from number of modules, you're probably noticing something else.

Anyway, I did add an explicit test that shows a model method using super to call the association method.

Emmanuel Gomez

@joshsusser

Nice work!

You might consider making the module 'smart', ie., don't just call Module#define_method on it over and over, give the Module itself methods that define the desired methods in the including (target) class/module. I've just started experimenting with this technique.

It's a bit of a brain-twist at first: you define Module instance methods which in turn dynamically define instance methods in the including class, but it works great. Most importantly, it lets you collect the dynamic method definition logic in one place and name it with intention-revealing selectors :D.

Also worth noting: if you define #inspect on the module, you'll get meaningful output from FooModel.ancestors (more meaningful than #<Module 0x00000>).

Josh Susser

@emmanuel: Cleaning that up is next on my list. I thought about enhancing the module behavior to show the association name. I at least want to make define_method public so there aren't all those sends making the code ugly.

Emmanuel Gomez

Making Module#define_method public would help the code read a bit more clearly.

That said, I think it's better still to treat the Module (@mixin) as an object with its own encapsulation. In this case, its responsibility is to define methods according to certain params (ie., Association::Builder#define_*).

In any case, a win for ActiveRecord.

Jon Leighton
Owner

I think just leave the destroy_associations hook as it is for now, we can fix that up at a later date.

added some commits November 27, 2011
Josh Susser use GeneratedFeatureMethods module for associations 61bcc31
Josh Susser changelog & docs for GeneratedFeatureMethods 10834e9
Josh Susser avoid warnings
This change uses Module.redefine_method as defined in ActiveSupport.
Making Module.define_method public would be as clean in the code, and
would also emit warnings when redefining an association. That is pretty
messy given current tests, so I'm leaving it for someone else to decide
what approach is better.
124c97f
Josh Susser

I think this change is ready to go. One named module for all associations, docs, and changelog. There is still a separate module for each habtm destroy_associations method, as we agreed. There's a discussion to be had about whether to emit warnings when redefining an association, but that's probably a different change than this one. For now, I fell back to using Module.redefine_method since it is consistent with the old way of defining association methods.

José Valim josevalim commented on the diff November 27, 2011
activerecord/lib/active_record/base.rb
@@ -450,6 +450,20 @@ module ActiveRecord #:nodoc:
450 450
                :having, :create_with, :uniq, :to => :scoped
451 451
       delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
452 452
 
  453
+      def inherited(child_class) #:nodoc:
  454
+        # force attribute methods to be higher in inheritance hierarchy than other generated methods
  455
+        child_class.generated_attribute_methods
  456
+        child_class.generated_feature_methods
  457
+        super
  458
+      end
  459
+
  460
+      def generated_feature_methods
3
José Valim Owner

Do we set this constant in order to have a readable output on Model.ancestors?

José Valim Owner

If I am not wrong, we were used to do the same thing for generated_attribute_methods and it caused memory leaks in development. I will try to find the relevant commits.

Yes, the module gets named MyModel::GeneratedFeatureMethods. But ActiveModel doesn't name the attributes module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
José Valim
Owner

Awesome, I took a quick look, everything looks great to me!

activerecord/test/cases/associations_test.rb
@@ -273,3 +274,24 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
273 274
     )
274 275
   end
275 276
 end
  277
+
  278
+class GeneratedMethodsTest < ActiveRecord::TestCase
  279
+  fixtures :developers, :computers, :posts, :comments
  280
+  def test_association_methods_override_attribute_methods_of_same_name
  281
+    assert_equal(developers(:david), computers(:workstation).developer)
  282
+    # this next line will fail if the attribute methods module is generated lazily
  283
+    # after the association methods module is generated
  284
+    assert_equal(developers(:david), computers(:workstation).developer)
  285
+    assert_equal(developers(:david).id, computers(:workstation)[:developer])
  286
+  end
  287
+
  288
+  def test_model_method_overrides_association_method
  289
+    Post.class_eval <<-"RUBY"
2
Jon Leighton Owner

Please could you define this stuff directly in models/post.rb? I know it makes the test easier to read by defining it right here, but it's altering global state in a single test which is something we should be avoiding generally.

Other than that looks good to merge to me.

Makes sense to me. Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jon Leighton jonleighton merged commit 2169603 into from November 29, 2011
Jon Leighton jonleighton closed this November 29, 2011
Jon Leighton
Owner

merged! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 6 unique commits by 1 author.

Nov 15, 2011
Josh Susser association methods are now generated in modules
Instead of generating association methods directly in the model
class, they are generated in an anonymous module which
is then included in the model class. There is one such module
for each association. The only subtlety is that the
generated_attributes_methods module (from ActiveModel) must
be forced to be included before association methods are created
so that attribute methods will not shadow association methods.
7cba6a3
Josh Susser add test for super-ing to association methods 9cdf33a
Nov 27, 2011
Josh Susser use GeneratedFeatureMethods module for associations 61bcc31
Josh Susser changelog & docs for GeneratedFeatureMethods 10834e9
Josh Susser avoid warnings
This change uses Module.redefine_method as defined in ActiveSupport.
Making Module.define_method public would be as clean in the code, and
would also emit warnings when redefining an association. That is pretty
messy given current tests, so I'm leaving it for someone else to decide
what approach is better.
124c97f
Nov 29, 2011
Josh Susser don't change class definition in test case c347b3c
This page is out of date. Refresh to see the latest.
6  activerecord/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,11 @@
1 1
 ## Rails 3.2.0 (unreleased) ##
2 2
 
  3
+*   Generated association methods are created within a separate module to allow overriding and
  4
+    composition using `super`. For a class named `MyModel`, the module is named
  5
+    `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after
  6
+    the `generated_attributes_methods` module defined in ActiveModel, so association methods
  7
+    override attribute methods of the same name. *Josh Susser*
  8
+
3 9
 *   Implemented ActiveRecord::Relation#explain. *fxn*
4 10
 
5 11
 *   Add ActiveRecord::Relation#uniq for generating unique queries.
20  activerecord/lib/active_record/associations.rb
@@ -196,6 +196,26 @@ def association_instance_set(name, association)
196 196
     # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
197 197
     #   <tt>Project#categories.delete(category1)</tt>
198 198
     #
  199
+    # === Overriding generated methods
  200
+    #
  201
+    # Association methods are generated in a module that is included into the model class,
  202
+    # which allows you to easily override with your own methods and call the original
  203
+    # generated method with +super+. For example:
  204
+    #
  205
+    #   class Car < ActiveRecord::Base
  206
+    #     belongs_to :owner
  207
+    #     belongs_to :old_owner
  208
+    #     def owner=(new_owner)
  209
+    #       self.old_owner = self.owner
  210
+    #       super
  211
+    #     end
  212
+    #   end
  213
+    #
  214
+    # If your model class is <tt>Project</tt>, the module is
  215
+    # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
  216
+    # is included in the model class immediately after the (anonymous) generated attributes methods
  217
+    # module, meaning an association will override the methods for an attribute with the same name.
  218
+    #
199 219
     # === A word of warning
200 220
     #
201 221
     # Don't create associations that have the same name as instance methods of
10  activerecord/lib/active_record/associations/builder/association.rb
@@ -16,6 +16,10 @@ def initialize(model, name, options)
16 16
       @model, @name, @options = model, name, options
17 17
     end
18 18
 
  19
+    def mixin
  20
+      @model.generated_feature_methods
  21
+    end
  22
+
19 23
     def build
20 24
       validate_options
21 25
       reflection = model.create_reflection(self.class.macro, name, options, model)
@@ -36,16 +40,14 @@ def define_accessors
36 40
 
37 41
       def define_readers
38 42
         name = self.name
39  
-
40  
-        model.redefine_method(name) do |*params|
  43
+        mixin.redefine_method(name) do |*params|
41 44
           association(name).reader(*params)
42 45
         end
43 46
       end
44 47
 
45 48
       def define_writers
46 49
         name = self.name
47  
-
48  
-        model.redefine_method("#{name}=") do |value|
  50
+        mixin.redefine_method("#{name}=") do |value|
49 51
           association(name).writer(value)
50 52
         end
51 53
       end
6  activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -25,14 +25,14 @@ def add_counter_cache_callbacks(reflection)
25 25
         name         = self.name
26 26
 
27 27
         method_name = "belongs_to_counter_cache_after_create_for_#{name}"
28  
-        model.redefine_method(method_name) do
  28
+        mixin.redefine_method(method_name) do
29 29
           record = send(name)
30 30
           record.class.increment_counter(cache_column, record.id) unless record.nil?
31 31
         end
32 32
         model.after_create(method_name)
33 33
 
34 34
         method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
35  
-        model.redefine_method(method_name) do
  35
+        mixin.redefine_method(method_name) do
36 36
           record = send(name)
37 37
           record.class.decrement_counter(cache_column, record.id) unless record.nil?
38 38
         end
@@ -48,7 +48,7 @@ def add_touch_callbacks(reflection)
48 48
         method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
49 49
         touch       = options[:touch]
50 50
 
51  
-        model.redefine_method(method_name) do
  51
+        mixin.redefine_method(method_name) do
52 52
           record = send(name)
53 53
 
54 54
           unless record.nil?
4  activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -58,7 +58,7 @@ def define_readers
58 58
         super
59 59
 
60 60
         name = self.name
61  
-        model.redefine_method("#{name.to_s.singularize}_ids") do
  61
+        mixin.redefine_method("#{name.to_s.singularize}_ids") do
62 62
           association(name).ids_reader
63 63
         end
64 64
       end
@@ -67,7 +67,7 @@ def define_writers
67 67
         super
68 68
 
69 69
         name = self.name
70  
-        model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
  70
+        mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
71 71
           association(name).ids_writer(ids)
72 72
         end
73 73
       end
6  activerecord/lib/active_record/associations/builder/has_many.rb
@@ -28,7 +28,7 @@ def configure_dependency
28 28
 
29 29
       def define_destroy_dependency_method
30 30
         name = self.name
31  
-        model.send(:define_method, dependency_method_name) do
  31
+        mixin.redefine_method(dependency_method_name) do
32 32
           send(name).each do |o|
33 33
             # No point in executing the counter update since we're going to destroy the parent anyway
34 34
             counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
@@ -45,7 +45,7 @@ class << o
45 45
 
46 46
       def define_delete_all_dependency_method
47 47
         name = self.name
48  
-        model.send(:define_method, dependency_method_name) do
  48
+        mixin.redefine_method(dependency_method_name) do
49 49
           send(name).delete_all
50 50
         end
51 51
       end
@@ -53,7 +53,7 @@ def define_delete_all_dependency_method
53 53
 
54 54
       def define_restrict_dependency_method
55 55
         name = self.name
56  
-        model.send(:define_method, dependency_method_name) do
  56
+        mixin.redefine_method(dependency_method_name) do
57 57
           raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
58 58
         end
59 59
       end
11  activerecord/lib/active_record/associations/builder/has_one.rb
@@ -44,18 +44,17 @@ def dependency_method_name
44 44
       end
45 45
 
46 46
       def define_destroy_dependency_method
47  
-        model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
48  
-          def #{dependency_method_name}
49  
-            association(#{name.to_sym.inspect}).delete
50  
-          end
51  
-        eoruby
  47
+        name = self.name
  48
+        mixin.redefine_method(dependency_method_name) do
  49
+          association(name).delete
  50
+        end
52 51
       end
53 52
       alias :define_delete_dependency_method :define_destroy_dependency_method
54 53
       alias :define_nullify_dependency_method :define_destroy_dependency_method
55 54
 
56 55
       def define_restrict_dependency_method
57 56
         name = self.name
58  
-        model.redefine_method(dependency_method_name) do
  57
+        mixin.redefine_method(dependency_method_name) do
59 58
           raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
60 59
         end
61 60
       end
6  activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -16,15 +16,15 @@ def define_accessors
16 16
       def define_constructors
17 17
         name = self.name
18 18
 
19  
-        model.redefine_method("build_#{name}") do |*params, &block|
  19
+        mixin.redefine_method("build_#{name}") do |*params, &block|
20 20
           association(name).build(*params, &block)
21 21
         end
22 22
 
23  
-        model.redefine_method("create_#{name}") do |*params, &block|
  23
+        mixin.redefine_method("create_#{name}") do |*params, &block|
24 24
           association(name).create(*params, &block)
25 25
         end
26 26
 
27  
-        model.redefine_method("create_#{name}!") do |*params, &block|
  27
+        mixin.redefine_method("create_#{name}!") do |*params, &block|
28 28
           association(name).create!(*params, &block)
29 29
         end
30 30
       end
14  activerecord/lib/active_record/base.rb
@@ -450,6 +450,20 @@ class << self # Class methods
450 450
                :having, :create_with, :uniq, :to => :scoped
451 451
       delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
452 452
 
  453
+      def inherited(child_class) #:nodoc:
  454
+        # force attribute methods to be higher in inheritance hierarchy than other generated methods
  455
+        child_class.generated_attribute_methods
  456
+        child_class.generated_feature_methods
  457
+        super
  458
+      end
  459
+
  460
+      def generated_feature_methods
  461
+        unless const_defined?(:GeneratedFeatureMethods, false)
  462
+          include const_set(:GeneratedFeatureMethods, Module.new)
  463
+        end
  464
+        const_get(:GeneratedFeatureMethods)
  465
+      end
  466
+
453 467
       # Executes a custom SQL query against your database and returns all the results. The results will
454 468
       # be returned as an array with columns requested encapsulated as attributes of the model you call
455 469
       # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
22  activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -77,7 +77,7 @@ class DeveloperWithCounterSQL < ActiveRecord::Base
77 77
 
78 78
 class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
79 79
   fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
80  
-           :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
  80
+           :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
81 81
 
82 82
   def setup_data_for_habtm_case
83 83
     ActiveRecord::Base.connection.execute('delete from countries_treaties')
@@ -445,6 +445,26 @@ def test_destroy_all
445 445
     assert david.projects(true).empty?
446 446
   end
447 447
 
  448
+  def test_destroy_associations_destroys_multiple_associations
  449
+    george = parrots(:george)
  450
+    assert !george.pirates.empty?
  451
+    assert !george.treasures.empty?
  452
+
  453
+    assert_no_difference "Pirate.count" do
  454
+      assert_no_difference "Treasure.count" do
  455
+        george.destroy_associations
  456
+      end
  457
+    end
  458
+
  459
+    join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
  460
+    assert join_records.empty?
  461
+    assert george.pirates(true).empty?
  462
+
  463
+    join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
  464
+    assert join_records.empty?
  465
+    assert george.treasures(true).empty?
  466
+  end
  467
+
448 468
   def test_deprecated_push_with_attributes_was_removed
449 469
     jamis = developers(:jamis)
450 470
     assert_raise(NoMethodError) do
16  activerecord/test/cases/associations_test.rb
... ...
@@ -1,4 +1,5 @@
1 1
 require "cases/helper"
  2
+require 'models/computer'
2 3
 require 'models/developer'
3 4
 require 'models/project'
4 5
 require 'models/company'
@@ -273,3 +274,18 @@ def test_has_one_association_redefinition_reflections_should_differ_and_not_inhe
273 274
     )
274 275
   end
275 276
 end
  277
+
  278
+class GeneratedMethodsTest < ActiveRecord::TestCase
  279
+  fixtures :developers, :computers, :posts, :comments
  280
+  def test_association_methods_override_attribute_methods_of_same_name
  281
+    assert_equal(developers(:david), computers(:workstation).developer)
  282
+    # this next line will fail if the attribute methods module is generated lazily
  283
+    # after the association methods module is generated
  284
+    assert_equal(developers(:david), computers(:workstation).developer)
  285
+    assert_equal(developers(:david).id, computers(:workstation)[:developer])
  286
+  end
  287
+
  288
+  def test_model_method_overrides_association_method
  289
+    assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
  290
+  end
  291
+end
9  activerecord/test/cases/base_test.rb
@@ -69,6 +69,15 @@ def setup
69 69
 class BasicsTest < ActiveRecord::TestCase
70 70
   fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
71 71
 
  72
+  def test_generated_methods_modules
  73
+    modules = Computer.ancestors
  74
+    assert modules.include?(Computer::GeneratedFeatureMethods)
  75
+    assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
  76
+    assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
  77
+           "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
  78
+    assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
  79
+  end
  80
+
72 81
   def test_column_names_are_escaped
73 82
     conn      = ActiveRecord::Base.connection
74 83
     classname = conn.class.name[/[^:]*$/]
1  activerecord/test/models/author.rb
@@ -128,7 +128,6 @@ def testing_proxy_target
128 128
   belongs_to :author_address,       :dependent => :destroy
129 129
   belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
130 130
 
131  
-  has_many :post_categories, :through => :posts, :source => :categories
132 131
   has_many :category_post_comments, :through => :categories, :source => :post_comments
133 132
 
134 133
   has_many :misc_posts, :class_name => 'Post',
4  activerecord/test/models/post.rb
@@ -24,6 +24,10 @@ def greeting
24 24
   belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts
25 25
   belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address
26 26
 
  27
+  def first_comment
  28
+    super.body
  29
+  end
  30
+  has_one :first_comment, :class_name => 'Comment', :order => 'id ASC'
27 31
   has_one :last_comment, :class_name => 'Comment', :order => 'id desc'
28 32
 
29 33
   scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} }
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.