Have below association in author class
has_many :books,
class_name :"Learning::Books",
through: :elearning,
dependent: :destroy
with after_commit as,
after_commit :any_book_added?, on: :update
def any_book_added?
book = books.select { |book| book.previous_changes.key?('id') }
# book's previous_changes is always empty hash even when newly added
end
Unable to find the newly added association with this method. Is this due to class_name?
Rails has a couple methods that might help you, before_add and after_add
Using this, you can define a method to set an instance variable to true
class Author < ApplicationRecord
has_many :books, through: :elearning, after_add: :new_book_added
def any_book_added?
#new_book_added
end
private
def new_book_added
#new_book_added = true
end
end
Then when you add a book to an author, the new_book_added method will be called and you can at any future time ask your Author class if any_book_added?
i.e.
author = Author.last
author.any_book_added?
=> false
author.books = [Book.new]
author.any_book_added?
=> true
As you can see, the callback method new_book_added can accept the book that has been added as well so you can save that information.
Related
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:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
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
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.
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.
I am running into a weird issue, and reading the callbacks RoR guide didn't provide me an answer.
class User < ActiveRecord::Base
...
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
has_many :user_teams, dependent: :destroy
has_many :teams, through: :user_teams
before_create :check_company!
after_create :check_team
def check_company!
return if self.companies.present?
domain = self.email_domain
company = Company.find_using_domain(domain)
if company.present?
assign_company(company)
else
create_and_assign_company(domain)
end
end
def check_team
self.companies.each do |company|
#do stuff
end
end
...
end
The after_create :check_team callback is facing issues because the line
self.companies.each do |company|
Here, self.companies is returning an empty array [] even though the Company and User were created and the User was associated with it. I know I can solve it by making it a before_create callback instead. But I am puzzled!
Why does the after_create callback not have access to self's associations after the commit?
Solution: Please read my comments in the accepted answer to see the cause of the problem and the solution.
inside before_create callbacks, the id of the record is not yet available, because it is before... create... So it is not yet persisting in the database to have an id. This means that the associated company_user record doesn't have a user_id value yet, precisely because the user.id is still nil at that point. However, Rails makes this easy for you to not worry about this "chicken-and-egg" problem, provided that you do it correctly:
I recreated your setup (Company, User, and CompanyUser models), and the following is what should work on your case (tested working):
class User < ApplicationRecord
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
before_create :check_company!
after_create :check_team
def check_company!
# use `exists?` instead of `present?` because `exists?` is a lot faster and efficient as it generates with a `LIMIT 1` SQL.
return if companies.exists?
## when assigning an already persisted Company record:
example_company = Company.first
# 1) WORKS
companies << example_company
# 2) WORKS
company_users.build(company: example_company)
## when assigning and creating a new Company record:
# 1) WORKS (this company record will be automatically saved/created after this user record is saved in the DB)
companies.build(name: 'ahaasdfwer') # or... self.companies.new(name: 'ahaasdfwer')
# 2) DOES NOT WORK, because you'll receive an error `ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved`
companies.create(name: 'ahaasdfwer')
end
def check_team
puts companies.count
# => 1 if "worked"
puts companies.first.persisted?
# => true if "worked"
end
end
There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end
on a Ruby on Rails project I'm trying to access association objects on an ActiveRecord prior to saving everything to the database.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
def item_ids=(ids)
ids.each do |item_id|
purchase_items.build(item_id: item_id)
end
end
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
If I build out my object like so:
purchase = Purchase.new(item_ids: [1, 2, 3]) and try to save it the item_validation method doesn't have the items collection populated yet, so even though items have been set set it doesn't get a chance to call the check_something method on any of them.
Is it possible to access the items collection before my purchase model and association models are persisted so that I can run validations against them?
If I change my item_validation method to be:
def item_validation
purchase_items.each do |purchase_item|
item = purchase_item.item
## Lookup something with the item
if item.something
errors.add :base, "Error message"
end
end
end
it seems to work the way I want it to, however I find it hard to believe that there is no way to directly access the items collection with rails prior to my purchase and associated records being saved to the database.
Try to adding the argument inverse_of: in the has_many and belongs_to definitions. The inverse_of argument it's the name of the relation on the other model, For example:
class Post < ActiveRecord::Base
has_many :comments, inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
end
Don't forget to add it also on the other classes, such as PurchaseItem and Item
Hope it helps
Remove your own item_ids= method - rails generates one for you (see collection_singular_ids=ids). This might already solve your problem.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
The second thing that comes in my mind looking at your code: Move the validation to the Item class. So:
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
end
class Item < ActiveRecord::Base
has_many :purchase_items
has_many :purchases, through: :purchase_items
validate :item_validation
private
def item_validation
if check_something
errors.add :base, "Error message"
end
end
end
Your Purchase record will also be invalid if one of the Items is invalid.
Do you have documentation that indicates purchase = Purchase.new(item_ids: [1, 2, 3]) does what you're expecting?
To me that looks like you are just setting the non-database attribute 'item_ids' to an array (i.e. not creating an association).
Your Purchase model should not even have any foreign key columns to set directly. Instead there are entries in the purchase_items table that have a purchase_id and item_id. To create a link between your purchase and the three items you need to create three entries in the joiner table.
What happens if you just do this instead?:
purchase = Purchase.new
purchase.items = Item.find([1,2,3])
You can use model.associations = [association_objects] and an Association Callback http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
I assume you can't access them because id of Purchase isn't available before record saved. But as you mention you have access to first level association purchase_items, so you can extract all ids and pass them in where for Item:
items = Item.where(purchase_item_id: purchase_items.map(&:id))
Now I hava a problem,how can I make the callback#after_add receive a reference to the join model in a has_many :through association?
my code like this:
class Emergency
has_many :departments, :through => :eme_references, :after_add => Proc.new { |eme_reference| eme_reference.eme_flag = 1}
end
the attribute eme_flag is the model EmeReference's attribute! but in the block ,i get the eme_reference.class is Emergency.
I want to set the attribute eme_flag of the model EmeReference.
That is my question!
cheers!
Presumably Emergency also has_many :eme_references in order for the :through association to work?
In that case, you should be able to attach the callback there:
has_many :eme_references,
:after_add => Proc.new { |emergency, eme_ref| # code here }
The block accepts 2 parameters, the first will be the Emergency, the 2nd will be the EmeReference being added.
Perhaps a before_save callback on EmeReference can also do what you want in this instance?
I think what you want to do can't be done there.
You could create an after_create hook on departments (I'm assuming Emergency has_many eme_references has_many departments):
class Emergency
has_many :departments, :through => :eme_references
def flag!
eme_flag=1
save
end
end
class Department
after_create :check_emergency
# this allows you to call department.emergency. Will return nil if anything is nil
delegate :emergency, :to =>:eme_reference, :allow_nil => true
def check_emergency
self.emergency.flag! if self.emergency.present?
end
end