Having trouble with :dependent => :destroy and Instance variables - ruby-on-rails

I have a project with many items; and it's :dependent => :destroy.
I'm trying to tell rails when calling callbacks (specifically the after_destroy of Item), to run ONLY if the Item is destroyed "alone", but all of the project is NOT being destroyed.
When the whole project is being destroyed, I actually don't need this after_destroy method (of Item) to run at all.
I don't want to do :dependent => :delete since the Item has many other associations connected to it (with :dependent => :destroy).
It works for me only with class variable, but I wish it would had worked with an instance variable:
class Project < ActiveRecord::Base
has_many :items, :dependent => :destroy
before_destroy :destroying_the_project
def destroying_the_project
# this is a class variable, but I wish I could had #destroying_me
# instead of ##destroying_me.
##destroying_me = true
end
def destroying_the_project?
##destroying_me
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses
def update_related_statuses
# I with I could had return if project.destroying_the_project?
# but since the callback gets the project from the DB, it's another instance,
# so the instance variable is not relevant here
return if Project::destroying_the_project?
# do a lot of stuff which is IRRELEVANT if the project is being destroyed.
# this doesn't work well since if we destroy the project,
# we may have already destroyed the suites and the entity
suite.delay.suite_update_status
entity.delay.update_last_run
end
end
The other option I can think of is remove the :dependent => :destroy and manually handle the destroy of the items inside the Project after_destroy method, but it seems too ugly as well, especially since Project has many item types with :dependent => :destroy that would have to shift to that method.
Any ideas would be appreciated

I hope that's not the best solution, but at least it works and doesn't introduce any global state via class variables:
class Project < ActiveRecord::Base
has_many :items
before_destroy :destroying_the_project
def destroying_the_project
Rails.logger.info 'Project#destroying_the_project'
items.each &:destroy_without_statuses_update
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses,
:unless => :destroy_without_statuses_update?
def update_related_statuses
Rails.logger.info 'Item#update_related_statuses'
end
def destroy_without_statuses_update
#destroy_without_statuses_update = true
destroy
end
def destroy_without_statuses_update?
!!#destroy_without_statuses_update
end
end

If you don't need to use callbacks when deleting the whole project, you could use delete_all instead of destroy:
Rails :dependent => :destroy VS :dependent => :delete_all

Related

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:
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.

after_create doesn't have access to associated records created during before_created callback?

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

Why relative position of associations and callbacks in a rails model affecting the results

I have a model Indent which has many indent_items and has many shipments through indent_items.
class Indent < ActiveRecord::Base
before_destroy :check_for_shipments # WORKS HERE
has_many :indent_items, inverse_of: :indent, :dependent => :destroy
has_many :shipments, :through => :indent_items
# before_destroy :check_for_shipments # DOESN"T WORK HERE
private
def check_for_shipments
# Should not be allowed to delete if there are any shipments.
if self.shipments.count > 0
errors.add(:base, "Cannot delete indent because shipments are there.")
return false
end
end
end
I guess it may be because if callback is mentioned after the association, all indent items gets marked for deletion and the shipment count check returns zero always.
But this shouldn't happen. Or may be I am missing something here. I don't know.
I am using rails 3.2.8.
This is the desired behaviour of rails and if you want that before_destroy callback is executed before the shipment has destroyed(because of dependent: :destroy option) then you have to use the prepend option on the before_destroy callback.
e.g.
class Indent < ActiveRecord::Base
has_many :indent_items, inverse_of: :indent, :dependent => :destroy
has_many :shipments, :through => :indent_items
before_destroy :check_for_shipments, prepend: true #this will work
private
def check_for_shipments
# Should not be allowed to delete if there are any shipments.
if self.shipments.count > 0
errors.add(:base, "Cannot delete indent because shipments are there.")
return false
end
end
end
And for the further reference you can read from here.

callbacks on active record associations

I have a vacation approval model that has_many :entries is there a way that if I destroy one of those entries to have the rest destroyed? I also want to send one email if they are, but not one for each entry. Is there a way to observe changes to the collection as a whole?
A callback probably isn't a good choice because:
class Entry < ActiveRecord::Base
def after_destroy
Entry.where(:vacation_id => self.vacation_id).each {|entry| entry.destroy}
end
end
would produce some bad recursion.
It could be that you should do it in the controller:
class EntriesController < ApplicationController
def destroy
#entry = Entry.find(params[:id])
#entries = Entry.where(:vacation_id => #entry.vacation_id).each {|entry| entry.destroy}
#send email here
...
end
end
You can use the before_destroy callback.
class VacationRequest < ActiveRecord::Base
has_many :entries
end
class Entry < ActiveRecord::Base
belongs_to :vacation_request
before_destroy :destroy_others
def destroy_others
self.vacation_request.entries.each do |e|
e.mark_for_destruction unless e.marked_for_destruction?
end
end
end
Definitely test that code before you use it on anything important, but it should give you some direction to get started.
I think this ought to work:
class Entry < ActiveRecord::Base
belongs_to :vacation_request, :dependent => :destroy
# ...
end
class VacationApproval < ActiveRecord::Base
has_many :entries, :dependent => :destroy
# ...
end
What should happen is that when an Entry is destroyed, the associated VacationApproval will be destroyed, and subsequently all of its associated Entries will be destroyed.
Let me know if this works for you.
So What i ended up doing is
class VacationApproval < ActiveRecord::Base
has_many :entries , :conditions => {:job_id => Job.VACATION.id }, :dependent => :nullify
class Entry < ActiveRecord::Base
validates_presence_of :vacation_approval_id ,:if => lambda {|entry| entry.job_id == Job.VACATION.id} , :message => "This Vacation Has Been Canceled. Please Delete These Entries."
and then
#entries.each {|entry| entry.destroy if entry.invalid? }
in the index action of my controller.
and
`raise "Entries are not valid, please check them and try again ( Did you cancel your vacation? )" if #entries.any? &:invalid?`
in the submit action
The problem with deleting the others at the same time is if my UI makes 10 Ajax calls to selete 10 rows, and it deletes all of them the first time I end up with 9 unahandled 404 responses, which was undesirable.
Since I don't care it they remain there, as long as the Entry cannot be submitted its OK.
This was the easiest / safest / recursion friendly way for me, but is probably not the best way. Thanks for all your help!
To anyone curious/ seeking info
I ended up solving this later by setting The Vacation APProval model like this
class VacationApproval < ActiveRecord::Base
has_many :entries , :conditions => {:job_id => Job.VACATION.id }, :dependent => :delete_all
end
and My Entry Model like this
class Entry < ActiveRecord::Base
after_destroy :cancel_vacation_on_destory
def cancel_vacation_on_destory
if !self.vacation_approval.nil?
self.vacation_approval.destroy
end
end
end
Using :delete_all does not process callbacks, it just deletes them

My "has_many through" join model has nil reference after saving

I'm trying to create an object and adding an existing object to a "has_many through" association, but after saving my object the reference to my newly created object is set to nil in the join model.
To be specific, I'm creating a Notification object and adding a pre-existing Member object to the Notification.members association. I'm using nested resources and I'm invoking the notification controller's new function using the following relative URL:
/members/1/notifications/new
After filling out the form and submitting, the create function is called, and from what I understand from the Rails Associations guide, section 4.3.3 "When are Objects Saved?", the members associations should be created in the database when the new notification object is saved:
"If the parent object (the one declaring the has_many association) is unsaved (that is, new_record? returns true) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved."
After creating the notification object, the following record was created in the database:
select id, notification_id, notifiable_type, notifiable_id from deliveries;
1|<NULL>|Member|1
I worked around the problem by saving the notification object before adding the member object to the association. At first this seemed to be an ok solution for now, but I soon discovered that this has it's downsides. I don't want to save the notification without it's member association since I then have to write workarounds for my callbacks so that they don't start performing tasks on the not yet valid notification object.
What am I doing wrong here? All tips are appreciated. :D
Models
class Notification < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :members, :through => :deliveries, :source => :notifiable, :source_type => "Member"
has_many :groups, :through => :deliveries, :source => :notifiable, :source_type => "Group"
end
class Member < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
class Delivery < ActiveRecord::Base
belongs_to :notification
belongs_to :notifiable, :polymorphic => true
end
# Group is not really relevant in this example.
class Group < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
Controller
class NotificationsController < ApplicationController
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
#notification.members << #member
respond_to do |format|
if #notification.save
...
end
end
end
end
After posting a bug report, I got som help from one of the Rails gurus. In short, it's impossible to get this working the way I thought.
I decided to proceed with slightly more controller code, seems to be working just fine:
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
respond_to do |format|
if #notification.save
#member.notifications << #notification
#member.save
...

Resources