Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

Associations are picking the wrong class

#2262

UPDATE

After long discussion, this ticket is no longer valid. Some one from core just mark it as invalid please.

This is being discussed on the mailing list:

http://groups.google.com/group/r...

Problem


  class User < ActiveRecord::Base
  end

  class Admin::User < ActiveRecord::Base
  end

  class Account < ActiveRecord::Base
    belongs_to :user
  end

When I was calling Account.new.build_user, it was trying to create a new Admin::User and not an ::User, since the default class_name is just "User".

Where?

Rails 2.3.2

Why this is a problem?

Let's suppose I'm building an application (without using namespaced models at all) and then I install a plugin that has Plugin::User. My associations will start to pick Plugin::User, and this is really hard to track down.

Solution?

I propose a solution where all associations :class_name is, by default, namespaced. So:


  # have default class_name of ::User
  class Account < ActiveRecord::Base; belongs_to :user; end

  # have default class_name of ::Admin::User
  class Admin::Account < ActiveRecord::Base; belongs_to :user; end

Compatibility?

It will break apps that are relying in this wrong behavior. The good point is that the error will be raised when the application starts (can't find association error). The way it's now, takes quite sometime for you to discover that Account was trying to build a Admin::User instead of User.

Reported by José Valim · March 17th, 2009 @ 12:58 AM

State: invalid
Milestone: 2.x
Assigned to: nobody
Importance: none

Activity

  1. Hugo Barauna
  2. Andrew White
    Andrew White
    • Title changed from Associations are picking the wrong class to Associations are picking the wrong class

    I can't replicate this in a new 2.3.2 application even if I create the Admin::User model in a plugin. I can nearly replicate the opposite with Admin::Account, but what happens is also depends on the store_full_sti_class setting along with the loaded states of the User and Admin::User models.

    Historically STI models were stored with a demodulized class name in the type column and the compute_type method was used to restore the namespace when loading the constant. This had the side-effect of making namespaced associated models working as well. However now that the STI class is stored in full the type_name_with_module method just returns the default class name unmodified and reverts to the standard Rails constant loading mechanism.

    So what happens here is that when store_full_sti_class is false the type_name_with_module method prefixes the name with Admin and Admin::User is found successfully. If store_full_sti_class is true and neither model is loaded then an Admin::User instance is returned. If the Admin::User model is loaded beforehand then either an exception is raised if the User model is not loaded or an instance of User is returned if it is loaded.

    The patch does fix this specific problem but I think the real bug is actually in AS::Dependencies.load_missing_constant. Rather than raising the exception it should just return the already defined constant as the test name for it seems to indicate that it's there to prevent double loading of constants. Changing that only breaks one test and that's checking to see if the exception is raised.

    Attached is a patch that makes this change.

    March 17th, 2009 @ 03:10 PM

  3. José Valim
    José Valim

    Andrew, I understand your problem but you're solving the other way: why Admin::Account is loading "Admin::User" instead of "User".

    My problem is Account loading "Admin::User" instead of "User". I've just tried your patch here and it hasn't fixed it.

    Who is returning the wrong class is class_eval. The backtrace would be: reflections call compute_type with "User", compute_type calls type_name_with_module with "User" and returns "User", and then class_eval is called and picks up "Admin::User" instead of "User".

    I think this problem is strictly related with namespaces because both "Admin::User" and "User" are loaded before the error occurs (so can I discard this an AS::Dependencies bug?).

    "I can't replicate this in a new 2.3.2 application even if I create the Admin::User model in a plugin."

    I don't think this is an easy bug to reproduce, as you said depends who is loaded and who is not, but it's an easy one to understand why. If we say to ruby that I want ::User, it will load the right one.

    However, your discussion just pointed me that we could solve the problem making type_name_with_module always returns "::User" or "::Admin::User".

    But it doesn't change the fact that associations should default to a namespaced class, and thus, I think the patch I propose is still needed.

    March 17th, 2009 @ 03:57 PM

  4. Andrew White
    Andrew White

    José, I just don't why it should be getting Admin::User instead of User as the constant lookup should Account::User and then Object::User - why would it go looking in the Admin namespace?

    March 17th, 2009 @ 04:37 PM

  5. Andrew White
    Andrew White

    There is one way I can reproduce it and that's by defining the User constant within Account, e.g:

    
    class Account < ActiveRecord::Base
      User = Admin::User
      belongs_to :user
    end
    

    Is it possible that something similar is happening in your code?

    March 17th, 2009 @ 04:53 PM

  6. José Valim
    José Valim

    I don't have a clue, debuggers don't go inside class_eval. :) But I agree that it's totally strange.

    March 17th, 2009 @ 04:54 PM

  7. José Valim
    José Valim

    Nops.

    
    User == Admin::User #=> false
    

    Have you tried with Admin::User being a module?

    March 17th, 2009 @ 05:23 PM

  8. Andrew White
    Andrew White

    I just cannot reproduce - I've tried Admin being both a AR model and just a module. I've tried both methods in a plugin and in the main application. I've also tried Admin::User as a module but that didn't make any difference either.

    What happens if you tweak the class_eval to be parent.class_eval in compute_type? Also what's the value of store_full_sti_class

    March 17th, 2009 @ 05:37 PM

  9. José Valim
    José Valim

    I'm afraid this can't be easily reproduced. The plugin I'm using that showed me this conflict is Typus (http://github.com/fesplugas/typu...>

    In this case, Typus::User is a module and it's included in ActiveRecord::Base:

    
      module Typus
        module User
        end
      end
    
      ActiveRecord::Base.send :include, Typus::User
    

    However I've cut down their code in order to have just the lines above, and the problem couldn't be reproduced.

    Anyway, with this bug or not, association are still wrong. It should have a default class name of ::Admin::User if I'm declaring it inside ::Admin::Account, and default class name of ::User if I'm declaring it on ::Account.

    March 17th, 2009 @ 05:42 PM

  10. José Valim
    José Valim

    store_full_sti_class is true. Calling parent.class_eval in compute_type works, but in my case the parent is Object.

    However, I cannot supply a patch with parent.class_eval because this would make namespaces break, since we would be calling Admin.class_eval(Admin::User).

    On the other hand, a possible solution could be calling Object.class_eval(User), which is the same as class_eval(::User). This is likely to don't break anything because type_name_with_module is already responsible to return a namespaced class.

    So I propose: Object.class_eval in compute_type AND make associations namespaced by default, but without '::' at the beginning, which looks strange.

    Andrew, your patch is still needed?

    March 17th, 2009 @ 06:00 PM

  11. Andrew White
    Andrew White

    José, I've tracked down your problem with Typus. Typus includes Typus::User in AR::Base so all AR models have a constant User, e.g: Account::User and even User::User is defined. This is what compute_type is finding.

    Your patch works around the problem by starting the search one level up, avoiding the match with Account::User. Unfortunately your patch would break one of my apps where I have the following:

    
    class DeliveryMethod < ActiveRecord::Base
      class PriceBand < ActiveRecord::Base; end
    
      has_many :price_bands
    end
    

    If you modify your patch so that the search starts at the same level then you'll still get the same problem with Account::User matching. The solution lies with Typus having a less common name for its User module - should be a simple fix.

    As for my patch - I think it probably does still need to be applied but I'll open a new ticket.

    March 18th, 2009 @ 08:14 AM

  12. José Valim
    José Valim

    Andrew, I see you case.

    However, I still think that a simple patch should be applied.

    At least one that makes Admin::Account when belongs_to User have a default class name of Admin::User (without the '::'). This wouldn't break your application, it wouldn't fix my problem, but I think it's needed.

    March 18th, 2009 @ 08:43 AM

  13. Andrew White
    Andrew White

    José, that's essentially what happens at the moment - the string "User" is evaluated within the Admin class scope so that if Admin::User is defined it will find it first before looking elsewhere.

    March 18th, 2009 @ 09:47 AM

  14. Andrew White
    Andrew White

    Sorry, slightly wrong on the last comment. What actually happens is the following lookup:

    1. Admin::Account::User
    2. Admin::User
    3. User

    What causes this to fail at the moment is the fact that load_missing_constant is called after not finding Admin::Account::User and the exception is raised if the Admin::User has been previously loaded. This is what my patch fixes.

    March 18th, 2009 @ 09:53 AM

  15. José Valim
    José Valim

    I just updated the ticket. Someone from core might close this one soon.

    March 18th, 2009 @ 01:17 PM

  16. Frederick Cheung
    Frederick Cheung
    • State changed from new to invalid

    May 8th, 2009 @ 12:11 AM