Change 'ORDER BY' Chain on ActiveRecord Subset - ruby-on-rails

Background:
I have Product model which includes 4 categories
class Product < ActiveRecord::Base
enum category: [:recent, :cheapest, :most_expensive, :popular]
end
I've implemented a custom ORDER BY for each category with pagination (LIMIT 10), so when I'm getting products list, I'm getting multiple SQL queries with different ORDER BY in each query like this:
recent:
SELECT "products".* FROM "products" ORDER BY "products"."created_at" DESC LIMIT 10
cheapest:SELECT "products".* FROM "products" ORDER BY "products"."price" ASC LIMIT 10
most_expensive: SELECT "products".* FROM "products" ORDER BY "products"."price" DESC LIMIT 10
popular: SELECT "products".* FROM "products" ORDER BY "products"."popularity" DESC, "products"."created_at" DESC LIMIT 10
So as mentioned, each of the above queries, results a Product::ActiveRecord_Relation contains 10 products with different order for each query.
Question:
I've added new column to Product model which is featured with boolean value, and I need to apply ORDER BY featured DESC at the beginning of each query with keeping the other ORDER BY fields as it is(i.e. popular query should be like this SELECT "products".* FROM "products" ORDER BY "products"."featured" DESC, "products"."popularity" DESC, "products"."created_at" DESC LIMIT 10).
Note: ORDER BY featured DESC is just appended at the beginning of the previous ORDER BY statement, and it is applied on the subset not on the whole model.
What I have tried?
I have tried the following scenarios:
Add #products = #products.order(featured: :desc) in the controller but the result is not as expected because it adds the order to the end of existing order by chain.
Use default_scope in Product model default_scope { order(featured: :desc) } but the result is not as expected because it implements the order on the whole model, but the expected result is applying the order only on the subset(10 records).
using reorder in the controller #products = #products.reorder('').order(featured: :desc) but the result still not as expected because this remove the old order and actually I need to keep it but at the end of ORDER BY chain
The only solution I'm able to do is by using string variable to save previous order by chain, then use reorder('').order(featured: :desc) and finally append the string at the end of new ORDER BY:
current_order = #products.to_sql[#products.to_sql.downcase.index('order by')+8..#products.to_sql.downcase.index('limit')-1]
#products = #products.reorder("featured desc, #{current_order}" )
But I'm sure there is a better solution which I need your support to achieve it.
Summary:
As summarised in the comments below, I need the following implementation:
Given just r where r = M.order(:a), I want to run r.something(:b) and get the effect of M.order(:b).order(:a) rather than the M.order(:a).order(:b) that r.order(:b) would give you

Is there a reason you're not using scope chaining here? This seems like a perfect case to use it in. The use of enum is unclear, as well.
Something like this:
# /app/models/product.rb
class Product < ActiveRecord::Base
scope :recent, { order(created_at: :desc) }
scope :cheapest, { order(price: :asc) }
scope :most_expensive, { order(price: :desc) }
scope :popular, { order(popularity: :desc) }
scope :featured, { where(featured: true) }
end
Then in your controller you could do:
# /app/controllers/products_controller.rb
...
Product.featured.cheapest.limit(10)
Product.featured.most_expensive.limit(10)
...
and so on.
AREL should build the query correctly, and IIRC you can swap the sequence of the scopes (featured after cheapest, for example) if you want them to be applied differently.

Related

How to filter in Rails the records of table using last part of a string IN an Array of values

Here is what I have build in the model class
class Organization < ApplicationRecord
...
scope :filter_by_domestic, ->(host_name_designation) do
joins("LEFT JOIN events ON events.organization_id = organizations.id").
where(events: { host_name: host_name_designation })
end
end
I am getting correct results when I am using something like this:
#organizations = Organization.filter_by_domestic ['www.abc.com', 'www.xyz.com']
But I would like to improve the query and get the results only on the last part of the host_name, something like all records for which host_name ends with the substring .com.
I need to apply split('.')last for this where(events: { host_name: host_name_designation }) and I don't know the correct syntax for this.
What is the correct statement here, in my case?
I want to be able to write something like:
scope :filter_by_domestic, ->(host_name_designation) do
joins("LEFT JOIN events ON events.organization_id = organizations.id").
where(events: { '.' + host_name:.split('.').last host_name_designation })
end
I need where to return something like this:
'.' + Event.first.host_name.split('.').last
Event Load (0.4ms) SELECT "events".* FROM "events" ORDER BY "events"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> ".com"
Practically I need only the last part of the host_name to be used in the where clause. I know how to build my own where clause to achieve this, but I am looking to have the query generated instead of developed.
Any clue?
OK, looks like the solution I am looking for does not exists most probably due to some limitations in ActiveRecord.

Rails group_by and sorting upon field in related model

I have a User that belongs to a User_type. In the user_type model, there's a field called position to handle the default sorting when displaying user_types and their users.
Unfortunataly this does not work when searching with Ransack, so I need to search from the User model and use group_by to group the records based on their user_type_id.
This works perfectly, but I need a way to respect the sorting that is defined in the user_type model. This is also dynamic, so there's no way of telling what the sorting is from the user model.
Therefor I think I need to loop through the group_by array and do the sorting manually. But I have no clue where to start. This is the controller method:
def index
#q = User.ransack(params[:q])
#users = #q.result(distinct: true).group_by &:user_type
end
How do I manipulate that array to sort on a field that in the related model?
Try to add this line to Usertype model
default_scope order('position')
First of all there is n+1 query problem. You are not joining user_types table to users and application calls SELECT on user_types n times where n is a number of Users + another one SELECT call to grab users:
...
UserType Load (0.2ms) SELECT "user_types".* FROM "user_types" WHERE "user_types"."id" = $1 LIMIT 1 [["id", 29]]
UserType Load (0.2ms) SELECT "user_types".* FROM "user_types" WHERE "user_types"."id" = $1 LIMIT 1 [["id", 7]]
...
So you need to include user_types and order by user_types.position:
#q.result(distinct: true).includes(:user_type).order('user_types.position')
There are a lot of examples for ordering here:
http://apidock.com/rails/ActiveRecord/QueryMethods/order
Your case (Ordering on associations) is also available
Information about n+1 query:
What is SELECT N+1?

How to eager load child model's sum value for ruby on rails?

I have an Order model, it has many items, it looks like this
class Order < ActiveRecord::Base
has_many :items
def total
items.sum('price * quantity')
end
end
And I have an order index view, querying order table like this
def index
#orders = Order.includes(:items)
end
Then, in the view, I access total of order, as a result, you will see tons of SUM query like this
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 1]]
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 2]]
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 3]]
...
It's pretty slow to load order.total one by one, I wonder how can I load the sum in a eager manner via single query, but still I can access order.total just like before.
Try this:
subquery = Order.joins(:items).select('orders.id, sum(items.price * items.quantity) AS total').group('orders.id')
#orders = Order.includes(:items).joins("INNER JOIN (#{subquery.to_sql}) totals ON totals.id = orders.id")
This will create a subquery that sums the total of the orders, and then you join that subquery to your other query.
I wrote up two options for this in this blog post on using find_by_sql or joins to solve this.
For your example above, using find_by_sql you could write something like this:
Order.find_by_sql("select
orders.id,
SUM(items.price * items.quantity) as total
from orders
join items
on orders.id = items.order_id
group by
order.id")
Using joins, you could rewrite as:
Order.all.select("order.id, SUM(items.price * items.quantity) as total").joins(:items).group("order.id")
Include all the fields you want in your select list in both the select clause and the group by clause. Hope that helps!

SELECT ... AS ... with ActiveRecord

Is it possible to make a query like this with rails?
SELECT items.*, (select count(*) from ads where item_id = items.id) as adscount WHERE 1
And then access this field like so?
#item.adscount
For example for each item there is a hundred ads or so.
And in items index view I need to show how many ads each item has.
For now I only found out how to make a unique query for every item, for example:
select count(*) from ads where item_id = 1
select count(*) from ads where item_id = 2
select count(*) from ads where item_id = 3
etc
Edit:
Made a counter cache column.
That gave me huge performance improvement.
One solution is to use Scopes. You can either have it be a special query, like
class Item < ActiveRecord::Base
scope :with_adscount, select("items.*, (select count(*) from ads where item_id = items.id) as adscount")
end
Then in the controller, or where ever you query from, you can use it like so:
#items = Item.with_adscount
#items.each do |item|
item.adscount
end
Or, you can put it as the default scope using:
class Item < ActiveRecord::Base
default_scope select("items.*, (select count(*) from ads where item_id = items.id) as adscount")
end
Use Rails Counter Cache http://railscasts.com/episodes/23-counter-cache-column
Then you will be able to use it like #item.ads.count and this will not produce any database queries.
And in addition (if you really want to use it your way) you can create a model method
def adscount
self.ads.count
end
You can do it at the controller like this
a = MyModel.find_by_sql('SELECT *, (select count(*) from table) as adscount FROM table').first
a.adscount #total
a.column1 #value of a column
a.column2 #value of another column

Rails: How to sort many-to-many relation

I have a many-to-many relationship between a model User and Picture. These are linked by a join table called Picturization.
If I obtain a list of users of a single picture, i.e. picture.users -> how can I ensure that the result obtained is sorted by either creation of the Picturization row (i.e. the order at which a picture was associated to a user). How would this change if I wanted to obtain this in order of modification?
Thanks!
Edit
Maybe something like
picture.users.where(:order => "created_at")
but this created_at refers to the created_at in picturization
Have an additional column something like sequence in picturization table and define sort order as default scope in your Picturization
default_scope :order => 'sequence ASC'
If you want default sort order based on modified_at then use following default scope
default_scope :order => 'modified_at DESC'
You can specify the table name in the order method/clause:
picture.users.order("picturizations.created_at DESC")
Well, in my case, I need to sort many-to-many relation by a column named weight in the middle-table. After hours of trying, I figured out two solutions to sort many-to-many relation.
Solution1: In Rails Way
picture.users.where(:order => "created_at")
cannot return a ActiveRecord::Relation sorted by Picturization's created_at column.
I have tried to rewrite a default_scope method in Picturization, but it does not work:
def self.default_scope
return Picturization.all.order(weight: :desc)
end
Instead, first, you need to get the ids of sorted Picturization:
ids = Picturization.where(user_id:user.id).order(created_at: :desc).ids
Then, you can get the sorted objects by using MySQL field functin
picture.users.order("field(picturizations.id, #{ids.join(",")})")
which generates SQL looks like this:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1#for instance
ORDER BY field(picturizations.id, 9,18,6,8,7)#for instance
Solution2: In raw SQL Way
you can get the answer directly by using an order by function:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1
order by picturizations.created_at desc

Resources