Home > database >  Rails: How to create two records with has_one association at the same time
Rails: How to create two records with has_one association at the same time

Time:12-24

I have a Rails 6 app. The Hat model is in a has_one polymorphic relationship with the Person model. (I know this seems backwards. I'm not the author of this code.) The Person model creates the associated Hat in a callback. The problem is that the Hat needs to reference attributes of its Person during creation, and that association is nil when created in this way...

class Person < ApplicationRecord
  belongs_to :wearable, polymorphic: true, required: false, dependent: :destroy

  after_create do 
    if wearable.nil?
      wearable = Hat.create(...) # at this moment, the Hat has no Person
      self.wearable = wearable
      save
    end
  end

end


class Hat < ApplicationRecord
  has_one    :person, as: :wearable, class_name: 'Person'

  after_create do
    embroider( self.person.initials ) # <-- This will error!!
  end

end

Is there a way the Person can create the Hat with the association in place from the outset?

I think this is possible with non-polymorphic relationships by calling create on the association method. I think something like self.hat.create(...) would work, but I'm not sure how to do this in a polymorphic context.

CodePudding user response:

self.hat.create(...) will not work since a belongs_to assocation is nil until you assign it. Instead the singular assocation macros generate new_wearable, create_wearable etc methods:

class Person < ApplicationRecord
  belongs_to :wearable, polymorphic: true, required: false, dependent: :destroy

  after_create do 
    self.create_wearable(type: 'Hat', ...) if wearable.nil?
  end
end

But general the entire approach using an after_create callback is far from ideal.

You're creating two different transactions instead of one and you're not getting the data integrity gaurentee provided by wrapping both in the same transaction:

person = Person.new(...) do |p|
  p.new_wearable(...)
end
person.save # saves both in a single transaction

If either insert fails here you get a rollback instead of leaving the data in an incomplete state.

To add to this are the general drawbacks with callbacks which is that they rely on implicit logic and it can be hard to actually control when and where they are fired. Chances are its better to just expliticly create the assocatiated record in the one place you actually need it (the create method in the controller).

Rails also provides accepts_nested_attributes that lets you pass the attributes for the nested record without the extra work:

class Person < ApplicationRecord
  # ...
  accepts_nested_attributes_for :wearable
end


person = Person.new(..., wearable_attributes: { type: 'Hat', ... }) 
person.save # saves both in a single transaction

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

CodePudding user response:

When creating a hat you can set the relationship that you have defined, which is person:

after_create do
  Hat.create!(person: self) unless wearable
  # NOTE: don't need the rest
  # self.wearable = wearable
  # save
end

You must use create! to rollback a transaction on errors.


Update

Seems I owe an explanation. It's not that I haven't tried everything else before posting:

class Person < ApplicationRecord
  belongs_to :wearable, polymorphic: true, required: false, dependent: :destroy
  after_create do
    # NOTE: MUST USE BANG METHODS
    Hat.create!(person: self) unless wearable
  end
end

class Hat < ApplicationRecord
  has_one :person, as: :wearable
  validates :name, presence: true # make a validation to fail
end

If we try to create a person. Single transaction, no records created:

# Rails 6.1.6.1
>> Person.create!
  TRANSACTION (0.1ms)  begin transaction
  Person Create (0.3ms)  INSERT INTO "people" DEFAULT VALUES
  TRANSACTION (0.2ms)  rollback transaction
/home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-6.1.6.1/lib/active_record/validations.rb:80:in `raise_validation_error': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)

What if after_create in Hat fails:

class Hat < ApplicationRecord
  has_one :person, as: :wearable
  after_create do
    raise ActiveRecord::RecordInvalid
  end
end

Single transaction, no records created:

>> Person.create!
  TRANSACTION (0.1ms)  begin transaction
  Person Create (0.3ms)  INSERT INTO "people" DEFAULT VALUES
  Hat Create (0.2ms)  INSERT INTO "hats" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2022-12-24 09:52:00.410465"], ["updated_at", "2022-12-24 09:52:00.410465"]]
  Person Update (0.1ms)  UPDATE "people" SET "wearable_type" = ?, "wearable_id" = ? WHERE "people"."id" = ?  [["wearable_type", "Hat"], ["wearable_id", 4], ["id", 4]]
  TRANSACTION (0.1ms)  rollback transaction
/home/alex/code/SO/rails6/app/models/hat.rb:7:in `block in <class:Hat>': Record invalid (ActiveRecord::RecordInvalid)

build_wearable, create_wearable - these methods are not created for polymorphic relationships.

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
See table: Singular associations (one-to-one)

>> Person.first.build_wearable
  Person Load (0.2ms)  SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT ?  [["LIMIT", 1]]
/home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-6.1.6.1/lib/active_model/attribute_methods.rb:469:in 
`method_missing': undefined method `build_wearable' for #<Person id: 7, name: nil, wearable_type: "Hat", wearable_id: 5> (NoMethodError)

accepts_nested_attributes_for doesn't work on a polymorphic relationship like this, type isn't going to do anything, this isn't STI:

>> Person.create!(wearable_attributes: {type: "Hat", name: "something"})
/home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-6.1.6.1/lib/active_record/nested_attributes.rb:430:in `assign_nested_attributes_for_one_to_one_association':
Cannot build association `wearable'. Are you trying to build a polymorphic one-to-one association? (ArgumentError)

This could be fixed to work, though.


So a single callback to make a backwards relationship work, seemed like a good choice. Hence, my short answer before the update. If I missed something, let me know.

  • Related