如何编写 Rails 查找器方法,其中 has_many 项的 none 具有非零字段?

How do I write a Rails finder method where none of the has_many items has a non-nil field?

我正在使用 Rails 5. 我有以下型号 ...

class Order < ApplicationRecord
    ...
    has_many :line_items, :dependent => :destroy

LineItem 模型有一个属性,"discount_applied." 我想 return 所有订单,其中 "discount_applied" 字段不为零的订单项的实例为零。如何编写这样的查找器方法?

一个可能的代码

Order.includes(:line_items).where.not(line_items: {discount_applied: nil})

我建议熟悉查询方法的 AR 文档。

更新

这似乎比我最初更感兴趣。而且更复杂,所以我无法给你一个工作代码。但我会研究使用 LineItem.group(order_id).having(discount_applied: nil) 的解决方案,它应该为您提供 line_items 的集合,然后将其用作子查询以查找相关订单。

首先,这实际上取决于您是想使用纯粹的 Arel 方法还是使用 SQL 是否合适。前者仅在您打算构建库时才建议使用前者,但如果您正在构建应用程序则不必要手动查询可能是您最少的麻烦)。

假设使用 SQL 没问题,应该适用于几乎所有数据库的最简单的解决方案是:

Order.where("(SELECT COUNT(*) FROM line_items WHERE line_items.order_id = orders.id AND line_items.discount_applied IS NULL) = 0")

这应该也适用于几乎所有地方(并且有更多的 Arel 和更少的手动 SQL):

Order.left_joins(:line_items).where(line_items: { discount_applied: nil }).group("orders.id").having("COUNT(line_items.id) = 0")

根据您的特定 DBMS(更具体地说:其各自的查询优化器),一个或另一个可能性能更高。

希望对您有所帮助。

如果我没理解错的话,您想要获取 none 订单项(如果有)应用了折扣的所有订单。

使用 ActiveRecord 获取这些订单的一种方法如下:

Order.distinct.left_outer_joins(:line_items).where(line_items: { discount_applied: nil })

以下是其工作原理的简要说明:

  • 该解决方案使用 left_outer_joins,假设您不会访问每个订单的行项目。您也可以使用 left_joins,这是一个别名。
  • 如果您需要为每个 Order 实例实例化订单项,请将 .eager_load(:line_items) 添加到链中,这将防止对每个订单 (N+1) 执行额外查询,即,执行order.line_items.each 在视图中。
  • 使用 distinct 对确保结果中仅包含一次订单至关重要。

更新

我之前的解决方案只是检查 discount_applied IS NULL 至少一个订单项,而不是所有订单项。以下查询应该 return 您需要的订单。

Order.left_joins(:line_items).group(:id).having("COUNT(line_items.discount_applied) = ?", 0)

这是怎么回事:

  • 该解决方案仍需要使用左外部联接 (orders LEFT OUTER JOIN line_items),以便包含没有任何关联项目的订单。
  • 将行项目分组以获得单个 Order 对象,而不管它有多少项目 (GROUP BY recipes.id)。
  • 它计算每个订单的折扣订单项数量,仅选择应用了零折扣的订单项 (HAVING (COUNT(line_items.discount_applied) = 0))。

希望对您有所帮助。

效率不高,但我认为它可以解决您的问题:

orders = Order.includes(:line_items).select do |order|
  order.line_items.all? { |line_item| line_item.discount_applied.nil? }
end

更新:

我们可以从输出结果中排除所有订单项都应用了折扣的订单,而不是查找其所有订单项都没有折扣的订单。这可以通过 where 子句中的 subquery 来完成:

# Find all ids of orders which have line items with a discount applied:
excluded_ids = LineItem.select(:order_id)
                       .where.not(discount_applied: nil)
                       .distinct.map(&:order_id)

# exclude those ids from all orders:
Order.where.not(id: excluded_ids)

您可以将它们组合在一个查找器方法中:

Order.where.not(id: LineItem
                    .select(:order_id)
                    .where.not(discount_applied: nil))

希望对您有所帮助

如果你想要所有 discount_applied 为 nil 的记录,那么:

Order.includes(:line_items).where.not(line_items: {discount_applied: nil})

(使用include避免n+1问题) 或

Order.joins(:line_items).where.not(line_items: {discount_applied: nil})

这是您问题的解决方案

order_ids = Order.joins(:line_items).where.not(line_items: {discount_applied: nil}).pluck(:id)
orders = Order.where.not(id: order_ids)

第一个查询将 return 个 Orders 的 ID,其中至少一个 line_item 具有 discount_applied。第二个查询将 return 所有 orders 其中 line_item 具有 discount_applied.

的零个实例

您无法使用经典的 rails left_joins 有效地执行此操作,但是 sql 左连接是为处理这些情况而构建的

Order.joins("LEFT JOIN line_items AS li ON li.order_id = orders.id 
                                       AND li.discount_applied IS NOT NULL")
     .where("li.id IS NULL")

一个简单的内部联接将 return 所有订单,与所有 line_items、
联接 但如果此订单没有 line_items,则忽略该订单(如 false where)
使用左连接,如果没有找到 line_items,sql 会将其连接到一个空条目以保留它

所以我们离开加入了我们不想要的line_items,并找到所有加入空line_items的订单

并避免使用 where(id: pluck(:id))having("COUNT(*) = 0") 的所有代码,这会在那天杀死您的数据库

我会使用 SQL 的 NOT EXISTS 特性,它至少在 MySQL 和 PostgreSQL

中可用

它应该是这样的

class Order
  has_many :line_items
  scope :without_discounts, -> {
    where("NOT EXISTS (?)", line_items.where("discount_applied is not null")
  }
end