sum method alternatives for active records - ruby-on-rails

I am in a legacy Ruby on Rails project. Rails v2.3.9.
I have a model class product, in database there is products table.
class Product < ActiveRecord::Base
...
end
There is price attribute which is a integer type.
I got all products (ActiveRecords) at some point. I need to calculate the total price of all products I got. I know I can do:
total_price = all_products.sum(&:price)
It works.
But it also triggers a database query SELECT sum(...). Is there an alternative way to calculate the summation of price of all products without triggering any database query? I know I can use for loop, but I wonder any other ways?

sum with block is delegated to Enumerable and it will always hit the database if all_products is not previously loaded, so you have to make sure it is not being lazy loaded.
In terms of performance, SUM query would be the fastest way to get the result as it doesn't need to load all records and makes the operation in the database and not in memory.
In your case, if you have the collection loaded and still creating a query, you can use
total_price = all_products.map(&:price).sum
which will default to kindof rockusbacchus solution
.inject { |sum, element| sum + element }

If you already have the products loaded into all_products, you can use Ruby's inject method to sum the prices like so:
total_price = all_products.inject(0){|accumulator, product| accumulator + product.price}
However, you will likely find that this takes longer than just running the extra query. You might want to familiarize yourself with the other "*ect" methods in Ruby such as select and reject. Here's a decent article reviewing them:
https://blog.abushady.com/2014/08/27/select-reject-collect-detect-inject.html

Related

Rails 5 get specific field/value from table

I have a table called group. I want this method to return just the content of the relevant record's ID field. At the moment it returns an active record object ID.
def get_group_name(group_id)
Group.select([:name]).where("id = ?", group_id)
end
Thanks in advance.
I think you can do easier with find
def get_group_name(group_id)
Group.find(group_id).name
end
This will get you only the name of the group.
def get_group_name(group_id)
Group.where(id: group_id).limit(1).pluck(:name).first
end
It will run this query:
SELECT name
FROM groups
WHERE id = ?
LIMIT 1;
A side note is, be careful of what you’re doing. Any time you have a method to get a single field’s value, while it can be more efficient at times, it can easily be misused. If you’re looping over a collection of group ids trying to grab all of the names, then you’d be better off 1 query up front for all of the names as opposed to 1 per group id on the page. So just keep and eye on your console and pay attention to the queries you’re running.
Also, if you are looking over a collection, you may want to look into includes for your ActiveRecord queries, to include the group data in the previous query. You can benchmark this all to figure out what’s fastest for your use case.

Rails: Order custom model using custom method

I have a custom model called product, and it has many reviews.
i have a method that calculates the review
def rating
total = 0
reviews_count = reviews.count
return 0 if reviews_count == 0
reviews.each do |review|
total += review.grade
end
total.to_f/reviews_count
end
i would like to know how could i use this method to Order my products.
At products_controller.rb, if i use:
#products = Product.all.order("price")
its easy, it gives me the products list ordered by price. But, if i use, for example:
#products = Product.all.sort_by{|p| p.rating}
it gives me an array and not a "ActiveRecord::Relation"
I would like to know how could i order my product using a custom method that returns a value.
In general, you can't. Ordering happens in your database, which has no knowledge about any method you typed in your application. What you need is a way of translating your method into a valid sql. In your case, you can do:
Product.joins(:reviews).group('products.id').order('AVG(reviews.grade)')
That will give you sorted results and the relation object. However, relations with join are not that nice to work with, especially if you try to add another join. Also this might get quite slow when your database grows.
What you're doing in your example is running the query then using sort_by to sort the result set.
If you want to get back an activerecord collection instead of an array, and potentially chain this with other scopes, you should move the logic from your method into SQL, and put it in a scope.

ruby on rails manys' many

I am wondering how to do this without double each loop.
Assume that I have a user model and order model, and user has_many orders.
Now I have a users array which class is User::ActiveRecord_Relation
How can I get the orders of these users in one line?
Actually, the best way to do that is :
users.includes(:orders).map(&:orders)
Cause you eager load the orders of the users (only 2 sql queries)
Or
Order.where(user_id: users.pluck(:id))
is quite good too in term of performance
If you've got a many-to-many association and you need to quickly load in all the associated orders you will need to be careful to avoid the so-called "N plus 1" load that can result from the most obvious approach:
orders = users.collect(&:orders).flatten
This will iterate over each user and run a query like SELECT * FROM orders WHERE user_id=? without any grouping.
What you really want is this:
orders = Order.where(user_id: users.collect(&:id))
That should find all orders from all users in a single query.
An answer just come up my mind after I asked....
users.map {|user| user.orders}

Rails subquery reduce amount of raw SQL

I have two ActiveRecord models: Post and Vote. I want a make a simple query:
SELECT *,
(SELECT COUNT(*)
FROM votes
WHERE votes.id = posts.id) AS vote_count
FROM posts
I am wondering what's the best way to do it in activerecord DSL. My goal is to minimize the amount of SQL I have to write.
I can do Post.select("COUNT(*) from votes where votes.id = posts.id as vote_count")
Two problems with this:
Raw SQL. Anyway to write this in DSL?
This returns only attribute vote_count and not "*" + vote_count. I can append .select("*") but I will be repeating this every time. Is there an much better/DRY way to do this?
Thanks
Well, if you want to reduce amount of SQL, you can split that query into smaller two end execute them separately. For instance, the votes counting part could be extracted to query:
SELECT votes.id, COUNT(*) FROM votes GROUP BY votes.id;
which you may write with ActiveRecord methods as:
Vote.group(:id).count
You can store the result for later use and access it directly from Post model, for example you may define #votes_count as a method:
class Post
def votes_count
##votes_count_cache ||= Vote.group(:id).count
##votes_count_cache[id] || 0
end
end
(Of course every use of cache raises a question about invalidating or updating it, but this is out of the scope of this topic.)
But I strongly encourage you to consider yet another approach.
I believe writing complicated queries like yours with ActiveRecord methods — even if would be possible — or splitting queries into two as I proposed earlier are both bad ideas. They result in extremely cluttered code, far less readable than raw SQL. Instead, I suggest introducing query objects. IMO there is nothing wrong in using raw, complicated SQL when it's hidden behind nice interface. See: M. Fowler's P of EAA and Brynary's post on Code Climate Blog.
How about doing this with no additional SQL at all - consider using the Rails counter_cache feature.
If you add an integer votes_count column to the posts table, you can get Rails to automatically increment and decrement that counter by changing the belongs_to declaration in Vote to:
belongs_to :post, counter_cache: true
Rails will then keep each Post updated with the number of votes it has. That way the count is already calculated and no sub-query is needed.
Maybe you can create mysql view and just map it to new AR model. It works similar way to table, you just need to specify with set_table_name "your_view_name"....maybe on DB level it will work faster and will be automatically re-calculating.
Just stumbled upon postgres_ext gem which adds support for Common Table Expressions in Arel and ActiveRecord which is exactly what you asked. Gem is not for SQLite, but perhaps some portions could be extracted or serve as examples.

Left outer join using the model of the joined table in rails

I need to do a left outer join in rails, but I need the model objects to be for the joined table.
What I want is a list of the days, with the metrics for each day. I need to have all days regardless of whether or not there were metrics, but I don't want to make a bunch of round trips to the database.
This works, but causes problems because it thinks I have PeriodDay objects when I really want Metric objects:
PeriodDay.select("metrics.*").join('LEFT OUTER JOIN metrics ON period_days.date = metrics.date').where('period_id = ?', current_period)
I can use find_by_sql on the Metric object, but the query building is more complicated (and conditional) than this simplified example, so I would rather figure out the "rails way" for this problem.
My current workaround is to loop through the records and create Metric objects from the attributes of the PeriodDay object. It doesn't feel efficient, but it is better than making multiple database calls.
metrics = []
recs = PeriodDay.select("metrics.*").join('LEFT OUTER JOIN metrics ON period_days.date = metrics.date').where('period_id = ?', current_period)
for rec in recs
metrics << Metric.new(rec.attributes)
end
Assuming that Period has many PeriodDay has many Metric, and that period_id is an attribute of your PeriodDay model, your workaround should be identical to something like this:
Metric.includes(:period_day).where(:period_day => {:period_id => #current_period})
This doesn't get you a list of days with their respective Metric objects as you mentioned in the original question, but it gets you a list of all Metric objects for a particular period. (unless I'm missing something...)
If you want a list of PeriodDay objects with their included Metric objects, you can use includes instead of joins.
PeriodDay.includes(:metrics).where(:period_id => #current_period)
This will execute two queries (one to get period days and the other to get metrics) but it is a lot more readable.

Resources