How to adjust ActiveRecord (or straight SQL) query to include zero count records on a JOIN with a WHERE clause

橙三吉。 提交于 2019-12-25 04:58:33

问题


I have a question about forming a query in ActiveRecord but also provided the SQL for those that aren’t familiar with ActiveRecord.

I have the following model class:

class Shoe < ActiveRecord::Base
  has_many :purchases

  def self.available_shoes
    #show all shoes that have been purchased less than num_in_stock
    num_in_stock = 3
    Shoe.includes(:purchases)
        .group("purchases.shoe_id")
        .having("COUNT(purchases.shoe_id) < ?", num_in_stock)
  end

  def self.available_shoes_for_user(user)
    #show all available_shoes that a user hasn’t already purchased
    Shoe.available_shoes.where("purchases.user_id != ?", user.id)
  end
end

The method Shoe.available_shoes works as expected in that it will return all shoes that have been purchased less times than the amount available in stock (in this case 3) including shoes that have been purchased zero times.

The problem arises when I call Shoe.available_shoes_for_user(user), it will show all shoes that have existing purchases, but it doesn’t show available shoes that have zero purchases.

I have extracted the raw SQL below:

#Shoe.available_shoes
SELECT shoes.*, purchases.* FROM shoes LEFT OUTER JOIN purchases ON purchases.shoe_id = shoes.id GROUP BY shoe_id HAVING COUNT(purchases.shoe_id) < 3

#Shoe.available_shoes_for_user(User.find(5))
SELECT shoes.*, purchases.* FROM shoes LEFT OUTER JOIN purchases ON purchases.shoe_id = shoes.id WHERE (purchases.user_id != 5) GROUP BY shoe_id HAVING COUNT(purchases.shoe_id) < 3

Question 1: How can I get Shoe.available_shoes_for_user(user) to work as intended (ie, show all shoes purchased less than 3 times (including zero times) that the client hasn’t already purchased?

Question 2: In the long run, what would be the optimal solution to this problem when there are hundreds of thousands/millions of shoes?

Thanks in advance!

============================ A SOLUTION (only works in MySQL not PostgresSQL) Thanks to @Frederick Cheung for pointing the way

class Shoe < ActiveRecord::Base
  has_many :purchases

  def self.available_shoes
    #show all shoes that have been purchased less than num_in_stock
    num_in_stock = 3
    Shoe.includes(:purchases)
        .group("purchases.shoe_id")
        .having("COUNT(purchases.shoe_id) < ?", num_in_stock)
  end

  def self.available_shoes_for_user(user)
    #show all available_shoes that a user hasn’t already purchased
    Shoe.available_shoes
         .joins("LEFT OUTER JOIN purchases purchased_by_user ON purchased_by_user.shoe_id = shoes.id AND purchased_by_user.user_id = '#{user.id}'")
         .where("purchased_by_user.id IS NULL")
  end
end

回答1:


If there are no purchases for a shoe, the left join means that for that shoe the result set will have NULL for all the columns from the purchases table.

You're applying a where purchases.user_id != 5 clause to remove purchases by that user but that is also filtering out the NULL rows. You could change that condition to

where purchases.id is null or purchases.user_id != 5

But i think this will still not do what you want: if a shoe had been purchased by that customer and by some other customer this would just make the reported count be 1 rather than 2 instead of removing the row altogether

You could do this by joining the purchases table a second time

left outer join purchases on purchases.shoe_id = shoes.id
left outer join purchases purchased_by_user on purchased_by_user.shoe_id = shoes.id and purchased_by_user.user_id = 5

Your where clause then needs to just ensure that purchased_by_user.id is null, signifying that the database could find no purchases for that shoe with that user_id



来源:https://stackoverflow.com/questions/11406221/how-to-adjust-activerecord-or-straight-sql-query-to-include-zero-count-records

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!