dcangulo
dcangulo

Reputation: 2107

How to detect changes in has_many through association?

I have the following models.

class Company < ApplicationRecord
  has_many :company_users
  has_many :users, :through => :company_users

  after_update :do_something

  private

  def do_something
    # check if users of the company have been updated here
  end
end

class User < ApplicationRecord
  has_many :company_users
  has_many :companies, :through => :company_users
end

class CompanyUser < ApplicationRecord
  belongs_to :company
  belongs_to :user
end

Then I have these for the seeds:

Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]

Let's say I want to update Company 1 users, I will do the following:

Company.first.update :users => [User.first, User.second]

This will run as expected and will create 2 new records on CompanyUser model.

But what if I want to update again? Like running the following:

Company.first.update :users => [User.third, User.fourth]

This will destroy the first 2 records and will create another 2 records on CompanyUser model.

The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?

However, updating an attribute works just fine:

Company.first.update :name => 'New Company Name'

How can I make it work on associations too?

So far I have tried the following but no avail:

Upvotes: 4

Views: 2766

Answers (3)

Bisho Silwal
Bisho Silwal

Reputation: 121

There is a collection callbacks before_add, after_add on has_many relation.

class Project
  has_many :developers, after_add: :evaluate_velocity

  def evaluate_velocity(developer)
    #non persisted developer
    ...
  end
end

For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks

Upvotes: 1

mechnicov
mechnicov

Reputation: 15372

You can use attr_accessor for this and check if it changed.

class Company < ApplicationRecord
  attr_accessor :user_ids_attribute

  has_many :company_users
  has_many :users, through: :company_users

  after_initialize :assign_attribute
  after_update :check_users

  private

  def assign_attribute
    self.user_ids_attribute = user_ids
  end

  def check_users
    old_value = user_ids_attribute

    assign_attribute

    puts 'Association was changed' unless old_value == user_ids_attribute
  end
end

Now after association changed you will see message in console.

You can change puts to any other method.

Upvotes: 0

morissetcl
morissetcl

Reputation: 564

I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:

This will destroy the first 2 records and will create another 2 records on CompanyUser model.

Knowing that I will advice you to try the following code:

Company.first.users << User.third

In this way you will not override current associations. If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.

You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference

Hope it will be helpful.

Edit:

Ok I thought it wasn't your real issue.

Maybe 2 solutions:

#1 Observer:

what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.

gem rails-observers

Inside this observer call a service or whatever you like that will do what you want to do with the values

class CompanyUserObserver < ActiveRecord::Observer

  def after_save(company_user)
    user = company_user.user
    company = company_user.company
    ...do what you want
  end

  def before_destroy(company_user)
    ...do what you want
  end
end

You can user multiple callback in according your needs.

#2 Keep records:

It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.

Sorry for the confusion.

Upvotes: -1

Related Questions