Rails - Sort by join table data - ruby-on-rails

I've got a RoR project in the works. Here are the applicable sections of my models.
Home
has_many :communities, :through => :availabilities
has_many :availabilities, :order => "price ASC"
Community
has_many :homes, :through => :availabilities
has_many :availabilities
Availability
belongs_to :home
belongs_to :community
The "availabilities" table in the database has the additional data column "price"
So now I can call
#home.availabilities.each do |a|
a.community.name
a.price
and get back the availabilities data ordered by price as I want. My question is this:
Is there a way to automatically order Homes by avaliabilities.first.price (first = lowest)? Maybe something with default_scope :order?

I would suggest to avoid using default_scope, especially on something like price on another table. Every time you'll use that table, join and ordering will take place, possibly giving strange results in complex queries and anyway making your query slower.
There's nothing wrong with a scope of its own, it's simpler and it's even clearer, you can make it as simple as:
scope :ordered, -> { includes(:availabilities).order('availabilities.price') }
PS: Remember to add an index on price; Also see other great answers in here to decide between join/include.

Figured it out with help from this related post.
I moved the ordering out of the Home model and into the Availability model:
Availability
default_scope :order => "price ASC"
Then I eager loaded availabilities into the Home model and sorted by price:
Home
default_scope :include => :availabilities, :order => "availabilities.price ASC"

#ecoologic answer:
scope :ordered, -> { includes(:availabilities).order('availabilities.price') }
is great, but it should be mentioned that includes could, and in some cases should be replaced by joins. They both have their optimal use cases (see: #1, #2).
From practical standpoint there are two main differences:
includes loads associated record(s); in this case Availability records. joins don't load any associated record(s). So you should use includes when you want to use data from join model e.g. display price somewhere. On the other hand, joins should be used if you intend to use join model's data only in query e.g. in ORDER BY or WHERE clauses.
includes loads all records, while joins loads only those records that have associated join model. So in OP's case, Home.includes(:availabilities) would load all homes, while Home.joins(:availabilities) would load only those homes that have associated at least one availability.
Also see this question.

Another way to achieve this:
scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price]) }
You can also specify ASC direction with
scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price].asc) }
DESC:
scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price].desc) }
Using arel_table on ActiveRecord model makes you save against scenario when table name changed (but it happens very rarely).
Note that it is nice to add main_table#id for determinate sorting.
So final version would be:
scope :ordered, -> {
includes(:availabilities).
order(Availability.arel_table[:price].asc, order(Home.arel_table[:id].asc)
}

In Rails 5.2+, you might get a deprecation warning when passing a string param to order method:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "table.column". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes.
To solve this, you can use Arel.sql():
scope :ordered, -> {
includes(:availabilities).order(Arel.sql('availabilities.price'))
}

You can also sort linked tables like this (e.g.):
class User
has_many :posts
end
class Post
belongs_to :user
scope :sorted_by_user_and_title, -> {
joins(:user).merge(
User.order(first_name: :asc, last_name: :asc)
)
.order(title: :desc)
# SELECT * FROM `posts`
# INNER JOIN `users` ON `posts`.`user_id` = `users`.`id`
# ORDER BY
# `users`.`first_name` ASC, `users`.`last_name` ASC, `posts`.`title` DESC;
}
scope :sorted_by_title_and_user, -> {
order(title: :desc)
.joins(:user).merge(
User.order(first_name: :asc, last_name: :asc)
)
# SELECT * FROM `posts`
# INNER JOIN `users` ON `posts`.`user_id` = `users`.`id`
# ORDER BY
# `posts`.`title` DESC, `users`.`first_name` ASC, `users`.`last_name` ASC;
}
end
Regards

Related

How to pluck id of has_one associations?

class Post
has_one :latest_comment, -> { order(created_at: :desc) }, class_name: 'Comment'
end
I want to do something like:
Post.joins(:latest_comment).pluck('latest_comment.id')
but it's not valid syntax and it doesn't work.
Post.joins(:latest_comment).pluck('comments.id')
Above works but it returns ids of all comments for a post, not only of the latest.
ActiveRecord::Assocations are a very leaky abstraction around SQL joins so your has_one :latest_comment assocation won't actually return a single row in the join table per record unless you're calling it on an instance of Post.
Instead when you run Post.joins(:latest_comment).pluck('comments.id')you get:
SELECT "comments"."id"
FROM "posts"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
ActiveRecord isn't actually smart enough to know that you want to get unique values from the comments table - and it actually just behaves like a has_many association. In its defence this isn't actually something thats even realistic to do in polyglot fashion.
What you want to do can instead is to select the rows from the comments table and get distinct values:
Comment.order(:post_id, created_at: :desc)
.pluck(Arel.sql('DISTINCT ON (post_id) id'))
DISTINCT ON is Postgres specific. The exact approach here will vary between RDBMS:es and there are many other alternatives such as lateral joins, window functions etc depending on your performance requirements.

Rails 5 select from two different tables and get one result

I have 3 models, Shop, Client, Product.
A shop has many clients, and a shop has many products.
Then I have 2 extra models, one is ShopClient, that groups the shop_id and client_id. The second is ShopProduct, that groups the shop_id and product_id.
Now I have a controller that receives two params, the client_id and product_id. So I want to select all the shops (in one instance variable #shops) filtered by client_id and product_id without shop repetition. How can I do this??
I hope I was clear, thanks.
ps: I'm using Postgresql as database.
Below query will work for you.
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
end
class Client
end
class Product
end
class ShopClient
belongs_to :shop
belongs_to :client
end
class ShopProduct
belongs_to :shop
belongs_to :product
end
#shops = Shop.joins(:clients).where(clients: {id: params[:client_id]}).merge(Shop.joins(:products).where(products: {id: params[:product_id]}))
Just to riff on the answer provided by Prince Bansal. How about creating some class methods for those joins? Something like:
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
class << self
def with_clients(clients)
joins(:clients).where(clients: {id: clients})
end
def with_products(products)
joins(:products).where(products: {id: products})
end
end
end
Then you could do something like:
#shops = Shop.with_clients(params[:client_id]).with_products(params[:product_id])
By the way, I'm sure someone is going to say you should make those class methods into scopes. And you certainly can do that. I did it as class methods because that's what the Guide recommends:
Using a class method is the preferred way to accept arguments for scopes.
But, I realize some people strongly prefer the aesthetics of using scopes instead. So, whichever pleases you most.
I feel like the best way to solve this issue is to use sub-queries. I'll first collect all valid shop ids from ShopClient, followed by all valid shop ids from ShopProduct. Than feed them into the where query on Shop. This will result in one SQL query.
shop_client_ids = ShopClient.where(client_id: params[:client_id]).select(:shop_id)
shop_product_ids = ShopProduct.where(product_id: params[:product_id]).select(:shop_id)
#shops = Shop.where(id: shop_client_ids).where(id: shop_product_ids)
#=> #<ActiveRecord::Relation [#<Shop id: 1, created_at: "2018-02-14 20:22:18", updated_at: "2018-02-14 20:22:18">]>
The above query results in the SQL query below. I didn't specify a limit, but this might be added by the fact that my dummy project uses SQLite.
SELECT "shops".*
FROM "shops"
WHERE
"shops"."id" IN (
SELECT "shop_clients"."shop_id"
FROM "shop_clients"
WHERE "shop_clients"."client_id" = ?) AND
"shops"."id" IN (
SELECT "shop_products"."shop_id"
FROM "shop_products"
WHERE "shop_products"."product_id" = ?)
LIMIT ?
[["client_id", 1], ["product_id", 1], ["LIMIT", 11]]
Combining the two sub-queries in one where doesn't result in a correct response:
#shops = Shop.where(id: [shop_client_ids, shop_product_ids])
#=> #<ActiveRecord::Relation []>
Produces the query:
SELECT "shops".* FROM "shops" WHERE "shops"."id" IN (NULL, NULL) LIMIT ? [["LIMIT", 11]]
note
Keep in mind that when you run the statements one by one in the console this will normally result in 3 queries. This is due to the fact that the return value uses the #inspect method to let you see the result. This method is overridden by Rails to execute the query and display the result.
You can simulate the behavior of the normal application by suffixing the statements with ;nil. This makes sure nil is returned and the #inspect method is not called on the where chain, thus not executing the query and keeping the chain in memory.
edit
If you want to clean up the controller you might want to move these sub-queries into model methods (inspired by jvillians answer).
class Shop
# ...
def self.with_clients(*client_ids)
client_ids.flatten! # allows passing of multiple arguments or an array of arguments
where(id: ShopClient.where(client_id: client_ids).select(:shop_id))
end
# ...
end
Rails sub-query vs join
The advantage of a sub-query over a join is that using joins might end up returning the same record multiple times if you query on a attribute that is not unique. For example, say a product has an attribute product_type that is either 'physical' or 'digital'. If you want to select all shops selling a digital product you must not forget to call distinct on the chain when you're using a join, otherwise the same shop may return multiple times.
However if you'll have to query on multiple attributes in product, and you'll use multiple helpers in the model (where each helper joins(:products)). Multiple sub-queries are likely slower. (Assuming you set has_many :products, through: :shop_products.) Since Rails reduces all joins to the same association to a single one. Example: Shop.joins(:products).joins(:products) (from multiple class methods) will still end up joining the products table a single time, whereas sub-queries will not be reduced.
Below sql query possibly gonna work for you.
--
-- assuming
-- tables: shops, products, clients, shop_products, shop_clients
--
SELECT DISTINCT * FROM shops
JOIN shop_products
ON shop_products.shop_id = shops.id
JOIN shop_clients
ON shop_clients.shop_id = shops.id
WHERE shop_clients.client_id = ? AND shop_products.product_id = ?
If you'll face difficulties while creating an adequate AR expression for this sql query, let me know.
Btw, here is a mock

How to order records by their latest child records attribute

I'm having troubles to order my records by their has_one association. I'm quite sure the solution is obvious, but I just can't get it.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
If I order by checks.status I always get different check ids. Shouldn't they be the same but with different order?
Or is the -> { } way to get the has_one association the problem?
Migration.all.includes(:latest_origin_check).order("checks.status DESC").each do |m| puts m.latest_origin_check.id end
So in one sentence: How do I order records through a custom has_one association?
I'm using Ruby 2.0.0, Rails 4.2 and PostgreSQL.
Update:
I wasn't specific enough. I've got two has_one relations on the checks relation.
Also very Important. One Migration has a way to big number of checks to include all the checks at once. So Migration.first.includes(:checks) would be very slow. We are talking about serveral thousand and I only need the latest.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
has_one :latest_target_check, -> { where(origin: false).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
Now if I get the latest_origin_check, I get the correct Record. The query is the following.
pry(main)> Migration.last.latest_origin_check
Migration Load (1.1ms) SELECT "migrations".* FROM "migrations" ORDER BY "migrations"."id" DESC LIMIT 1
Check Load (0.9ms) SELECT "checks".* FROM "checks" WHERE "checks"."migration_id" = $1 AND "checks"."origin" = 't' ORDER BY "checks"."at" DESC LIMIT 1 [["migration_id", 59]]
How do I get the latest check of each migration and then sort the migrations by a attribute of the latest check?
I'm using ransack. Ransack seems to get it right when I order the records by "checks.at"
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."at" ASC
But the same query returns wrong results when I order by status
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."status" ASC
Check.status is a boolean, check.at is a DateTime. A colleague suggested that the boolean is the problem. Do I need to convert the booleans to an integer to make them sortable? How do I do that only for the :latest_origin_check? Something like that?
.order("(case when \"checks\".\"status\" then 2 when \"checks\".\"status\" is null then 0 else 1 end) DESC")
You already have a has_many relationship with Check on Migration. I think you are looking for a scope instead:
scope :latest_origin_check, -> { includes(:checks).where(origin:true).order("checks.status DESC").limit(1)}
Drop the has_one :latest_origin_check line on Migration.
Migration.latest_origin_check
I think the line about should return your desired result set.

Why is my controller not ordering by what Im telling to order by?

My controller action is calling all images belonging to a specific user and Im trying to order by its position (Im using the acts_as_list gem) but when I go to the page, the images are FIRST sorted by the created date, and then position (according to rails console). But because it orders by the creation date first my controller order is being ignored which is no good.
here is my action
def manage_tattoos
#tattoos = current_member.tattoos.order("position DESC")
end
and my server console shows:
Tattoo Load (0.6ms) SELECT `tattoos`.* FROM `tattoos` WHERE
(`tattoos`.member_id = 1) ORDER BY tattoos.created_at DESC, position DESC
Have you tried specifiing the order in the association?
class TodoList < ActiveRecord::Base
has_many :todo_items, :order => "position"
end
Does your association between Member and Tattoo have an order clause? E.g.
# Member class
has_many :tattoos, :order => "created_at DESC"
Is this the case? If so you might need to change your query to something like:
Tattoo.where(:member_id=>current_member.id).order("position DESC")
I'm unaware of a way to clear the order clause from an ActiveRecord association.
Or specify what to do with created_at:
current_member.tattoos.order("position DESC, created_at DESC")
Well it seems Im just very absent minded and had completely forgotten that I set a default scope on the model. Took that out and everything is fine

how to modify complex find_by_sql query w/ union into rails 3

here's the current query:
#feed = RatedActivity.find_by_sql(["(select *, null as queue_id, 3 as model_table_type from rated_activities where user_id in (?)) " +
"UNION (select *, null as queue_id, null as rating, 2 as model_table_type from watched_activities where user_id in (?)) " +
"UNION (select *, null as rating, 1 as model_table_type from queued_activities where user_id in (?)) " +"ORDER BY activity_datetime DESC limit 100", friend_ids, friend_ids, friend_ids])
Now, this is a bit of kludge, since there are actually models set up for:
class RatedActivity < ActiveRecord::Base
belongs_to :user
belongs_to :media
end
class QueuedActivity < ActiveRecord::Base
belongs_to :user
belongs_to :media
end
class WatchedActivity < ActiveRecord::Base
belongs_to :user
belongs_to :media
end
would love to know how to use activerecord in rails 3.0 to achieve basically the same thing as is done with the crazy union i have there.
It sounds like you should consolidate these three separate models into a single model. Statuses such as "watched", "queued", or "rated" are then all implicit based on attributes of that model.
class Activity < ActiveRecord::Base
belongs_to :user
belongs_to :media
scope :for_users, lambda { |u|
where("user_id IN (?)", u)
}
scope :rated, where("rating IS NOT NULL")
scope :queued, where("queue_id IS NOT NULL")
scope :watched, where("watched IS NOT NULL")
end
Then, you can call Activity.for_users(friend_ids) to get all three groups as you are trying to accomplish above... or you can call Activity.for_users(friend_ids).rated (or queued or watched) to get just one group. This way, all of your Activity logic is consolidated in one place. Your queries become simpler (and more efficient) and you don't have to maintain three different models.
I think that your current solution is OK in case of legacy DB. As native query it is also most efficient as your DBMS does all hard work (union, sort, limit).
If you really want to get rid of SQL UNION without changing schema then you can move union to Ruby array sum - but this may be slower.
result = RatedActivity.
select("*, null as queue_id, 3 as model_table_type").
where(:user_id=>friend_ids).
limit(100).all +
QueuedActivity...
Finally you need to sort and limit that product with
result.sort(&:activity_datetime)[0..99]
This is just proof of concept, as you see it is inefficient is some points (3 queries, sorting in Ruby, limit). I would stay with find_by_sql.

Resources