Reputation: 383
I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric, allow_destroy: true
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I don't know how to destroy relation between material and rubric.
I have a test case:
it 'should delete relation to rubric' do
# create two materials with relation to the same rubric
item = FactoryBot.create(:material_with_rubric)
expect(Rubric.count).to eq(1)
expect(item.rubric_id).to_not eq(nil)
FactoryBot.create(:material, rubric_id: item.rubric_id)
expect(Material.count).to eq(2)
# try to destroy relation for first material
item.assign_attributes(
rubric_attributes: {
id: item.rubric_id,
_destroy: '1'
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(nil)
# rubric should exist in database because we have second material with relation
expect(Rubric.count).to be > 0
end
But, after running test case I see an error:
Failure/Error: expect(Rubric.count).to be > 0
expected: > 0
got: 0
How to destroy a relation using rubric_attributes and don't delete the item from database. For example accepts_nested_attributes_for with allow_destroy works pretty well with has_many
P.S. I know about item.update(rubric_id: nil). But I want to have the same result using item.update(rubric_attributes: {})
Upvotes: 1
Views: 2544
Reputation: 6603
Rails nested_attributes
(in particular for this case your rubic_attributes
Hash) means that you are modifying the associated record(s) and not the record itself. Because item_id
is an attribute of the Material model and not the associated Rubic
model, then nested attribute Hash won't be able to update that value (unless its associated record is destroyed through _destroy
, of which Rails seem to automatically update material.rubric_id
to nil
).
But because you do not want to destroy the associated record but just update material.rubric_id
to nil
through the material.update(rubric_attributes: {...})
Hash and that you do not want to use the "normal-way" of simply doing material.update(rubric_id: nil)
, then you can do a workaround like below which still makes use of rubric_attributes
:
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric, allow_destroy: true
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
accepts_nested_attributes_for :materials
end
rubric = Rubric.create!
material = Material.create!(rubric: rubric)
material.update!(
rubric_attributes: {
id: material.rubric.id,
materials_attributes: {
id: material.id,
rubric_id: nil # or `rubric: nil`
}
}
)
puts material.rubric_id
# => 1
# not sure why `material` needs to be reloaded. Rails `inverse_of` seems to not work on "nested" nested_attributes
material.reload
puts material.rubric_id
# => nil
puts Rubric.exists?(id: 1)
# => true
# now you've updated material.rubric_id to nil, but not destroying the Rubric record
IMPORTANT! If this nested attributes is going to be used in the controller, for security purposes, don't forget to only permit the params.permit(rubric_attributes: [materials_attributes: [:rubric_id]])
... or any other fields you deem whitelistable.
Upvotes: 2