ActiveRecord::MissingAttributeError after update to rails v 2.3.4 #633

Closed
lighthouse-import opened this Issue May 16, 2011 · 35 comments

Projects

None yet

1 participant

@lighthouse-import

Imported from Lighthouse. Original ticket at: http://rails.lighthouseapp.com/projects/8994/tickets/3165
Created by Łukasz Bandzarewicz - 2009-09-08 14:23:02 UTC

After update Rails to 2.3.4 version I get following exception:

  1) Error:
test_tagging(BacklogItemTest):
ActiveRecord::MissingAttributeError: missing attribute: domain_id
    app/models/mixins/domain_checks.rb:54:in `ensure_same_domain'
    app/models/mixins/domain_checks.rb:35:in `after_find'
    vendor/rails/activerecord/lib/active_record/callbacks.rb:347:in `send'
    vendor/rails/activerecord/lib/active_record/callbacks.rb:347:in `callback'
    vendor/rails/activerecord/lib/active_record/base.rb:1653:in `send'
    vendor/rails/activerecord/lib/active_record/base.rb:1653:in `instantiate'
    vendor/rails/activerecord/lib/active_record/base.rb:661:in `find_by_sql'
    vendor/rails/activerecord/lib/active_record/base.rb:661:in `collect!'
    vendor/rails/activerecord/lib/active_record/base.rb:661:in `find_by_sql'
    vendor/rails/activerecord/lib/active_record/base.rb:1548:in `find_every'
    vendor/rails/activerecord/lib/active_record/base.rb:1505:in `find_initial'
    vendor/rails/activerecord/lib/active_record/base.rb:692:in `exists?'
    vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:380:in `send'
    vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:380:in `method_missing'
    vendor/rails/activerecord/lib/active_record/base.rb:2143:in `with_scope'
    vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb:206:in `send'
    vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb:206:in `with_scope'
    vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:376:in `method_missing'
    vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:336:in `include?'
    test/unit/backlog_item_test.rb:159:in `test_tagging'
    vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__'
    vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run

Error occurs when in test I'm doing something like that:

item.tags.include? tags(:banana)

..it generates following sql query:

Tag Load (0.2ms)   SELECT `tags`.id FROM `tags` INNER JOIN `backlog_item_tags` ON `tags`.id = `backlog_item_tags`.tag_id WHERE (`tags`.`id` = 59467727) AND ((`backlog_item_tags`.backlog_item_id = 1737950289)) ORDER BY name ASC LIMIT 1
@lighthouse-import

Imported from Lighthouse.
Comment by Adam Byrtek - 2009-09-09 08:37:08 UTC

The SQL query is fine, id is enough to do include?. The problem is that models are instantiated having only id, without other properties and after_find assumes fully instantiated models.

@lighthouse-import

Imported from Lighthouse.
Comment by Adam Byrtek - 2009-09-09 09:06:52 UTC

Trivial patch attached, do "SELECT *" instead of "SELECT id". This is done only on a single row (LIMIT 1) so there is no need for micro-optimization.

@lighthouse-import

Imported from Lighthouse.
Comment by Jorge Dias - 2009-09-10 11:15:49 UTC

I was having the same error, applied the patch and now it works

@lighthouse-import

Imported from Lighthouse.
Comment by Marek Kowalski - 2009-09-17 15:34:15 UTC

I spend some time debugging this issue and the problem is with the ActiveRecord::Base.exists? method. In Rails 2.3.2 it looked like this:

     def exists?(id_or_conditions = {})
        connection.select_all(
          construct_finder_sql(
            :select     => "#{quoted_table_name}.#{primary_key}",
            :conditions => expand_id_conditions(id_or_conditions),
            :limit      => 1
          ),
          "#{name} Exists"
        ).size > 0
      end

Method above doesn't instantiate the record nor does it call the after_find filter chain.

In Rails 2.3.4 exists? method calls find_initial with :select => "#{table}.#{primary_key}". This causes record to be instantiated and after_find filter called. However the only attribute loaded is 'id', so calling any other method inside the filter will cause the ArgumentError.

IMO after_find filter should be called but with all the columns loaded

@lighthouse-import

Imported from Lighthouse.
Comment by Tim Connor - 2009-09-26 19:16:19 UTC

This also pops up if you try to access any attributes in an after_initialize, which almost makes it seem like a regression from about 2007, or so. Not sure that the patch will fix that part of the bug.

@lighthouse-import

Imported from Lighthouse.
Comment by kbrock - 2009-10-02 08:08:18 UTC

This patch should fix after_initialize

But I still like the previous way of testing exists.

It did not pull back fields, it did not instantiate an object. It just pulled back the id.

I'd go back to the previous code. And better yet, Id tweak the select clause to optimize even more (do :select => 1)
That way it could possibly only use the index and not even touch the data page depending upon the exists clause.

suggestion:

      def exists?(id_or_conditions = {})
        connection.select_all(
          construct_finder_sql(
            :select     => 1,
            :conditions => expand_id_conditions(id_or_conditions),
            :limit      => 1
          ),
          "#{name} Exists"
        ).size > 0
      end

Thanks for writing this up

@lighthouse-import

Imported from Lighthouse.
Comment by Adam Byrtek - 2009-10-02 18:32:23 UTC

One way or another it would be nice to have this resolved by a Rails commiter.

@lighthouse-import

Imported from Lighthouse.
Comment by Kieran P - 2009-10-08 00:38:02 UTC

+1 This problem has caused some issues in our application. The fixes above don't help though.

test: The topic related image slideshow when several images are related to a topic, the slideshow should be populated in the session on selected image visit. (TopicsControllerTest):
ActiveRecord::MissingAttributeError: missing attribute: basket_id
    vendor/rails/activerecord/lib/active_record/association_preload.rb:309:in`send'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:309:in `preload_belongs_to_association'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:308:in`each'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:308:in `preload_belongs_to_association'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:120:in`send'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:120:in `preload_one_association'
    vendor/rails/activesupport/lib/active_support/ordered_hash.rb:97:in`each'
    vendor/rails/activesupport/lib/active_support/ordered_hash.rb:97:in `each'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:114:in`preload_one_association'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:91:in `preload_associations'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:90:in`preload_associations'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:90:in `each'
    vendor/rails/activerecord/lib/active_record/association_preload.rb:90:in`preload_associations'
    vendor/rails/activerecord/lib/active_record/base.rb:1549:in `find_every'
    vendor/rails/activerecord/lib/active_record/base.rb:615:in`find'
    vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:60:in `find'
    lib/image_slideshow.rb:194:in`find_related_images'
    lib/image_slideshow.rb:139:in `populate_slideshow'
    lib/image_slideshow.rb:119:in`prepare_slideshow'
    vendor/rails/activesupport/lib/active_support/callbacks.rb:178:in `send'
    vendor/rails/activesupport/lib/active_support/callbacks.rb:178:in`evaluate_method'
    vendor/rails/activesupport/lib/active_support/callbacks.rb:166:in `call'
    vendor/rails/actionpack/lib/action_controller/filters.rb:225:in`call'
    vendor/rails/actionpack/lib/action_controller/filters.rb:629:in `run_before_filters'
    vendor/rails/actionpack/lib/action_controller/filters.rb:615:in`call_filters'
    vendor/rails/actionpack/lib/action_controller/filters.rb:610:in `perform_action_without_benchmark'
    vendor/rails/actionpack/lib/action_controller/benchmarking.rb:68:in`perform_action_without_rescue'
    vendor/rails/activesupport/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    /opt/ruby-enterprise-1.8.7-20090928/lib/ruby/1.8/benchmark.rb:308:in`realtime'
    vendor/rails/activesupport/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    vendor/rails/actionpack/lib/action_controller/benchmarking.rb:68:in`perform_action_without_rescue'
    vendor/rails/actionpack/lib/action_controller/rescue.rb:160:in `perform_action_without_flash'
    vendor/rails/actionpack/lib/action_controller/flash.rb:146:in`perform_action'
    vendor/rails/actionpack/lib/action_controller/base.rb:532:in `send'
    vendor/rails/actionpack/lib/action_controller/base.rb:532:in`process_without_filters'
    vendor/rails/actionpack/lib/action_controller/filters.rb:606:in `process'
    vendor/rails/actionpack/lib/action_controller/test_process.rb:567:in`process_with_test'
    vendor/rails/actionpack/lib/action_controller/test_process.rb:447:in `process'
    vendor/rails/actionpack/lib/action_controller/test_process.rb:398:in`get'
    lib/image_slideshow_test_helper.rb:22:in `__bind_1254962019_963356'
    /opt/ruby-enterprise-1.8.7-20090928/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/context.rb:351:in`call'
    /opt/ruby-enterprise-1.8.7-20090928/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/context.rb:351:in `test: The topic related image slideshow when several images are related to a topic, the slideshow should be populated in the session on selected image visit. '
    vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in`**send**'
    vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run'

@lighthouse-import

Imported from Lighthouse.
Comment by Tim Connor - 2009-10-19 20:34:05 UTC

I wonder what we have to do to get this in the queue for 2.3.5?

@lighthouse-import

Imported from Lighthouse.
Comment by Randy Souza - 2009-10-21 12:58:18 UTC

+1 -- bitten by this same problem.

@lighthouse-import

Imported from Lighthouse.
Comment by Matt Jones - 2009-10-21 21:37:46 UTC

This is being caused by the fix introduced in #2543, which switched to using find_initial to properly account for scoping applied to the association. Assigning to Koz, as he handled the original ticket.

@lighthouse-import

Imported from Lighthouse.
Comment by Caius - 2009-10-22 14:08:00 UTC

This also pops up if you try to access any attributes in an after_initialize

+1 - I'm running into this trying to set default values in after_initialise.

@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2009-10-24 08:16:21 UTC

The issue is that find_by_sql is

This bug bit me as well. Instead of changing the select query from 'id' to '*', I believe the correct fix is to not instantiate the ActiveRecord object for calls to #exists?. In the ticket that created this bug, #2543, Peter Marklund proposed adding a :instantiate = false option to find_every that would tell find_by_sql to not instantiate the records.

I have attached a patch with a test which demonstrates this bug by attempting to access a database attribute in an after_initialize. As a solution, I've implemented Peter Marklund's idea for a skip instantiation option. In addition to resolving this issue, this patch is beneficial because it improves the performance of calls to #exists?. As Koz stated in the previous ticket, this saves AR from firing the relevant callbacks for the model.

Any feedback is welcome.

@lighthouse-import

Imported from Lighthouse.
Comment by Marek Kowalski - 2009-10-24 11:40:32 UTC

-1. In my opinion #exists? should instantiate the object. At least I would expect the after_find filter to be fired.

@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2009-10-25 04:18:49 UTC

Marek,

Why would you expect the after_find filter to be fired for exists? As you mentioned earlier, that was not the behavior in Rails 2.3.2.

I think of #exists? as the equivalent of executing a SQL count (even though it is not for performance reasons) and intuitively would not expect it to call after_find.

@lighthouse-import

Imported from Lighthouse.
Comment by Marek Kowalski - 2009-10-25 09:54:04 UTC

Rob,

In fact in Rails 2.3.2 the after_find filter was not fired but I found out about it just when I was debugging the issue with missing attributes. Why I think it should be fired? Well, we should consider for what purpose people use the after_find filter. In the application I work on we use it as the last security fence - it makes sure that the newly instantiated object belongs to the scope of interest of the current user. So not firing this filter when using #exists? theoretically opens the way for the true-negative effect: checking exists? returns true, because the record with given ID is present, however attempt to load the object will fail. Of course we have other security algorithms to double check it, but maybe some other people don't. Or maybe I'm just talking non sense... So for what purpose do you use the after_find filter ?

Best,
Marek

@lighthouse-import

Imported from Lighthouse.
Comment by Marek Kowalski - 2009-10-25 10:02:53 UTC

Rob,

In fact in Rails 2.3.2 the after_find filter was not fired but I found out about it just when I was debugging the issue with missing attributes. Why I think it should be fired? Well, we should consider for what purpose people use the after_find filter. In the application I work on we use it as the last security fence - it makes sure that the newly instantiated object belongs to the scope of interest of the current user. So not firing this filter when using #exists? theoretically opens the way for the true-negative effect: checking exists? returns true, because the record with given ID is present, however attempt to load the object will fail. Of course we have other security algorithms to double check it, but maybe some other people don't. Or maybe I'm just talking non sense... So for what purpose do you use the after_find filter ?

Best,
Marek

@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2009-10-26 03:26:57 UTC

Marek, thank you for sharing how you have employed after_find in your application. I do not know all of the details of your application but I typically handle that situation differently.

One common way I ensure that the object I am finding belongs to the scope of interest of the current user is to do the find on an association proxy. Koz covered this in a blog post on therailsway.com.

In that case the find will return nil which then tells me that the record does not exist. With this technique I can accomplish the same thing in 1 sql query that you are doing with 2.

To be honest I have not found a good use for the after_find filter yet. However, since I did not know that it was available until recently, I would not have thought to use it before.

@lighthouse-import

Imported from Lighthouse.
Comment by Marek Kowalski - 2009-10-26 10:09:49 UTC

Rob, of course you are right, this is sensible approach. But you also have to take into consideration that some lame developer could forget about accessing the object through the association, just like: Model.find(params[:id]). This would be a major security breach, but after_find filter comes to the rescue. Rails code is not very idiot-resistant, so if you are working in the team where people come and go you should better optimize the security rather than the number of SQL queries.

@lighthouse-import

Imported from Lighthouse.
Comment by Tom Lea - 2009-10-26 15:20:36 UTC

@Rob Olson: Just a note, your failing test does not fail. after_initialize is not quite like other callbacks, it's slipped unless defined as an actual method (for performance reasons).

@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2009-10-28 06:52:58 UTC

@Tom Lea: I have been unable to recreate after_initialize not running. On my machines it runs (and results in an error) the way I have specified it. Any ideas?

Also, I realize that the test results in an error instead of a failure. To receive a failure instead the test could be written like this:

def test_exists_on_model_with_after_initialize_method_should_not_blow_up
  assert_nothing_raised { assert Entrant.exists? }
end
@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2009-11-10 03:37:38 UTC

As Tom Lea pointed out, in the 2-3-stable branch after_initialize must be declared as an actual method. It was working fine for me before but after updating Rails passing a block to after_initialize stopped working.

I've adjusted the patch for 2-3-stable. The old patch file (above) still works for Rails 3.0 and is preferred for the master branch since "def after_initialize" gives a deprecation warning in Rails 3.

@lighthouse-import

Imported from Lighthouse.
Comment by Mark Dodwell - 2009-12-06 01:16:05 UTC

I ran into this same bug, and side-stepped it by changing the way I was setting my attribute in #after_initialize:

def after_initialize
  self.token ||= "foobar"
end

becomes:

def after_initialize
  write_attribute(:token, "foobar") unless read_attribute(:token)
end

and that made the ActiveRecord::MissingAttributeError error go away. Might be of use to somebody?

@lighthouse-import

Imported from Lighthouse.
Comment by CDD Developers - 2010-02-19 06:52:54 UTC

Rob's patch works well on 2.3.5 for our production server. It would be nice if this could be committed for 2.3.6

@lighthouse-import

Imported from Lighthouse.
Comment by Tim Connor - 2010-05-23 21:37:36 UTC

Did this NOT make it into 2.3.6?

@lighthouse-import

Imported from Lighthouse.
Comment by Nikos Dimitrakopoulos - 2010-06-08 19:34:02 UTC

And nor in 2.3.8... Could a committer please spend some integrating Rob's patch?

Thanks

@lighthouse-import

Imported from Lighthouse.
Comment by Yuri - 2010-07-09 15:11:47 UTC

Still broken in 2.3.8. Annoying.

@lighthouse-import

Imported from Lighthouse.
Comment by Geoffroy - 2010-07-28 10:13:20 UTC

Still seems unresolved in 3.0.0rc

the solution from Mark Dodwell works though

def after_initialize
write_attribute(:token, "foobar") unless read_attribute(:token)
end

maybe worth to include in the final?

@lighthouse-import

Imported from Lighthouse.
Comment by Ernie Miller - 2010-07-28 20:28:15 UTC

Yup, just ran into this in 3.0.0rc as well. Only happens when doing a query that includes associations with corresponding conditions, triggering the old join code.

@lighthouse-import

Imported from Lighthouse.
Comment by Brian Artiaco - 2010-09-02 18:07:33 UTC

I just spent half a day on this, before I finally found this ticket. While I'm sad that it's been almost exactly a year since this was reported, and it hasn't been fixed yet, here's my simple work around for my scenario:

validates_uniqueness_of :email, :scope => :library_id

def after_initialize
  self.status ||= "Invited"
end

As explained above, this will cause a 'MissingAttributeError' to be thrown, if there are records returned by the validates_uniqueness_of query. My simple solution is this:

def after_initialize
  self.status ||= "Invited" if new_record?
end

While other people are having more complex issues, this should solve the simple case, until an actual solution is commited into rails.

(Also added 2.3.8 to the list of tags, as that is what I'm working against, in preparation for upgrading to rails 3)

@lighthouse-import

Imported from Lighthouse.
Comment by Tim Connor - 2010-09-02 18:37:55 UTC

Brian, no need for that new_record check, if you just use read_attribute and write_attribute. I don't think there is any more complex case that can't solve as a work-around.

@lighthouse-import

Imported from Lighthouse.
Comment by Santiago Pastorino - 2011-02-02 16:49:04 UTC

This issue has been automatically marked as stale because it has not been commented on for at least three months.

The resources of the Rails core team are limited, and so we are asking for your help. If you can still reproduce this error on the 3-0-stable branch or on master, please reply with all of the information you have about it and add "[state:open]" to your comment. This will reopen the ticket for review. Likewise, if you feel that this is a very important feature for Rails to include, please reply with your explanation so we can consider it.

Thank you for all your contributions, and we hope you will understand this step to focus our efforts where they are most helpful.

@lighthouse-import

Imported from Lighthouse.
Comment by Victor Costan - 2011-02-17 06:36:42 UTC

The bug is still present in Rails 3.0.4. This blog post (not mine) shows a workaround: http://blog.edseek.com/archives/2009/04/16/missingattributeerror-from-within-after_initialize/

The error is confusing, and will waste a few hours of time for anyone doing after_initialize / after_find hooks.

[state:open]

@lighthouse-import

Imported from Lighthouse.
Comment by Rob Olson - 2011-04-18 21:55:00 UTC

This has finally been resolved in Rails 3.0.7 by #6127.

It's still present in 2-3-stable though.

@lighthouse-import

Attachments saved to Gist: http://gist.github.com/971642

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