用 Arel 创建一个 WHERE (columns) IN (values) 子句?

Create a WHERE (columns) IN (values) clause with Arel?

有没有办法在 Arel 中以编程方式创建 where 子句,其中列和值分别指定?

SELECT users.*
WHERE (country, occupation) IN (('dk', 'nurse'), ('ch', 'doctor'), ...

假设输入是一个非常长的我们想要匹配的对列表。

我不是在问如何生成 WHERE AND OR 子句,这对于 ActiveRecord.

来说非常简单

到目前为止,我只有基本的字符串操作:

columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
User.where(
  "(#{columns.join(', ')}) IN (#{ pairs.map { '(?, ?)' }.join(', ')})", 
  *pairs
)

它不仅与查询的长度有关 WHERE (columns) IN (values) 也将 perform much better on Postgres (以及其他),因为它可以使用仅索引扫描,其中 OR 将导致位图扫描.

我只是在寻找可以证明使用 Arel 生成 WHERE (columns) IN (values) 查询的答案。没有别的。

我读过的所有关于 Arel 的文章都开始建立一个单一的专栏:

arel_table[:foo].eq...

而且我找不到任何涉及此案例的文档或文章。

我认为我们可以使用数组条件来做到这一点here

# notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value)

此外,如前所述 here 我们可以使用 SQL in 语句:

Model.where('id IN (?)', [array of values])

或者简单地说,正如 kdeisz 指出的那样(使用 Arel 创建 SQL 查询):

Model.where(id: [array of values])

我自己没有尝试过,但您可以尝试通过这些示例进行探索。

总是乐于助人!

这仍然会生成中间有 'OR' 的长查询。但我觉得这是实现你想要的东西的小 elegant/different 方法。

ut = User.arel_table
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]

where_condition = pairs.map do |pair|
  "(#{ut[columns[0]].eq(pair[0]).and(ut[columns[1]].eq(pair[1])).to_sql})"
end.join(' OR ')

User.where(where_condition)

我最后尝试了这种不同的方法。希望对你有用。

class User < ActiveRecord::Base
  COLUMNS = %i(
    country
    occupation
  )

  PAIRS = [['dk', 'nurse'], ['ch', 'doctor']]

  scope :with_country_occupation, -> (pairs = PAIRS, columns = COLUMNS) { where(filter_country_occupation(pairs, columns)) }

  def self.filter_country_occupation(pairs, columns)
    pairs.each_with_index.reduce(nil) do |query, (pair, index)|
      column_check = arel_table[columns[0]].eq(pair[0]).and(arel_table[columns[1]].eq(pair[1]))
      if query.nil?
        column_check
      else        
        query.or(column_check)
      end
    end.to_sql
  end 
end

称这个范围为 User.with_country_occupation 让我知道它是否适合你。

谢谢!

这样做的诀窍是正确构建分组,然后将它们传递给 Arel In 节点,例如:

columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]

User.where(
    Arel::Nodes::In.new(
        Arel::Nodes::Grouping.new( columns.map { |column| User.arel_table[column] } ),
        pairs.map { |pair| Arel::Nodes::Grouping.new(
            pair.map { |value| Arel::Nodes.build_quoted(value) } 
        )}
    )
)

以上将生成以下 SQL 语句(对于 MySQL):

"SELECT users.* FROM users WHERE (users.country, users.occupation) IN (('dk', 'nurse'), ('ch', 'doctor'))"