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.