So in order to reduce duplication, I broke image attachments away from a Product model onto an Image model, so that similar products could share an image. Like so
class Product < ActiveRecord::Base
has_many :imaged_items
has_many :images, :through => :imaged_items
end
class ImagedItems < ActiveRecord::Base
belongs_to :product
belongs_to :image
end
class Image < ActiveRecord::Base
has_many :imaged_items
has_many :products, :through => :imaged_items
end
What I would like to do is if I'm editing a product, I'd like to check whether the image has changed before I save it. If the image has changed, I don't want to update the existing record, I want to create a new one since the old image could be used by multiple Products.
Is there any way to do that? I'm stumped.
GO Through Class: ActiveRecord::Dirty ...You can use changed
person.changed # => []
person.name = 'bob'
person.changed # => ['name']
####OR ..other way as well
person.name = 'Bill'
person.name_changed? # => false
person.name_change # => nil
I had the problem in an after_update callback and I use .dirty? instead of thumb_changed?
after_update :foo, if: Proc.new{
|model| model.title_changed? || model.summary_changed? || model.thumb.dirty?
}
Worked for me perfectly. Hope it helps
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've been looking for this kind of functionality in AR, but don't seem able to find it. The Dirty implementation of AR states that an already persisted instance is deemed only dirty if one of its direct attributes has changed. So, let's say:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
end
in this case, I can do something like:
p = Picture.new(:gallery => Gallery.new, :frame => Frame.new)
p.save #=> it will save the three instances
p.gallery = Gallery.new
p.save #=> will not save the new gallery
p.gallery_id_will_change!
p.gallery = Gallery.new
p.save #=> this will save the new Gallery
but now I can't do something similar for the has_one association, since the Picture implementation doesn't own an attribute referring to it. So, it seems such dirty markups are impossible. Or aren't they?
Best thing I can figure out to do is:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
after_save :force_save_frame
def force_save_frame
frame.save! if frame.changed?
end
end
Like weexpectedTHIS said Dirty flags are set for attributes on the model itself, not for related objects. It only works for the belongs_to because there is a foreign key on the model.
But you can make a trick like that
module FrameDirtyStateTracking
def frame=(frame)
attribute_will_change!('frame')
super
end
end
class Picture < ActiveRecord::Base
prepend FrameDirtyStateTracking
belongs_to :gallery
has_one :frame
end
picture = Picture.new
picture.frame = Frame.new
picture.changed? # => true
picture.changed # => ['frame']
Is there any way to halt saving of child before parent.
I am using accepts_nested_attributes_for with polymorphic association.
I used multiple options validates_presence_of :parent_id , validates_assoicated :parent but none are working.
For example, I do have a class
Class Person
include HasPhoneNumbers
..
end
module HasPhoneNumbers
def self.included(kclass)
kclass.has_many :phone_numbers, :as => :callable, :dependent => kclass == Person ? :destroy : :nullify
end
klass.accepts_nested_attributes_for :phone_numbers, :reject_if => lambda {|pn| pn.keys.any?{|k| k.to_sym != :id && pn[k].blank?} }
end
class PhoneNumber
belongs_to :callable, :polymorphic => true
end
So while saving person due to validation in person object, it was not saving. However, child(phone_number) was saving. So I need to restrict it to not save child(phone_number) before parent(person) saves.
I did try multiple options using validates_presence_of and validates_associated, but none are working for me.
#person = Person.new(params[:person])
ActiveRecord::Base.transaction do
person.save!
end
Wrapping your saves within a transaction should roll back the phone number save if the person fails validation.
Reference: ActiveRecord Transactions
I am trying to associate existing records while still being able to add new ones. The following does not work but is pretty close to what I need. How can I accomplish associating existing records and creating new ones?
has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true
def autosave_associated_records_for_comments
comments.each do |comment|
if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
self.comments.reject! { |hl| hl.fax == existing_comment.fax && hl.name == existing_comment.name }
self.comments << existing_comment
else
self.comments << comment
end
end
end
Here is a relevant line of source: https://github.com/rails/rails/blob/v3.0.11/activerecord/lib/active_record/autosave_association.rb#L155
I've made a solution, but if you know of a better way to do this please let me know!
def autosave_associated_records_for_comments
existing_comments = []
new_comments = []
comments.each do |comment|
if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
existing_comments << existing_comment
else
new_comments << comment
end
end
self.comments << new_comments + existing_comments
end
I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_record_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || StateTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.
In Rails 6 (and probably earlier versions too), it's easy to overwrite the attributes writer generated by accepts_nested_attributes_for and use find_or_initialize_by.
In this case, you could simply write:
has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true
def comments_attributes=(hashes)
hashes.each { |attributes| comments << Comment.find_or_initialize_by(attributes)
end
Haven't tested this when passing :id or :_destroy keys, as it doesn't apply in my situation, but feel free to share your thoughts or code if you do.
I would love to see Rails implement this natively, perhaps by passing an upsert: true option to accepts_nested_attributes_for.
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