How to pluck id of has_one associations? - ruby-on-rails

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.

Related

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

Rails 4: Joins in ActiveRecord relation lambdas not include when doing a join

I'm creating a Revision system for a project where a base table contains the current revision for a given id, and a revision table contains the data tagged with a given revision, eg:
foos
- id
- revision
foo_revisions
- foo_id
- revision
{data}
For relations between these I have used the lamda syntax to specify conditions on the relation like this:
class Article
belongs_to :product, ->{ joins(:base).where("products.revision = product_revisions.revision") }, :class_name=> "Product::Revision", :primary_key => :product_id
Where article is not revisioned, but product is (Product::Revision is the model that contains the actual data, and is a ActiveRecord::Base mapping to product_revisions, while Product maps to products).
The :base relation is from Product::Revision to Product
This works fine for the normal things like
a = Article.find(..)
a.product
which products the sql (a.product only)
SELECT `product_revisions`.* FROM `product_revisions`
INNER JOIN `products` ON `products`.`id` = `product_revisions`.`product_id`
WHERE `product_revisions`.`product_id` = 406
AND (products.revision = product_revisions.revision) ORDER BY `product_revisions`.`id` ASC LIMIT 1
But when I do Article.joins(:product) it fails, since it doesn't join in the products table:
SELECT `articles`.* FROM `articles` INNER JOIN `product_revisions`
ON `product_revisions`.`product_id` = `articles`.`product_id`
AND (products.revision = product_revisions.revision)
with the error:
Mysql2::Error: Unknown column 'products.revision' in 'on clause'
To me it seems like ActiveRecord simply ignores the joins in the lamba when it does the joins query, which seems stupid. Is this a bug, or is there a better/correct way to do this?
I've encountered a similar problem. Any joins specified in a lambda for a has_many are silently ignored.
I found this in the Rails issues that solves the problem for me:
https://github.com/rails/rails/pull/11518
The author mentions the problem occurring when there is an order clause but I think this muddies the water - it makes no difference whether there is an order clause or not.
I cannot say whether this is a bug or intended behaviour but I suspect the former.

Specifying conditions on eager loaded associations returns ActiveRecord::RecordNotFound

The problem is that when a Restaurant does not have any MenuItems that match the condition, ActiveRecord says it can't find the Restaurant. Here's the relevant code:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
def self.with_meals_of_the_week
includes({menu_items: :meal}).where(:'menu_items.date' => Time.now.beginning_of_week..Time.now.end_of_week)
end
end
And the sql code generated:
Restaurant Load (0.0ms)←[0m ←[1mSELECT DISTINCT "restaurants".id FROM "restaurants"
LEFT OUTER JOIN "menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN "meals" ON "meals"."id" = "menu_items"."meal_id" WHERE
"restaurants"."id" = ? AND ("menu_items"."date" BETWEEN '2012-10-14 23:00:00.000000'
AND '2012-10-21 22:59:59.999999') LIMIT 1←[0m [["id", "1"]]
However, according to this part of the Rails Guides, this shouldn't be happening:
Post.includes(:comments).where("comments.visible", true)
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded.
The SQL generated is a correct translation of your query. But look at it,
just at the SQL level (i shortened it a bit):
SELECT *
FROM
"restaurants"
LEFT OUTER JOIN
"menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN
"meals" ON "meals"."id" = "menu_items"."meal_id"
WHERE
"restaurants"."id" = ?
AND
("menu_items"."date" BETWEEN '2012-10-14' AND '2012-10-21')
the left outer joins do the work you expect them to do: restaurants
are combined with menu_items and meals; if there is no menu_item to
go with a restaurant, the restaurant is still kept in the result, with
all the missing pieces (menu_items.id, menu_items.date, ...) filled in with NULL
now look aht the second part of the where: the BETWEEN operator demands,
that menu_items.date is not null! and this
is where you filter out all the restaurants without meals.
so we need to change the query in a way that makes having null-dates ok.
going back to ruby, you can write:
def self.with_meals_of_the_week
includes({menu_items: :meal})
.where('menu_items.date is NULL or menu_items.date between ? and ?',
Time.now.beginning_of_week,
Time.now.end_of_week
)
end
The resulting SQL is now
.... WHERE (menu_items.date is NULL or menu_items.date between '2012-10-21' and '2012-10-28')
and the restaurants without meals stay in.
As it is said in Rails Guide, all Posts in your query will be returned only if you will not use "where" clause with "includes", cause using "where" clause generates OUTER JOIN request to DB with WHERE by right outer table so DB will return nothing.
Such implementation is very helpful when you need some objects (all, or some of them - using where by base model) and if there are related models just get all of them, but if not - ok just get list of base models.
On other hand if you trying to use conditions on including tables then in most cases you want to select objects only with this conditions it means you want to select Restaurants only which has meals_items.
So in your case, if you still want to use only 2 queries (and not N+1) I would probably do something like this:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
cattr_accessor :meals_of_the_week
def self.with_meals_of_the_week
restaurants = Restaurant.all
meals_of_the_week = {}
MenuItems.includes(:meal).where(date: Time.now.beginning_of_week..Time.now.end_of_week, restaurant_id => restaurants).each do |menu_item|
meals_of_the_week[menu_item.restaurant_id] = menu_item
end
restaurants.each { |r| r.meals_of_the_week = meals_of_the_week[r.id] }
restaurants
end
end
Update: Rails 4 will raise Deprecation warning when you simply try to do conditions on models
Sorry for possible typo.
I think there is some misunderstanding of this
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any
posts, all the posts would still be loaded. By using joins (an INNER
JOIN), the join conditions must match, otherwise no records will be
returned.
[from guides]
I think this statements doesn't refer to the example Post.includes(:comments).where("comments.visible", true)
but refer to one without where statement Post.includes(:comments)
So all work right! This is the way LEFT OUTER JOIN work.
So... you wrote: "If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded." Ok! But this is true ONLY when there is NO where clause! You missed the context of the phrase.

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

Rails - Sort by join table data

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

Resources