我有3个模型,Shop
,Client
,Product
。
一家商店有很多客户,一家商店有很多产品。
然后我有2个额外的模型,一个是ShopClient
,它将shop_id
和client_id
分组。第二个是ShopProduct
,它将shop_id
和product_id
分组。
现在我有一个接收两个参数的控制器,client_id
和product_id
。所以我想选择所有商店(在一个实例变量@shops
中),由client_id
和product_id
过滤而不重复商店。我怎么能这样做?
我希望我很清楚,谢谢。
ps:我使用Postgresql作为数据库。
答案 0 :(得分:2)
以下查询适合您。
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]}))
答案 1 :(得分:0)
我觉得解决此问题的最佳方法是使用子查询。我将首先从 ShopClient 收集所有有效的商店ID,然后从 ShopProduct 收集所有有效的商店ID。然后将它们提供给 Shop 上的where查询。这将导致一个SQL查询。
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">]>
以上查询导致下面的SQL查询。我没有指定限制,但这可能是因为我的虚拟项目使用了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]]
将两个子查询合并到一个中不会产生正确的响应:
@shops = Shop.where(id: [shop_client_ids, shop_product_ids])
#=> #<ActiveRecord::Relation []>
生成查询:
SELECT "shops".* FROM "shops" WHERE "shops"."id" IN (NULL, NULL) LIMIT ? [["LIMIT", 11]]
请记住,当您在控制台中逐个运行语句时,通常会产生3个查询。这是因为返回值使用 #inspect 方法让您查看结果。 Rails重写此方法以执行查询并显示结果。
您可以通过使用;nil
后缀语句来模拟正常应用程序的行为。这样可以确保返回nil
,并且不会在where链上调用 #inspect 方法,因此不会执行查询并将链保留在内存中。
如果要清理控制器,可能需要将这些子查询移动到模型方法中(受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
子查询优于连接的优点是,如果查询不唯一的属性,则使用连接可能最终会多次返回相同的记录。例如,假设某个产品的属性 product_type 为'physical'
或'digital'
。如果您想选择所有销售数字产品的商店,当您使用加入时,不要忘记在链上打电话 distinct ,否则同一商店可能会多次返回。
但是,如果您必须查询产品中的多个属性,并且您将在模型中使用多个帮助程序(每个帮助程序joins(:products)
)。多个子查询可能会更慢。 (假设您设置了has_many :products, through: :shop_products
。)由于Rails将所有连接减少到与单个关联相同的关联。示例:Shop.joins(:products).joins(:products)
(来自多个类方法)仍然会一次加入产品表,而子查询不会减少。
答案 2 :(得分:0)
下面的sql查询可能会为你工作。
--
-- 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 = ?
如果您在为此sql查询创建足够的AR表达式时遇到困难,请告诉我们。
答案 3 :(得分:0)
对Bansal王子提供的答案进行重复。如何为这些连接创建一些类方法?类似的东西:
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
然后你可以这样做:
@shops = Shop.with_clients(params[:client_id]).with_products(params[:product_id])
顺便说一下,我确信有人会说你应该把这些类方法放到范围内。你当然可以做到这一点。我把它作为类方法来实现,因为这是the Guide推荐的内容:
使用类方法是接受范围参数的首选方法。
但是,我意识到有些人更喜欢使用范围的美学。所以,无论哪个最让你高兴。