Shaun Decker
Shaun Decker

Reputation: 46

Best way to update (create & delete) multiple associations (has_many :through)

I have what i feel could be a simple question, and i have this working, but my solution doesn't feel like the "Rails" way of doing this. I'm hoping for some insight on if there is a more acceptable way to achieve these results, rather than the way i would currently approach this, which feels kind of ugly.

So, lets say i have a simple has_many :through setup.

# Catalog.rb
class Catalog < ActiveRecord::Base
  has_many :catalog_products
  has_many :products, through: :catalog_products
end

# Products.rb
class Product < ActiveRecord::Base
  has_many :catalog_products
  has_many :catalogs, through: :catalog_products
end

# CatalogProduct.rb
class CatalogProduct < ActiveRecord::Base
  belongs_to :catalog 
  belongs_to :product
end

The data of Catalog and the data of Product should be considered independent of each other except for the fact that they are being associated to each other.

Now, let's say that for Catalog, i have a form with a list of all Products, in say a multi-check form on the front end, and i need to be able to check/uncheck which products are associated with a particular catalog. On the form field end, i would return a param that is an array of all of the checked products.

The question is: what is the most accepted way to now create/delete the catalog_product records so that unchecked products get deleted, newly checked products get created, and unchanged products get left alone?

My current solution would be something like this:

#Catalog.rb
...
def update_linked_products(updated_product_ids)
  current_product_ids = catalog_products.collect{|p| p.product_id}
  removed_products = (current_product_ids - updated_product_ids)
  added_products   = (updated_product_ids - current_product_ids)
  catalog_products.where(catalog_id: self.id, product_id: removed_products).destroy_all
  added_products.each do |prod|
    catalog_products.create(product_id: prod)
  end
end
...

This, of course, does a comparison between the current associations, figures out which records need to be deleted, and which need to be created, and then performs the deletions and creations. It works fine, but if i need to do something similar for a different set of models/associations, i feel like this gets even uglier and less DRY every time it's implemented.

Now, i hope this is not the best way to do this (ignoring the quality of the code in my example, but simply what it is trying to achieve), and i feel that there must be a better "Rails" way of achieving this same result.

Upvotes: 1

Views: 2694

Answers (2)

Mosaaleb
Mosaaleb

Reputation: 1089

First,

has_many :products, through: :catalog_products

generate some methods for you like product_ids, check this under auto-generated methods to know more about the other generated methods.

so we don't need this line:

current_product_ids = catalog_products.collect{|p| p.product_id}

# exist in both arrays are going to be removed
will_be_removed_ids = updated_product_ids & product_ids 
# what's in updated an not in original, will be appended
will_be_added_ids = updated_product_ids - product_ids  

Then, using <<, and destroy methods which are also generated from the association (it gives you the ability to deal with Relations as if they are arrays), we are going to destroy the will_be_removed_ids, and append the will_be_added_ids, and the unchanged will not be affected.

Final version:

def update_linked_products(updated_product_ids)
  products.destroy(updated_product_ids & product_ids)
  products << updated_product_ids - product_ids
end

Upvotes: 1

cmramseyer
cmramseyer

Reputation: 447

Take a look at this https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many-collection-objects

You don't have to remove and create manually each object. If you have already the product_ids array, I think this should work:

#Catalog.rb
...
def update_linked_products(updated_product_ids)
  selected_products = Product.where(id: updated_product_ids)
  products = selected_products
end
...

Upvotes: 1

Related Questions