How to determine if association changed in ActiveRecord? - ruby-on-rails

ActiveRecord offers change tracking, where calling #name_changed? returns true/false depending on whether the name attribute changed between when the model was loaded and now.
Is there an equivalent for associations? I am specifically looking for has_many associations, but all association types would be useful.
Rails 5.2, Ruby 2.5.1

Ok, not a answer but this is a workaround I came up with.
In my project I was already using the gem audited to keep track of changes on the model I also want to receive changes on. In my case the model is Location and the model I want to check if the location was changes is Employee
In the after_save I then check if there is made an audit on the location and if it's created within seconds. If so, I'm able to check the changes.
Simplified example:
# app/models/location.rb
class Location < ApplicationRecord
audited
end
# app/models/employee.rb
class Employee < ApplicationRecord
after_save :inform_about_changes!
belongs_to :location
def inform_about_changes!
last_location_change = location.audits.last
if last_location_change.created_at.between?(2.seconds.ago, Time.now)
if last_location_change.audited_changes.include? 'city'
city_was = last_location_change.audited_changes[0]
end
end
end
end
Once again, this is not an answer, but it did the job in my case

there is no #association_changed? equivalent on associations as far as the documention shows.
https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html

has_many - nope
but if we talk about belongs_to :user
you can use something like this object.saved_change_to_user_id?

Related

Model column that depends on other columns

I have a gamification app that has four types of points, and the sum of all these kinds is the total points for a user, I want to be able to do sum and scopes on that column, so I think I should have it as a column in the DB.
scope :points_rank, -> { order(points: :desc) }
I was using a before_save for adding all four point types and storing it in points, but now I'm using a gem that does increment to these types of points, so when it updates those values, the before_save is not called, hence not updating the points value as expected.
What is the correct ActiveRecord callback to be using instead of before_save, or what else could I be doing to keep the column updated.
Try using the after_touch callback instead.
after_touch callback is triggered whenever an object is touched.
So, whenever point type changes, it should update the points.
First of all, counter_culture seems to be a way to enhance the counter_cache functionality of rails...
Used to cache the number of belonging objects on associations. For example, a comments_count column in a Post class that has many instances of Comment will cache the number of existent comments for each post.
It might not be exactly what you want, judging from your question.
Okay I get it. You're using points in your User model to create a "cached" column which can be used for wider application functionality. Okay that's cool...
--
Your setup, then, will look something like this (you were manually setting the counter_cache column, and now the gem handles it):
#app/models/user.rb
class User < ActiveRecord::Base
counter_cache :points
end
#app/models/point.rb
class Point < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
The question is then that when you update the points model, you need to be able to update the "cached" column in the users model, now without any callbacks.
What is the correct ActiveRecord callback to be using instead of before_save
I'm presuming you're calling before_save on your User model (IE adding the associated data and putting the points column?
If so, you should try using a callback on the Point model, perhaps something like this:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :points
end
#app/models/point.rb
class Point < ActiveRecord::Base
belongs_to :user, inverse_of: :points
after_commit :update_user
private
def update_user
if user?
user.update(points: x + y + z)
end
end
end
--
Oberservers
If you have real problems, you could look at ActiveRecord observers.
Here's an answer I wrote about it: Ruby On Rails Updating Heroku Dynamic Routes
Whether this will trigger without any callbacks is another matter, but what I can say is that it will work to give you functionality you may not have had access to otherwise:
#config/application.rb (can be placed into dev or prod files if required)
config.active_record.observers = :point_observer
#app/models/point_observer.rb
class PointObserver < ActiveRecord::Observer
def before_save(point)
#logic here
end
end
A good way to test this would be to use it (you'll have to use the rails-observers gem) with different methods. IE:
#app/models/point_observer.rb
class PointObserver < ActiveRecord::Observer
def initialize(point)
#if this fires, happy days
end
end

Rails ActiveRecord Query - How to create a complex, multi-relation conditional query?

I was trying to figure out how to query my database using a custom method within my model. As far as I can tell, this is not possible, so I'm looking for alternative ideas.
The challenge is that I need to find all records of a model where at least one relational condition is true. For example, lets say that I have a Notebook record and I need to find all instances of Notebook that had at least one of their related objects modified. Let's say a Notebook has a front_cover, back_cover, interior_pages, and interior_folders. Within my Notebook model class I have this method:
class Notebook < ActiveRecord::Base
has_one :front_cover
has_one :back_cover
has_many :interior_pages
has_many :interior_folders
...
def modified?
if !front_cover.modified? or !back_cover.modified? or !interior_pages.empty? or !interior_folders.empty?
return true
end
false
end
...
end
The modified methods within front_cover and back_cover may have their own relationships that they will check as well:
class FrontCover < ActiveRecord::Base
has_one :cover_graphic
has_many :ring_holes
belongs_to :notebook
...
def modified?
modified = false
if (!cover_graphic.nil? and !cover_graphic.url.nil?) or !ring_holes.empty?
modified = true
end
modified
end
end
I'm really not sure how I can construct such a conditional check as a query. Any ideas?

Can I disable touch for belongs_to association temporarily when I save a model?

I have two model like this:
class Topic < ActiveRecord::Base
has_many :replies
end
class Reply < ActiveRecord::Base
belongs_to :topic, touch: true
end
When I save reply with reply.save!, touch on topic will be called and updated_at of topic will be updated to current time.
But sometimes I don't want to change updated_at automaticlly when I created a reply. So, how to disable touch when I save reply?
I'd suggest the opposite to be explicit, only touch in cases that you want to. Testing for the negative will quickly become unmanageable and could be hard to pick up by those unfamiliar with the code.
You can achieve a simple touch by calling topic.touch
This is a old question, but in case you have to skip touching records and you know what you are doing (linke in migration) you could use:
ActiveRecord::Base.no_touching do
Project.first.touch # does nothing
Message.first.touch # does nothing
end
from here: https://api.rubyonrails.org/classes/ActiveRecord/NoTouching/ClassMethods.html#method-i-no_touching

where constraint on a related record

I'm not getting a concept (nothing new there) on how to scope a Active Record query. I want to only receive the records where there is a certain condition in a related record. The example I have happens to be polymorphic just in case that is a factor. I'm sure there is somewhere where this is explained but I have not found it for whatever reason.
My Models:
class User < ActiveRecord::Base
belongs_to :owner, polymorphic: true
end
class Member < ActiveRecord::Base
has_one :user, as: :owner
end
I want to basically run a where on the Member class for related records that have a certain owner_id/owner_type.
Lets say we have 5 Members with ids 1-5 and we have one user with the owner_id set to 3 and the owner_type set to 'Member'. I want to only receive back the one Member object with id 3. I'm trying to run this in Pundit and thus why I'm not just going at it form the User side.
Thanks for any help as always!!!
Based on your comment that you said was close I'd say you should be able to do:
Member.joins(:user).where('users.id = ?', current_user.id)
However based on how I'm reading your question I would say you want to do:
Member.joins(:user).where('users.owner_id = ?', current_user.id)
Assuming current_user.id is 3.
There may be a cleaner way to do this, but that's the syntax I usually use. If these aren't right, try being a little more clear in your question and we can go from there! :)

How can I access ActiveRecord Associations in class callbacks in rails?

Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?

Resources