Model column that depends on other columns - ruby-on-rails

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

Related

How to determine if association changed in ActiveRecord?

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?

Rails ActiveRecord fire action after after_save method is done

In my Rails 4 Postgres applicaiton I have a Project model and a WorkingDay model with a has_many relationship between them:
working_day.rb
class WorkingDay > ActiveRecord::Base
# working_times: text that is parsed into yml json format
belongs_to :project
end
and project.rb
class Project > ActiveRecord::Base
has_many :working_days
# total_cost: decimal based on calculations of working_times of each working_day
before_save :calculate_total_cost
def calculate_total_cost
# carry out certain calculations based on working_days of this project
self
end
def regen_working_days(wd_json_data)
self.working_days.each(&:destroy)
wd_json_data.each do |wd_data|
self.working_days.create(wd_data)
end
self.save!
self
end
end
The issue I am having is that once the regen_working_days is fired with the correct wd_json_data parameters, the total_cost is not calculated correctly. It is actually calculated based on values of previous working_days, which means that in the instance this method is fired, the new working_day objects are not yet created and the old ones are still there.
Is there any way to make the self.save! part of the regen_working_days method be fired only after all other transactions are finished? Or am I generally wrong in the way I handle nested objects and the transactions associated with them with my approach?

Rails: set a value upon submit based on nested models values

Suppose in my rails application I have a model Entry, which has a nested model Measures, such that each entry has_many measures (and measures belongs_to entry).
Each measure has its own incentive. Is it possible that Entry has an integer also named incentive, whose value is equal to the sum of all of its measures? How do you achieve this?
To me, it seems like this kind of becomes a two part question:
How to make a models field, upon submission, be defined based on another fields value? Then.. How to make a value, upon submission, be defined based on its nested models values?
Try implement a callback using after_update in the model of the nested attributes, which updates its parent:
class Measure < ActiveRecord::Base
after_update :calculate_measure_sum
...
private
def calculate_measure_sum
# calculate sum
self.entry.save
end
end
You might need to use the same method on the after_create callback as well.
EDIT:
After having read about touch in another question, I'd like to update my approach:
class Entry < ActiveRecord::Base
has_many :measures
after_touch :calculate_measure_sum
...
private
def calculate_measure_sum
# calculate sum
self.entry.save
end
end
class Measure < ActiveRecord::Base
belongs_to :entry, touch: true
...
end
What happens here, is that everytime a Measure is created or edited, it informs its Entry that it is updated by calling its touch method. In the entry, we may use the callback after_touch in order to recalculate the sum of the measures. Note that the after_touch-callback is called on creation, deletion and modification of the measures.
Compared to my previous approach, this approach puts the responsability on the Entry-objects, which is favourable from a design point-of-view.

At what level in a model object does ActiveRecord not load associated objects

I have a couple of models that are composites of multiple objects. I basically manage them manually for saves and updates. However, when I select items out, I don't have access to the associated properties of said item. For example:
class ObjectConnection < ActiveRecord::Base
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |value, key|
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
return r
end
end
and each item:
class Item < ActiveRecord::Base
has_many :assets, :as => :assetable, :dependent => :destroy
When I use the ObjectLocation.find_three_by_location_id, I don't have access to assets whereas if use Item.find(id) in most other situations, I do.
I tried using includes but that didn't seem to do it.
thx
Sounds like the simplest solution would be to add methods to your ObjectConnection model for easy access like so:
class ObjectConnection < ActiveRecord::Base
def engine
Engine.find(engine_id)
end
def chassis
Chassis.find(chassis_id)
end
# rest of class omitted...
I'm not exactly sure what you're asking... If this doesn't answer what you're asking, then can you try to be a little bit more clear with what exactly you are trying to accomplish? Are the Chassis and Engine mdoels supposed to be polymorphic associations with your Item model?
Also, the code you're using above won't work due to the fact that you are trying to dynamically set properties on a model. It's not your calls to Item.find that are failing, it's your calls to value[:engine_item]= and value[:chassis_item] that are failing. You would need to modify it to be something like this if you wanted to keep that flow:
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |obj_conn, key|
# at this point, obj_conn is an ActiveRecord object class, you can't dynamically set attributes on it at this point
value = obj_conn.attributes # returns the attributes of the ObjectConnection as a hash where you can then add additional key/value pairs like on the next 2 lines
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
r
end
But I still think that this whole method seems unnecessary due to the fact that if you setup proper associations on your ObjectConnection model to begin with, then you don't need to go and try to handle the associations manually like you're attempting to do here.

Ruby on Rails Increment Counter in Model

I'm attempting to increment a counter in my User table from another model.
class Count < ActiveRecord::Base
belongs_to :user
after_create :update_count
def update_count
user = User.find(self.user_id)
user.increment(:count)
end
end
So when count is created the goal would be to increment a counter column for that user. Currently it refuses to get the user after creation and I get a nil error.
I'm using devise for my Users
Is this the right (best practice) place to do it? I had it working in the controllers, but wanted to clean it up.
I'm very inexperienced with Model callbacks.
If User has many Counts and Count belongs to User (like it seems to be), then you might want to use a counter cache. It does exactly what you want to do, and it is built-in into ActiveRecord.
I think a better place for this would be using an observer that listens for the on_create for User objects, and then runs this logic.
Something like:
class UserObserver < ActiveRecord::Observer
def after_create(user)
Counter.find_by_name("user_count").increment
end
end
If you would like more extensible counter caches, check out counter_culture. It supports basic counter cache functionality, but also allows you to create counters of records that meet various conditions. For example, you could easily create an inactive user count with code like this:
class Product < ActiveRecord::Base
belongs_to :category
counter_culture :category, :column_name => \
Proc.new {|model| model.inactive? ? 'inactive_count' : nil }
end

Resources