Search code examples
ruby-on-railsrubyactiverecordassociations

How can I avoid unnecessary loading of Associated model while creating it?


In my Rails app, we have a couple of models similar to this

class Pen < ActiveRecord::Base
  has_one :ink
  after_create :add_ink

  def add_ink
    create_ink(color: "blue")
  end
end

class Ink < ActiveRecord::Base
  belongs_to :pen
end

and in a Controller we do something like this

pen = Pen.new(..)
pen.save

Conceptually, a Pen always has an Ink, and we create the Ink when the Pen is created. (Also open to better ways to accomplish this than an after_create hook).

The problem is that it's always issuing a SELECT for the association even though there's guaranteed to be nothing there.

[DEBUG]   TRANSACTION (0.2ms)  BEGIN
[DEBUG]   Pen Create (1.5ms)  INSERT INTO pens ("owner_id") VALUES ('00f74077-c079-4482-aa03-15b23951a7bd') RETURNING "id"
[DEBUG]   Ink Load (0.4ms)  SELECT "inks".* FROM "inks" WHERE "inks"."pen_id" = '93a45bae-2cf3-48c1-ac00-6b3059dca5ae' LIMIT 1
[DEBUG]   Ink Create (0.3ms)  INSERT INTO "inks" ("pen_id", "color") VALUES ('93a45bae-2cf3-48c1-ac00-6b3059dca5ae', 'blue') RETURNING "pen_id"
[DEBUG]   TRANSACTION (0.2ms)  COMMIT

Specifically, this shouldn't be happening

[DEBUG]   Ink Load (0.4ms)  SELECT "inks".* FROM "inks" WHERE "inks"."pen_id" = '93a45bae-2cf3-48c1-ac00-6b3059dca5ae' LIMIT 1

I have tried using build_association followed by a save, create_association, and using an association= with an Association.create and they all result in an unnecessary Load at some point.


Solution

  • What you are seeing is caused by the fact that the Pen is already persisted when the creation of the Ink occurs in an after_create.

    From the Docs

    • Assigning an object to a has_one association automatically saves that object and the object being replaced (if there is one), in order to update their foreign keys - except if the parent object is unsaved (new_record? == true).
    • If either of these saves fail (due to one of the objects being invalid), an ActiveRecord::RecordNotSaved exception is raised and the assignment is cancelled.

    So what you are seeing is rails trying to determine if the associated record already exists, so that it can be updated accordingly.

    Depending on how your desired outcome you could try Ink.create(pen_id: self.id, color: 'blue'); however, I think just moving this to a before_create action is probably a better implementation e.g.

    class Pen < ActiveRecord::Base
      has_one :ink
      before_create :add_ink
    
      def add_ink
        build_ink(color: "blue")
      end
    end
    

    According to the Docs

    :autosave

    If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. If false, never save or destroy the associated object. By default, only save the associated object if it’s a new record.

    So since this is a before_create the Pen is a new record and therefore it should save the Ink as well, without querying the database.