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.
Related
I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!
Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!
I have a Recipes model and an Ingredients model. Ingredients belongs_to Recipes and Recipes has_many Ingredients. Everything is working fine. Nested attributes are being updated.
But I have one attribute on my nested model that I want to manipulate before it gets stored in the DB. I have the code in the IngredientsController update method to handle this.
I was expecting that the RecipeController's update method would call the IngredientController's update method when updating the nested Ingredients. This obviously doesn't happen.
What mechanism can I use to manipulate the nested model objects during the update process?
MORE DETAIL:
I'm storing the ingredient quantity in the database as a float
(1.25, 0.33333, 3.5, 4.0, etc ).
I want the user to be able to see and edit the value as a sloppy fraction
(1 1/4, 1/3, 3 1/2, 4 ).
So I wrote String.to_f_sloppy and Float.to_s_sloppy functions to manipulate the numbers. On display, there is no problem, I just use ingredient.quantity.to_s_floppy. But when someone edits the ingredient and changes the quantity, the value is already changed (like it was run through to_f) when it gets to the before_update and before_validation functions.
Here are the parameters that come through:
Parameters: {"utf8"=>"√", "authenticity_token"=>"ojOwp68P3ObiufowfKtbfYxpV31+vZPz64qYQAL/1ld8Px93OrDX2Gvy/yxljENJOhiLW3DUoE0C2upvHuF3CA==", "recipe"=>{"user_id"=>"1", "name"=>"Chicken Piccata", "description"=>"", "category"=>"entree", "yield"=>"4.0",
"ingredients_attributes"=>{"0"=>{"recipe_id"=>"12",
"name"=>" skinless and boneless chicken breasts",
"unit_id"=>"40",
"quantity"=>"2 1/2",
"comment"=>"",
"_destroy"=>"0", "id"=>"122"}}}, "commit"=>"Save Recipe", "id"=>"12"}
In ingredients.rb, I have:
class Ingredient < ActiveRecord::Base
belongs_to :unit
belongs_to :recipe
before_update :translate_quantity
def translate_quantity
puts "UPDATE QUANTITY IS #{self.quantity}"
end
end
This outputs:
UPDATE QUANTITY IS 2.0
So before I can get to the value, it has been changed.
You can use Activerecord's callbacks, these are implemented on the model and it let you modify the record using them.
The one you want is the before_update callback.
More info here.
Something like this.
class Ingredient < ActiveRecord::Base
...
before_update :stuff
private
def stuff
# do stuff
end
end
I found the solution. I used this answer:
How can I override the attribute assignment in an active record object?
To override the behavior in the model:
def quantity=(value)
super(value.to_f_sloppy)
end
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
I have two tables:
stores
raw_stores_data
The raw_stores_data is received from a third party daily.
I'd update certain fields of the stores model if those fields have been modified for that record in raw_stores_data.
Currently I have a bunch of conditional statements that check each of those fields. Is there any better way to code this?
new_data = raw_stores_data.all.select do |item|
item.store_id.present?
end
new_data.each do |item|
if item.field1 != item.stores.field1
...
...
...
# update record with hash of fields to update created above
end
You could add an association and special mutators to the 'raw' model that know how manipulate the 'stores' object. This serves to keep the model code in the model. Thin controller, comprehensive models, etc.
class Store < ActiveRecord::Base
has_one :raw_stores_data
end
class RawStoresData < ActiveRecord::Base
belongs_to :store
def field1=(value)
store.field1 = value
store.save!
field1 = value
end
end
I'm hand waving at some of the details, and you might want to reverse the direction of the association or make it go both directions.
EDIT:
You would use this as such:
raw_data = RawStoreData.find(param[:id]) # or new or however you get this object
raw_data.field1 = param[:field1]
The act of assigning will use the 'field1=' method, and make the change to the associated store object. If you're worried about saving unnecessarily, you could conditionalize in that method to only save if the value changed.
I hope this is clearer.