Rails 5从两个不同的表中选择并获得一个结果

时间:2018-02-14 19:36:27

标签: ruby-on-rails ruby postgresql activerecord

我有3个模型,ShopClientProduct

一家商店有很多客户,一家商店有很多产品。

然后我有2个额外的模型,一个是ShopClient,它将shop_idclient_id分组。第二个是ShopProduct,它将shop_idproduct_id分组。

现在我有一个接收两个参数的控制器,client_idproduct_id。所以我想选择所有商店(在一个实例变量@shops中),由client_idproduct_id过滤而不重复商店。我怎么能这样做?

我希望我很清楚,谢谢。

ps:我使用Postgresql作为数据库。

4 个答案:

答案 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

Rails子查询与连接

子查询优于连接的优点是,如果查询不唯一的属性,则使用连接可能最终会多次返回相同的记录。例如,假设某个产品的属性 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表达式时遇到困难,请告诉我们。

Btw, here is a mock

答案 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推荐的内容:

  

使用类方法是接受范围参数的首选方法。

但是,我意识到有些人更喜欢使用范围的美学。所以,无论哪个最让你高兴。