If I have a class method for a product returning its average rating, how do I query products with average_rating greater than a param I receive from frontend?
class Product < ApplicationRecord
def average_rating
ratings.sum(:rate) / ratings.count
end
end
For a scope I would need to pass the param from controller to model. Should I do that? Or should I just calculate the average rating in the controller instead? Or save average_rating in database with a callback when a rating is saved?
You cannot call class methods from SQL. You can however load all records into memory and do this, which is quite inefficient if you have a lot of records.
Product.all.select { |record| average_rating > record.value }
If this is a ruby method you can't do it (with SQL), unless you move your "average" logic into SQL too.
If the amount of records is a small one of the simplest solution is to use Ruby.
Product.all.select{|e| :&average_rating > e.value}
Related
I wondering if it is posible to use a model instance method as a where clause query. I mean. I have a model School with a method defined
class School < ActiveRecord::Base
def my_method
users.where(blablabla).present?
end
end
Is it posible to get something like:
School.all.where(my_method: true)
I know that I can do something like:
School.all.map{|x| x if x.my_method}
But this way has a huge penalization in performance compared to where query. Furthermore, the return of what I'm searching is an ActiveRecord Relation and map returns an array.
UPDATE:
Also there is another way to do it like:
School.all.joins(:users).where("users.attribute = something")
But this do not fit exactly what I want for several reasons.
Thanks in advance
I don't really know the relations between your models.
but where clause gets a hash of key - value.
So you can for example return the ID's of the users in a some kind of a hash and then use it.
def my_method
{user_id: users.where(blablabla).ids}
end
and use it:
School.all.where(my_method)
I have a Track model with an integer attribute called rank.
I'm updating the rank by specific actions: listens, downloads, purchases, ect. Ex: when a track is downloaded, in the track_controller I use track.increment!(:rank, by = 60)
I am thinking of creating an association model TrackRank so I can have a timestamp anytime a track's rank is updated (so I can do a rolling 3-week query of a track's rank for filtering and display purposes).
For every time a Tracks rank attr is updated, is there a way to auto-create an associated TrackRank object?
The ultimate goal:
Be able query the top X amount of tracks based on rank count in the last 3 weeks.
You can add a conditional call back on the update of the track
class Track < ActiveRecord::Base
# ignored associations and stuff
after_update :create_track_rank, if: :rank_changed?
# ignored methods
private
def create_track_rank
track_ranks.create(data)
end
end
You can update it on the controller
class TracksController < ApplicationController
def update
#track.update!
TrackRank.create(#track)
end
end
In my Rails app I have this:
class Invoice < ActiveRecord::Base
has_many :payments
before_save :save_outstanding_amount
def save_outstanding_amount # atomic saving
self.outstanding_amount = new_outstanding_amount
end
def update_outstanding_amount # adds another SQL query
update_column(:outstanding_amount, new_outstanding_amount)
end
private
def new_outstanding_amount
total - payments.sum(&:amount)
end
end
How can make this dynamic, so that the first method gets called from all instances of the Invoice class and the second method gets called from all instances of other classes, e.g. the Payment class?
You are doing something very dangerous here. Make sure use sql to change the value instead of setting a new value.
Imagine the following:
Invoice amount: 1000
User1 deducts 100, but does it with a very slow request.
Right after user 1 starts, User2 deducts 500, and does it really quickly.
Since you're doing this stuff within the application, you end up with an outstanding amount of 900, since that's the last executed method. update_counters creates a 'safe' sql query.
self.class.update_counters self.id, payments.sum(&:amount)
This also solves your question on how to call them from two classes. Always update those columns like this.
I want to use a subquery with ActiveRecord like so:
User.select(
'users.*,
(select sum(amount)
from subscriptions
where subscriptions.user_id = user.id
and created_at < ? ) as amt)'
).order('amt')
However, on the second to last line, I have the problem that I can't figure out how to bind the Time class parameter, as ActiveRecord::Base's select method doesn't accept more than one parameter (the sql string). What do I do?
You can use one of the ActiveRecord::Sanitization class methods.
These are mixed into ActiveRecord::Base and are almost all protected so they are most easily accessed by defining a class method on your model:
class User < ActiveRecord::Base
def self.select_with_args(sql, args)
query = sanitize_sql_array([sql, args].flatten)
select(query)
end
end
It's also worth looking for other ways to fetch your data and comparing their performance to the subquery method. For example, you could fetch the subscription counts with a separate query:
subs_by_user = Subscription.group(:user_id).where('created_at < ?', d).count()
User model has all_scores attribute and i created the method below
models/user.rb
def score
ActiveSupport::JSON.decode(self.all_scores)["local"]
end
What i'm trying to do this using this virtual attribute score to filter users. For example:
I need the users whose score is over 50. The problem is i can't use virtual attribute as a regular attribute in a query.
User.where("score > 50") # i can't do this.
Any help will be appreciated.
Well, the "easiest" solution would probably be User.all.select{|user| user.score > 50}. Obviously that's very inefficient, pulling every User record out of the database.
If you want to do a query involving the score, why don't you add a score column to the users table? You could update it whenever all_scores is changed.
class User < AR::Base
before_save :set_score, :if => :all_scores_changed?
protected
def set_score
self.score = ActiveSupport::JSON.decode(self.all_scores)["local"] rescue nil
end
end
This will also avoid constantly deserializing JSON data whenever you access user#score.