#152 invalid
Nick Dufresne

has_many :class => "NameOfClass" constant not found issue

Reported by Nick Dufresne | February 11th, 2008 @ 02:56 PM

When I try to set up the following (in a fresh rails app):

#in user.rb
class User < DataMapper::Base
  property :name, :string
  has_many :logins, :class => "UserLogin"
end

#in user_login.rb
class UserLogin < DataMapper::Base
  property :ip_address, :string
  belongs_to :user
end

#run the following in the in the script/console
database.save(User)
database.save(UserLogin)
u = User.create(:name => "name")
l = UserLogin.new(:ip_adress => "123")
u.logins << l
l.save

#and then i quit the console and start it back up so its fresh
User.first.logins


#gives me >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>:

NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.to_sym
	from /usr/local/lib/ruby/gems/1.8/gems/datamapper-0.3.0/lib/data_mapper/associations/has_many_association.rb:267:in `fetch_sets'
	from /usr/local/lib/ruby/gems/1.8/gems/datamapper-0.3.0/lib/data_mapper/associations/has_many_association.rb:186:in `items'
	from /usr/local/lib/ruby/gems/1.8/gems/datamapper-0.3.0/lib/data_mapper/associations/has_many_association.rb:105:in `each'
	from /usr/local/lib/ruby/gems/1.8/gems/datamapper-0.3.0/lib/data_mapper/associations/has_many_association.rb:210:in `entries'
	from /usr/local/lib/ruby/gems/1.8/gems/datamapper-0.3.0/lib/data_mapper/associations/has_many_association.rb:210:in `inspect'
	from /usr/local/lib/ruby/1.8/irb.rb:298:in `output_value'
	from /usr/local/lib/ruby/1.8/irb.rb:151:in `eval_input'
	from /usr/local/lib/ruby/1.8/irb.rb:259:in `signal_status'
	from /usr/local/lib/ruby/1.8/irb.rb:147:in `eval_input'
	from /usr/local/lib/ruby/1.8/irb.rb:146:in `eval_input'
	from /usr/local/lib/ruby/1.8/irb.rb:70:in `start'
	from /usr/local/lib/ruby/1.8/irb.rb:69:in `catch'
	from /usr/local/lib/ruby/1.8/irb.rb:69:in `start'
	from /usr/local/bin/irb:13

the nil.to_sym is the call to foreign_key_column.to_sym

I tracked this down to the fact that the has_many association initialization in HasNAssociation calls

Persistence::dependencies.add(associated_constant_name) do |klass|
  #a bunch of important code that loads the foreign_key_column
end

and the block won't be evaluated because the "UserLogin" constant is not defined. But it seems like if i knock out the above block and just run the contained code without the context provided by Persitence::dependecies... it works fine.

The other workaround that I can do is either require "user_login" in user.rb or do has_many :logins, :class => UserLogin (no quotes) and the constant is evaluated (and therefore defined) before the has_many call initializes.

I'm not sure if this is expected behavior ... but with ActiveRecord they use Inflector.constantize to load the constants rather than the datamapper Object.get_const and Object.const_defined? and which causes the above block to bail out in the initialize of HasNAssociation.

Comments and changes to this ticket

  • Michael Boutros

    Michael Boutros February 11th, 2008 @ 04:51 PM

    Hello Nick! First of all, you a small but important typo in your example code - when you call UserLogin.create you mean :ip_address, not :ip_adress.

    When I put all of your code into one file and run it, it works fine, but it appears that it isn't working in your case because of file load order. However, when you add dependencies the blocks are never called until later on, so something isn't working right.

    Can you please post exactly what line in your console that you got the error? The trace says line 13, but since there are only eight lines I assume you put some other stuff there before.

    Thanks a lot :)

  • Nick Dufresne

    Nick Dufresne February 11th, 2008 @ 06:59 PM

    Thanks for working through the issue. Sorry about the type on ip_address. So ... the error i get occurs on the first line of the console once i fire it up again. To clarify.

    • I create a new rails app (defaults to sqlite3 database so i don't need to configure anything)
    • I create two models (user.rb and user_login.rb) with the above code.
    • then i run the following in ./script/console
    >> database.save(User)
    >> database.save(UserLogin)
    >> u = User.create(:name => "name")
    >> l = UserLogin.new(:ip_adress => "123")
    >> u.logins << l
    >> l.save
    >> quit
    

    I quit and restart the ./script/console (so that UserLogin is no longer loaded) and run the following single line:

    >> User.first.logins
    

    and thats what gives the error. It still says irb:13 (which i assume is the eval of the console line in irb.rb. But i haven't looked.

    It looks to me that the add dependencies block does execute right away, but it boils down to line 15 of dependency_queue.rb that calls:

    if Object.const_defined?(class_name)  [line 15]
      klass = Object.const_get(class_name)
    
      callbacks.each do |b|
        b.call(klass)
      end
              
      callbacks.clear
    end
    

    where Object.const_defined?(class_name) is false for "UserLogin" when the has_many :logins, :class=>"UserLogin" executes it, and so the block that is passed in that sets the foreign_key_column is never executed.

    I am able to get the code to work all in one file as well, but its the issue that the Constant isn't auto loading (which im used to from ActiveRecord). I was expecting the class to be automatically loaded, but im not sure if this is what i should be expecting.

    Also -- as i mentioned above -- if i drop the add dependencies call around the block and just have the block execute in the initialize of HasNAssociation the constant does load from the Kernel.get_const(association_constant_name) call in association_constant method.

    Let me know if this doesn't clarify the situation. Thanks

  • Nick Dufresne

    Nick Dufresne February 11th, 2008 @ 07:02 PM

    oops ... i copied the above console lines and made the same typo .... I redid the whole test run above to check that it was correct, except i ran this in the console:

    >> database.save(User)
    >> database.save(UserLogin)
    >> u = User.create(:name => "name")
    >> l = UserLogin.new(:ip_address => "123")
    >> u.logins << l
    >> l.save
    >> quit
    

    and then (on restart)

    >> User.first.logins
    
  • Michael Boutros

    Michael Boutros February 11th, 2008 @ 09:43 PM

    You're right, they do get called right after they are added to the queue. For now you can just take off the quotes and it'll work (as you said), but that's only a temporary fix. I'm going to look at it tomorrow, but I'm sure by that time someone will have fixed it.

  • Michael Boutros

    Michael Boutros February 12th, 2008 @ 07:39 PM

    I've been digging through and looking for what exactly happens. It goes something like this:

    1. When the association is added, the dependencies are also added.

    2. DependencyQueue#resolve! is called, and if the constant is found, then all of the callbacks are called. However, if the constant is not found, nothing happens - until next resolve! is run, where the constant will be looked for again.

    3. When it eventually does find the constant, the callbacks are run.

    I can't seem to replicate your results outside of Rails, so tonight I'll try to replicate them in Rails. Until then, does whatever you're trying to do still work in the controllers and helpers? Or is the problem everywhere, not just the console?

  • Nick Dufresne

    Nick Dufresne February 12th, 2008 @ 08:42 PM

    I ran into the problem when I was trying to iterate through the list of of user.logins in a view/controller situation. I rarely use the console except to debug and with DataMapper to sync my tables and stuff. I would try something like this in a view:

    <% for login in @user.logins %>
      <p><%= login.ip_address %></p>
    <% end %>
    

    When i ran into the problem i tried to narrow things down in the console and found the same result.

    In order to generate the problem i think you need a situation similar to rails where you might have models defined in separate files and the framework doesn't automatically load everything right away -- so some sort of "magic" has to happen behind the scenes to load the constants. It seems (to me) that Rail's and Datamapper's "magic" code is in the String.constantize and Kernel.get_const respectively. Unfortunatley the dependencies block doesn't ever execute if the constant isn't found so the magic never takes place. It seems like the dependencies block is useful in some sense, but in the case of associations shouldn't we expect the constant to be defined and therefore throw an error if its not found rather than silently fail ... or try to load it first and then throw and error if that fails? I believe thats what Inflector.constantize tries to do. Thanks for checking this out so thoroughly. Let me know if you can't recreate this in rails.

  • Michael Boutros

    Michael Boutros February 13th, 2008 @ 05:14 PM

    Nick, I haven't died, and I promise that I will look into this more and try to come up with why it's happening - I'm just busy at the moment. Hang in there :)

  • Nick Dufresne

    Nick Dufresne February 13th, 2008 @ 04:59 PM

    no rush ... the workaround of passing the class is fine for now. Plus i can always hack away at my local copy of data_mapper to test things out for now. Just posted here in case someone else has a similar issue. Thanks

  • asceth

    asceth February 14th, 2008 @ 12:39 AM

    What are the benefits of keeping support for passing a String to :class ?

  • Adam French

    Adam French February 14th, 2008 @ 02:29 PM

    • State changed from “new” to “open”

    passing :class => Class isn't a hack or workaround, it's a feature ;-)

    Doing string inflection is just an extra expense that we shouldn't have to pay when DM can easily have the klass passed into it.

    so, I'm not particularly for 'fixing' this issue.

    I would, however, propose that we do the hacking to make strings work in an include (like the rumored AR Compatibility module).

  • Michael Boutros

    Michael Boutros February 14th, 2008 @ 02:34 PM

    I still, at the moment, have no idea why this isn't working. I thought Rails automatically loaded all files?

  • Dan Kubb (dkubb)
  • Sam Smoot

    Sam Smoot April 28th, 2008 @ 01:01 PM

    • State changed from “open” to “invalid”

    Closing this out since it doesn't apply to 0.9.0. I'd like to keep the tickets focused on the new, specific, small, and with specs attached if possible.

    Feel free to reopen with comments if you have a specific request.

    BTW, 0.9.0 just uses :class_name => "Foo::Bar". :class not supported since trying to get away from multiple ways to accomplish stuff, and it can fail on circular dependencies if you're not really careful with it.

Please Sign in or create a free account to add a new ticket.

With your very own profile, you can contribute to projects, track your activity, watch tickets, receive and update tickets through your email and much more.

New-ticket Create new ticket

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

People watching this ticket

Pages