ruby-on-railspolymorphismassociationsruby-on-rails-6polymorphic-associations

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.


Solution

  • 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.


    These don't work here:

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

    accepts_nested_attributes_for doesn't work on a polymorphic relationship.


    Because these ^ don't work. You can add inverse_of option on :wearable and just assign new Hat to wearable:

    # normally this will fail, which is what the current issue is
    Person.new(wearable: Hat.new).wearable.person.initials
    
    # if you add inverse_of
    belongs_to :wearable,
      polymorphic: true,
      required:    false,
      dependent:   :destroy,
      inverse_of:  :person
    
    # now it works
    Person.new(wearable: Hat.new).wearable.person.initials
    
    # this also avoids an extra "Person Update"
    
    # `after_create` in `Person` can be taken out and you can create
    # a person like this:
    Person.create(wearable: Hat.new)