user3517175
user3517175

Reputation: 383

Rails accepts_nested_attributes_for with belongs_to. How to delete relation, not item?

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

Answers (1)

Jay-Ar Polidario
Jay-Ar Polidario

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

Usage:

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

Related Questions