I have the following relationship in my model.
class Visit < ApplicationRecord
belongs_to :visitor
end
class Visitor < ApplicationRecord
has_many :visits
end
And I'm trying to do
visitors.where(visits.count.gt(value.to_i))
which throws
NoMethodError (undefined method visits for <Visitor::ActiveRecord_Relation:0x00007ff2f0c77578>):
If I understand correctly, you are to get visitors who has more than certain amount of visits. Then you can try the below way -
Visitor.joins(:visits).group('visitors.id').having('count(visitor_id) > ?', value.to_i)
Query wise what you want is:
SELECT "visitors".* FROM "visitors"
INNER JOIN "visits" ON "visits"."visitor_id" = "visitors"."id"
GROUP BY visitors.id
HAVING (count(visits.id) > ?)
LIMIT $1
The reason you want HAVING (count(visits.id) > ?) and not WHERE (count(visits.id) > ?) is that you are setting conditions on an aggregate.
You can create this query either with SQL strings:
Visitor.joins(:visits)
.group('visitors.id') # you can just use :id on PG - other drivers are not as smart
.having('count(visits.id) > ?', 5)
Or with Arel:
Visitor.joins(:visits)
.group(Visitor.arel_table[:id])
.having(Visit.arel_table[:id].count.gt(5))
Using Arel is theoretically more portable as the table names are not hard-coded.
Related
I'm using Rails 6 and I've noticed a strange behavior in Active Record when trying to get the latest record from a collection. Here is what I have:
session.rb
class Session < ApplicationRecord
has_many :participations
end
participation.rb
class Participation < ApplicationRecord
belongs_to :session
end
When I'm trying to get the latest participation with:
Participation.order(created_at: :desc).last
The SQL query generated looks like:
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" ASC
LIMIT $1
Note that I did order(created_at: :desc) but the SQL is using ASC.
However, if I change my code to:
Participation.order(created_at: :asc).last
The SQL query is doing the opposite (a DESC):
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" DESC
LIMIT $1
Does anyone have an explanation as to why it behave this way ? Is it a Rails bug ?
Seems like using last with order is causing this issue. If I remove last, ActiveRecord is generating the correct SQL (using the correct order)
ActiveRecord is optimizing the SQL statement for you. This
Participation.order(created_at: :desc).last
returns the same result as
Participation.order(created_at: :asc).first
But the latter statement is more efficient because it has to traverse fewer rows, so Rails generates SQL as if you had written it that way.
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
I have Order model and a Container model with a scope as below:
class Order < ActiveRecord::Base
has_many :containers, inverse_of: :order, dependent: :destroy
end
class Container < ActiveRecord::Base
scope :full_pickup_ready, -> { where.not(full_pickup_ready_date: nil) }
end
The order model has a field call quantity which represents the quantity of containers the order requires but is not necessarily the size of the containers association as not all container data is entered at the time of order creation.
I would like to have a scope on the Order model based on whether the count of the containers with a full_pickup_ready_date is less than the quantity field of the order.
I know I can use merge on the order model to access the scope on the containers like this:
def self.at_origin
joins(:containers).merge(Container.full_pickup_ready).uniq
end
but how can I limit the scope to orders where the total number of containers with a full_pickup_ready_date is less that the quantity field on the order?
UPDATE:
this is reasonably close, but I don't think using the select is efficient:
includes(:containers).select {|o| o.containers.full_pickup_ready.size < o.quantity }
If you're willing to forgo reuse of the scope on Container, then you should be able to use something like:
# scope on Order
joins(:containers)
.group("orders.id")
.having("count(CASE WHEN full_pickup_ready_date THEN 1 END) < orders.quantity")
I think you need to get this SQL query to work for you. So the idea is to get the SQL query right, and then translate it to "Rails".
SQL
If I am correct, this should be the SQL query you want to achieve. Maybe you can try it in your rails db
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
ActiveRecord
If this is the right query, then we can do this using rails
Order.joins(:containers).where("( SELECT COUNT(containers.id) FROM containers WHERE containers.full_pickup_ready_date IS NOT NULL ) < orders.quantity")
This should return an ActiveRecord relation. You could also do this:
sql = %{
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
}.gsub(/\s+/, " ").strip
Order.find_by_sql(sql)
Just add this in a class method (better than scope IMHO) and you are good to go.
So, your Order class should look like this:
class Order < ActiveRecord::Base
has_many :containers, inverse_of: :order, dependent: :destroy
def self.gimme_a_query_name
joins(:containers).where("( SELECT COUNT(containers.id) FROM containers WHERE containers.full_pickup_ready_date IS NOT NULL ) < orders.quantity")
end
def self.gimme_another_query_name
sql = %{
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
}.gsub(/\s+/, " ").strip
find_by_sql(sql)
end
end
I have no way to try this, but it should work with few tweak to get the SQL query right.
I hope this help!
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
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.