Skip to content

MissingAttributeError for database column in after_find callback #3313

Closed
shlevy opened this Issue Oct 12, 2011 · 8 comments

6 participants

@shlevy
shlevy commented Oct 12, 2011

Under some circumstances (which are not clear to me), calling self.column_name in an after_find throws a MissingAttributeError even when column_name is a valid db column. In particular, I have a project with a milestones table that has a billing_invoice field where

Milestone.find 1, include: [project: [:time_logs]], order: "projects.name"

fails with a MissingAttributeError for billing_invoice within the after_find callback but

Milestone.all conditions: "milestones.id = 1", include: [project: [:time_logs]], order: "projects.name"

succeeds. An essentialized version of the project with steps to repro the bug can be found at https://github.com/shlevy/after-find-bug

@Karunakar

I just looking your code. Seems that you have added the invoiceable_model lib. Its expecting the billing_invoice.

As per My idea Its not related to Rails source code. Its related to invoiceable_mode.

@shlevy
shlevy commented Oct 16, 2011

invoiceable_model.lib is only there to reduce code repetition. It is very small, literally all it does is define two functions, without overriding any core class or anything. Here is the code, can you explain how it is causing this problem?

module InvoiceableModel
  private
  def set_billing_invoice
    if self.invoice_id_changed?
      self.billing_invoice = self.invoice.nil? ? nil : self.invoice.number
    end
  end

  def set_invoice
    unless self.billing_invoice.nil? || self.billing_invoice == ""
      if self.invoice.nil? || self.invoice.number != self.billing_invoice
        invoice = Invoice.find_by_number self.billing_invoice

        if invoice.nil?
          self.create_invoice number: self.billing_invoice
        else
          self.invoice = invoice
        end
      end
    end
  end
end
@isaacsanders

@shlevy Is this still an issue?

@shlevy
shlevy commented May 5, 2012

@isaacsanders Yes, I've updated my sample github project with repro steps to 3.2.3 and the issue persists.

@senny
Ruby on Rails member
senny commented Nov 10, 2012

you need to be very careful when you use after_find callbacks. To execute:

Milestone.find 1, include: [project: [:time_logs]], order: "projects.name"

ActiveRecord executes the following sql:

  Milestone Load (2.0ms)  SELECT DISTINCT "milestones".id FROM "milestones" LEFT OUTER JOIN "projects" ON "projects"."id" = "milestones"."project_id" LEFT OUTER JOIN "time_logs" ON "time_logs"."project_id" = "projects"."id" WHERE "milestones"."id" = ? ORDER BY projects.name LIMIT 1  [["id", 1]]

  SQL (0.2ms)  SELECT "milestones"."id" AS t0_r0, "milestones"."project_id" AS t0_r1, "milestones"."invoice_id" AS t0_r2, "milestones"."billing_invoice" AS t0_r3, "milestones"."created_at" AS t0_r4, "milestones"."updated_at" AS t0_r5, "projects"."id" AS t1_r0, "projects"."name" AS t1_r1, "projects"."created_at" AS t1_r2, "projects"."updated_at" AS t1_r3, "time_logs"."id" AS t2_r0, "time_logs"."project_id" AS t2_r1, "time_logs"."invoice_id" AS t2_r2, "time_logs"."billing_invoice" AS t2_r3, "time_logs"."created_at" AS t2_r4, "time_logs"."updated_at" AS t2_r5 FROM "milestones" LEFT OUTER JOIN "projects" ON "projects"."id" = "milestones"."project_id" LEFT OUTER JOIN "time_logs" ON "time_logs"."project_id" = "projects"."id" WHERE "milestones"."id" = ? AND "milestones"."id" IN (1) ORDER BY projects.name  [["id", 1]]

you can see, that the first call only fetches the ID, to fetch the complete records later on. This has the effect, that AR first instantiates a Milestone Model with only the ID present. The after_find callback runs and the code breaks. If you add a guard clause to your set_invoice method, everything works.:

def set_invoice
  return unless attributes.has_key?(:billing_invoice)
  ...
end

The second example that worked:

Milestone.all conditions: "milestones.id = 1", include: [project: [:time_logs]], order: "projects.name"

results in the following sql:

  SQL (0.2ms)  SELECT "milestones"."id" AS t0_r0, "milestones"."project_id" AS t0_r1, "milestones"."invoice_id" AS t0_r2, "milestones"."billing_invoice" AS t0_r3, "milestones"."created_at" AS t0_r4, "milestones"."updated_at" AS t0_r5, "projects"."id" AS t1_r0, "projects"."name" AS t1_r1, "projects"."created_at" AS t1_r2, "projects"."updated_at" AS t1_r3, "time_logs"."id" AS t2_r0, "time_logs"."project_id" AS t2_r1, "time_logs"."invoice_id" AS t2_r2, "time_logs"."billing_invoice" AS t2_r3, "time_logs"."created_at" AS t2_r4, "time_logs"."updated_at" AS t2_r5 FROM "milestones" LEFT OUTER JOIN "projects" ON "projects"."id" = "milestones"."project_id" LEFT OUTER JOIN "time_logs" ON "time_logs"."project_id" = "projects"."id" WHERE (milestones.id = 1) ORDER BY projects.name

this time, everything is loaded from the beginning and as such, the code works as expected.

I don't think this is a bug but rather the functionality how AR works. I don't suggest to use after_find for application logic as in your example application.

@steveklabnik I don't think this is a bug, what do you think?

@steveklabnik
Ruby on Rails member

Ouch. That's rough. Yeah, I'm leaning that way too. Let's wait for @tenderlove or @jonleighton to weigh in, though.

@rafaelfranca
Ruby on Rails member

@senny I think is worth to dig in the code. The after_find callback is being triggered in the wrong moment.

@senny senny closed this in db51704 Dec 4, 2012
@senny senny added a commit to senny/rails that referenced this issue Dec 4, 2012
@senny senny backport #8403, no intermediate AR objects when eager loading.
Closes #3313

Conflicts:

	activerecord/CHANGELOG.md
	activerecord/test/models/developer.rb
1b96176
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.