SELECT ... AS ... with ActiveRecord - ruby-on-rails

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

Related

Change 'ORDER BY' Chain on ActiveRecord Subset

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.

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?

Find records with multiple foreign key values in Rails

I've got two models User and Post where a
:user has_many :posts
How do I find a list of users who have more than n posts?
I'm looking for a simple statement like where rather than something to do with using scopes.
The where query does not allow aggregate functions like count inside it, the ideal method to tackle this problem is using the having method.
User.joins(:posts).group('users.id').having('COUNT(*) >= ?', n)
This will be mapped to
SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" GROUP BY users.id HAVING COUNT(*) >= n
There is another method possible which is more easier, by using counter cache inside the Post Model and keeping the count as a posts_count column on the user table.
belongs_to :user, counter_cache:true
User.where('posts_count > ?', n)
But I prefer the first method as it does not involve any DB changes and is pretty straight forward
User.join(:posts).group('users.id').having('count(user_id) > n')
Another way of solving this is to use counter_cache and add a posts_count column on the user table.
belongs_to :user, counter_cache:true # Inside the Post model
Then you can do
User.where('posts_count > ?', n)
Let try this:
User.where('id IN (SELECT user_id
FROM posts
GROUP BY user_id
HAVING COUNT(*) > ?
)', n)

How to add aggregated columns from SQL SELECT to AR object in Rails

Suppose we have a model Order and Item where order has_many items.
I'd now like to get the sum of all items for each order.
#orders = Order.select("orders.*, sum(items.amount) as items_sum)
.joins("LEFT JOIN orders ON items.order_id = orders.id")
.group("orders.id")
produces
SELECT sum(items.amount) as items_sum, transfers.* FROM "orders"
LEFT JOIN items ON items.order_id = orders.id GROUP BY orders.id
The SQL itself works fine. The problem is that #orders doesn't include the items_sum. I'd like to be able to do #orders[0].items_sum. How is that possible?
you should be able to access it as
#orders.first.items_sum
or
#orders.first["items_sum"]
Documentation here

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