【问题标题】:Create a WHERE (columns) IN (values) clause with Arel?使用 Arel 创建 WHERE(列)IN(值)子句?
【发布时间】:2020-04-22 11:48:39
【问题描述】:

有没有办法在 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...

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

【问题讨论】:

  • 这个问题只是为了好奇——我只是在寻找一种更好的方法,它可以更容易地组合不同的输入,例如哈希数组。

标签: ruby-on-rails arel


【解决方案1】:

这样做的诀窍是正确构建分组,然后将它们传递给 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):

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

【讨论】:

  • 与 PG 上的广告完全一样。干得好!
【解决方案2】:

这仍然会生成长查询,其间有“OR”。但我觉得这是一种优雅/不同的方法来实现你想要的。

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)

【讨论】:

  • 这根本不是我想要的,同样的事情可以用.or(where(...))来完成
  • 在这种情况下,我认为在“where”中避免使用“or”没有什么特别的好处。您已经提到要尝试不同的输入。如果您可以分享可用的输入,我们可能会想出办法。
  • WHERE (columns) IN (values) 可以使用仅索引扫描,其中 OR 子句将强制执行 BITMAP OR 扫描。前者更容易让数据库优化。
【解决方案3】:

我已经尝试过这种不同的方法。希望它对你有用。

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,让我知道它是否适合您。

谢谢!

【讨论】:

  • 再次阅读问题。我不是在询问有关如何执行 WHERE AND OR 的更多坏建议。
  • 请查看此博客以获取Dynamic SQL Generation。我刚刚尝试使其动态化viget.com/articles/composable-sql-queries-in-rails-using-arel
  • 我已经阅读了那篇文章,其中没有任何与我的问题相关的内容。你有吗?
  • 根据您的问题标题Is there a way to programatically create a where clause in Arel where the columns and values are specified separately? 我已经尝试过了。那个时候你没有提到how to do a WHERE AND OR.
【解决方案4】:

我认为我们可以使用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”。
  • @Prabakaran 在这里是正确的。 WHERE id IN 1,2,3 将匹配 ID 为 1、2、3 的任何记录。 WHERE id, foo IN ((1,2),(3,4)) 更接近于 AND OR。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-24
  • 2011-09-07
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
相关资源
最近更新 更多