简体   繁体   中英

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

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.

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM