Search code examples
ruby-on-rails-3activerecordhas-and-belongs-to-manybefore-saveafter-save

Getting previous HABTM values


In my app, I have several clients, and they have several elements (via has_many_through association) depending on a certain BusinessType to which Client belongs to so that instead of manually adding all the elements to the Client, I can just select the BusinessType and everything gets added automatically (business_type in Client is attr_readonly). BusinessType HABTM elements.

Here's the catch, after creation with the default BusinessType, the clients can update their elements and remove or add as they please (mostly add), so what I'm trying to do is the following:

Suppose one business_type has elements [1,2,3] and is assigned to one client, then, the following elements are added manually to the client = [4,5,6] so it ends up having [1,2,3,4,5,6], ok everything's fine here.

But after this, the business_type gets updated and has element 2 removed, so it ends up being [1,3]. Here's the deal, I want the client to be updated by removing the 2, but not the [4,5,6] that do not correspond to the business_type in question so that it ends up [1,3,4,5,6], I'm using an after_update callback to update the clients' elements but the _was method doesn't work for HABTM relationships (to get the old business_type's elements.

I've tried using a before_update callback to first to client.elements = client.elements - business_type.elements to store momentarily in the DB [1,2,3,4,5,6] - [1,2,3] = [4,5,6], and in the after_update do client.elements = client.elements + business_type.elements to get [4,5,6] + [1,3] = [1,3,4,5,6]but this has already the new value of [1,3]. How can I get the old business_type.elements value in the before_update or after_update?

Thanks in advance for your help!


Solution

  • I had a similar problem in an app, and the only solution I could come up with was to store the values before doing update_attributes in the controller.

    Example code:

    Models

    class Product < ActiveRecord::Base
      has_and_belongs_to_many :categories, :join_table => "categories_products"
    
      def remember_prev_values(values)
        @prev_values = values
      end
    
      def before_update_do_something
        puts @prev_values - self.category_ids # Any categories removed?
        puts self.category_ids - @prev_values # Any categories added?
      end
    end
    
    class Category < ActiveRecord::Base
      has_and_belongs_to_many :products, :join_table => "categories_products"
    end
    

    In the update method in the products controller I do the following:

    class ProductsController < ApplicationController
      ...
    
      def update
        @product.remember_prev_values(@product.category_ids)
        if @product.update_attributes(params[:product])
          flash[:notice] = "Product was successfully updated."
          redirect_to(product_path(@product))
        else
          render :action => "edit"
        end
      end
    
      ...
    
    end
    

    It is not ideal, but it is then possible to "catch" the habtm inserts/removes before they are executed.

    I do think it is possible to do in a callback, but you might need to "hack" into ActiveRecord.

    I did not spend much time on trying to dig into ActiveRecord internals, as this is a simple implementation that works.