In Rails is there a way that I can make a method call itself based on a change in the database? For instance, lets say I have two classes: Products and Orders.
Orders have three possible enum values:
class Order < ActiveRecord::Base
enum status: [:pending, :processing,:shipped]
belongs_to :products
end
I would like to batch process Orders so when a product has 50 orders, I want it to set all Orders associated with it to processed. Orders default to :pending. To change an order to :processing I would call order.processing!. I could write a method into the Products model like:
def process_orders
if self.orders.count=50
self.orders.each do |order|
order.processing!
end
end
The problem with this is that I would have to call the process_orders method for it to execute, is there anyway I could make it automatically execute once a product has 50 orders?
This is sounds like a good opportunity to use an Active Record Callback.
class Order < ActiveRecord::Base
belongs_to :product
after_save do
product.process_orders if product.pending_threshold_met?
end
end
class Product < ActiveRecord::Base
has_many :orders
def pending_threshold_met?
orders.where(status: :pending).count >= 50
end
end
I think you can use update_all to update the status column of all of your orders at once rather looping through them one by one:
self.orders.update_all(status: :processing)
and wrap that inside a callback.
Something like this:
class Order < ActiveRecord::Base
after_save do
product.process_orders if product.has_fifty_pending_orders?
end
# rest of your model code
end
class Product < ActiveRecord::Base
# rest of your model code
def process_orders
self.orders.update_all(status: :processing)
end
def has_fifty_pending_orders?
self.orders.where(status: :pending).count >= 50
end
end
Related
In my app controller looks pretty simple:
class ProductsController
before_action :set_company
def index
#products = company.products.includes(:shipment)
end
private
def set_company
#company = Company.find(params[:company_id])
end
end
But what I worry about is the #product inside Index action was properly declared? What if I'll have millions of products? is there more efficient solution?
Model relations below:
class Products < ApplicationRecord
belongs_to :company
has_many :shipment
end
class Company < ApplicationRecord
has_many :products
end
class Shipment < ApplicationRecord
belongs_to :products
end
There is definitely a problem if you have million of records because your query is going to be too big, in this case you can add pagination in any flavor or use any other strategy that reduce the number of records queried each time.
To do pagination in Rails you can use https://github.com/kaminari/kaminari but this is not the only strategy available to do this.
In my Rails 4 app I have the following models:
class Invoice < ActiveRecord::Base
has_many :allocations
has_many :payments, :through => :allocations
end
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
end
class Payment < ActiveRecord::Base
has_many :allocations, :dependent => :destroy
has_many :invoices, :through => :allocations
after_save :update_invoices
after_destroy :update_invoices # won't work
private
def update_invoices
invoices.each do |invoice|
invoice.save
end
end
end
The problem is that I need to update an invoice when one of its payments gets destroyed.
The update_invoices callback above obviously can't ever get triggered because at the time it gets called the connection with the invoice has already been destroyed.
So how can this be done?
Right now, I am doing this in my PaymentsController:
def destroy
#payment.destroy
current_user.invoices.each do |invoice|
invoice.save
end
...
end
However, this is very expensive of course because it goes through each and every invoice that a user has.
What might be a better alternative to this?
Thanks for any feedback.
One solution would be to grab the invoices before destroying the payment instance. Its add a bit more logic to the Controller however, but this is where the intent of both actions ( destroy payment and update invoices ) originate. It also reduces the iteration to just those invoices affected by the destroyed payment.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.each do |invoice|
invoice.save
end
...
end
Presumably you are overriding the save method of the Invoice model ( or have a callback on that as well), though I would choose a more explicit method for this intent. For example, removed_payment could be a method to handle this specific scenario and update the appropriate attributes - outstanding_amount and payment_status, etc.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.map(&:removed_payment)
...
end
The problem is that the associated allocation is also destroyed when destroying the payment. If you move the invoice updating to the Allocation model instead it will work as intended.
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
after_destroy :update_invoice
def update_invoice
if destroyed?
invoice.save!
end
end
end
Here's a Rails 4.1 test project with tests for this:
https://github.com/infused/update_parent_after_destroy
I have a 3 simple models:
class User < ActiveRecord::Base
has_many :subscriptions
end
class Game < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :game
end
What I am wondering is, is it possible when I query for Games to include another attribute called 'is_subbed' which will contain wether a particular user is subscribed to a game? Something like:
a_user = User.first
games = Game.scoped
games.conditions blah blah blah
and games will include a 'virtual' or in memory attribute that will be custom to a_user called is_subed
You can make a class method and an instance method (for a single game) like so:
class Game < ActiveRecord::Base
def self.subscribed?(user)
joins(:subscriptions).where(subscriptions: { user_id: user.id}).exists?
end
def subscribed?(user)
subscriptions.where(user_id: user.id).exists?
end
end
To get this result for each game using the query API, you can do this:
scope :with_subscriptions, lambda do |user|
joins("LEFT JOIN subscriptions ON subscriptions.game_id = games.id AND subscriptions.user_id = #{user.id}")
select("games.*, CASE WHEN subscriptions.user_id IS NULL THEN true ELSE false END as is_subscribed")
end
This will give you an is_subscribed parameter on each game object returned.
If I'm not mistaken you need to get games which are subscribed by some user and also apply some scope on these games?
May be this way would be acceptable
# extend user model with games collection
class User < ActiveRecord::Base
# your code
has_many :games, through: :subscriptions
end
And then in the controller (or anywhere you need) just call
#user.games.your_games_scope1.your_games_scope2.etc
Sorry if I misunderstood you
I have a Post model which has many ratings and I would like to store the average ratings figure for each post in the post ratings db column. I have a counter cache on the post model which counts the number of ratings.
I have the following code in my post.rb
def update_rating
if self.ratings_count_changed?
self.rating = self.ratings.average(:rating)
end
end
I had this as a before_save callback so whenever the post is edited and saved it updates the ratings, not very useful.
Is there a way I can call this method whenever a new rating is created?
One way of getting closer to your goal is to add after_create callback to Rating model:
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_attributes(:rating => self.post.ratings.average(:rating))
end
end
Or even more OO friendly version:
class Post < ActiveRecord::Base
has_many :ratings
# ...
def update_average_rating
update_attributes(:rating => self.ratings.average(:rating))
end
end
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_average_rating
end
end
Do you think about storing average rating in rating model instead post model? In that case you don't need any callback and you need recalc average by first request after changes.
Instead of before_save on post, do an after_create on the rating because it sounds like you need to update that rating score when a new rating is created, not before the post is saved.
How about putting this into Rating model:
after_create :update_post_average_rating
def update_post_average_rating
self.post.rating = #....
end
I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..