Can I delete missing associations using accepts_nested_attributes_for in rails?

1.3k Views Asked by At

TL;DR

This seems like it should be really simple. I want to set up #accepts_nested_attributes_for such that when I save the parent class with any non-empty params set for the the child class, it deletes any other children that aren't directly referenced by their id in the params.

For some reason I'm really struggling to find a sensible way of doing this.

That's really it.

Longer version

I have a model, Pledge:

class Pledge < ActiveRecord::Base
  has_many :companies, dependent: :destroy
  accepts_nested_attributes_for :companies

  # Other stuff
end

And a corresponding Company model:

class Company < ActiveRecord::Base
  belongs_to :pledge
end

Suppose I have company_1 (id: 1, name: company_1) already saved on pledge_a, and I then call pledge_.update(params), where params either doesn't reference a company id, thus:

{  
  "someotherval"=>"5",
  "id"=>"10",
  "companies_attributes"=>
  {  
    "0"=>
    { 
      "name"=>"company_2" 
    }
  }
}

or (exactly as above, but) with a different id for the companies in question:

{  
  "someotherval"=>"5",
  "id"=>"10",
  "companies_attributes"=>
  {  
    "0"=>
    { 
      "id"=>"2"
      "name"=>"company_2" 
    }
  }
}

What's the simplest way to instruct Rails delete any companies that aren't included in the params, ie to create a fresh set every time I call #update with any present company in the nested params?

I've tried quite a few things:

Using a before_save hook on Pledge to delete companies - this seemed to delete them after the companies were saved, so that I always ended up with a companyless-pledge

Building up an array of companies by adding a :reject_if block to the accepts_nested_attributes_for a line and then using it for a destroy_all_companies after_save hook - this didn't work because at some point Rails seemed to re-initialize the pledge, so that by the time we hit the hook, the array had vanished

Writing some code in the controller before I call #update to delete the companies - this is the hack I've been reduced to, but it's horrible. Firstly I don't always want to delete the companies if I'm passing in params that don't include any nested company params. Secondly I have to go through the params and delete any non-empty company #id values, otherwise Rails explodes when it can't find the company in question. Thirdly if the validations fail and the Pledge doesn't update, I've now deleted all the companies when I didn't want to.

There must be a saner way to do this... right?

1

There are 1 best solutions below

0
On

Given a list of the companies you want to destroy, i.e. something like this:

companies_to_destroy = @pledge.companies.reject do |company|
  pledge_params[:companies_attributes]
    .map { |attrs| attrs[:id] }
    .include? company.id
end

As @gabrielhilal's comment suggested, you could use allow_destroy: true in the accepts_nested_attributes_for and then add to the companies_attributes array { id: 'x', _destroy: '1' } for each company to destroy.

Alternatively you could call mark_for_destruction on the companies you want to destroy.

In either case the companies to destroy will be destroyed when you call save on the pledge.