【问题标题】:Grouping AND and OR clauses with Arel使用 Arel 对 AND 和 OR 子句进行分组
【发布时间】:2013-02-23 06:42:46
【问题描述】:

我正在尝试在我的 rails 模型中构建一个范围,当它被调用时,它将在 7 个布尔字段上为我提供一组嵌套的 AND 和 OR 子句。为了清楚起见,这里有一个简化的列名示例:

SELECT * FROM mytable
WHERE (a AND b AND c) OR (a AND d AND e) OR (a AND f AND g);

请注意,列 a 出现在所有三个子句中。另一种写法是:

SELECT * FROM mytable
WHERE a AND ((b AND c) OR (d AND e) OR (f AND g));

Arel 似乎对第二种形式不太宽容。我已经非常接近以下范围:

scope :needs_any_delivery, lambda {
  table = self.arel_table
  common_assert = table[:a].eq(true)
  where(
    common_assert.and(
      table[:b].eq(true).and(
        table[:c].eq(false)
      )
    ).or(
      common_assert.and(
        table[:d].eq(true).and(
          table[:e].eq(false)
        )
      ).or(
        common_assert.and(
          table[:f].eq(true).and(
            table[:g].eq(false)
          )
        )
      )
    )
  )
}

这会产生以下查询:

SELECT * FROM mytable
WHERE (
  (a = 't' AND b = 't' AND c = 'f'
    OR (a = 't' AND d = 't' AND e = 'f' OR a = 't' AND f = 't' AND g = 'f')
  )
)

已接近,但第三个AND 组并未与第二个AND 组分开。我发现如果我在第三组末尾添加一些额外的虚假 or 子句,那么 Arel 会适当地将第三个子句单独分组......但这似乎是一个 hack。

想知道是否有任何 Rails/arel 大师有任何想法。谢谢!

【问题讨论】:

    标签: ruby-on-rails activerecord arel


    【解决方案1】:

    如果您像我一样并且真的想继续使用 Arel 来实现此功能,我发现创建一个新的 or 方法似乎是最佳途径。

    我添加了一个名为 arel_fixed_or.rb 的新初始化程序,其内容如下:

    Arel::Nodes::Node.class_eval do
    
      ###
      # Factory method to create a Nodes:Or that has a Nodes::Grouping
      # node as a child.
      def fixed_or right
        Arel::Nodes::Or.new self, Arel::Nodes::Grouping.new(right)
      end
    
    end
    

    无论您通常想要使用or 语句,您都可以继续使用fixed_or,它将在您的子句末尾附加一个或-ed 分组语句,这对我来说应该是预期的方法。如果这对您没有意义,请随意删除 Arel::Nodes::Grouping 部分。

    需要注意的一点是,您可能仍然需要手动放置分组,以便正确的元素被括号括起来。例如:

    table = Arel::Table.new(:mytable)
    common_assert = table[:a].eq(true)
    
    first_or = table[:b].eq(true).and(table[:c].eq(true))
    second_or = table[:d].eq(true).and(table[:e].eq(true))
    third_or = table[:f].eq(true).and(table[:g].eq(true))
    
    common_assert.and(
      table.grouping(
        table.grouping(first_or)
          .fixed_or(second_or)
          .fixed_or(third_or)
      )
    )
    

    它的to_sql 输出变为:

    "mytable"."a" = 't' AND (
      ("mytable"."b" = 't' AND "mytable"."c" = 't') OR
      ("mytable"."d" = 't' AND "mytable"."e" = 't') OR
      ("mytable"."f" = 't' AND "mytable"."g" = 't')
    )
    

    【讨论】:

      【解决方案2】:

      除非我读错了,否则使用 active_record_or 之类的东西可能比直接使用 arel 更容易。

      使用该 gem,您应该能够通过以下方式获得正确的结果:

      common_assert = where(a: true) # WHERE a
      option_one = where(b: true).where(c: true) # b AND c
      option_two = where(d: true).where(e: true) # d AND e
      option_three = where(f: true).where(g: true) # f AND g
      combined_optionals = option_one.or.option_two.or.option_three # (b AND c) OR (d AND e) OR (f AND g)
      common_assert.merge(combined_optionals) # WHERE a AND ((b AND c) OR (d AND e) OR (f AND g))
      

      【讨论】:

      • 有趣。我不确定在这种情况下我会使用除 Arel 之外的任何东西,但感谢您的回复和回答!
      【解决方案3】:

      你也可以

      def dnf(clauses)
        clauses
          .map { |clause| clause.reduce(:and) }
          .reduce(:or)
      end
      
      table = Arel::Table.new(:some_fancy_table)
      table[:a].eq(true).and dnf [
        [table[:b].eq(true), table[:c].eq(false)],
        [table[:d].eq(true), table[:e].eq(false)],
        [table[:f].eq(true), table[:g].eq(false)],
      ]
      

      另外,我认为您实际上不需要在 and-connected 子句周围加上括号,请参阅SQL Logic Operator Precedence: And and Or

      【讨论】:

        猜你喜欢
        • 2014-07-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-18
        • 2012-12-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多