【问题标题】:Rails SQL "select in" across several columns: where (code1, code2) in (("A", 1), ("A", 3), ("Q", 9))Rails SQL 在多个列中“选择”: where (code1, code2) in (("A", 1), ("A", 3), ("Q", 9))
【发布时间】:2018-05-03 18:21:07
【问题描述】:

我有一个业务需求,需要根据一个表中的两个字段选择记录:code1 和 code2。选择是复杂且硬编码的,没有可编码的押韵或原因,并且在表中实际存在的一百对中包括大约十对。

  • C, 1
  • C, 2
  • J, 9
  • Z, 0

注意表中还有其他“C”代码,如(C, 3)。没有组合字段将它们都捕获为一个值,例如“C3”。

SQL 支持这样的查询:Two columns in subquery in where clause 例如

SELECT * from rejection_codes
  where (code1, code2) in (("A", 1), ("A", 3), ("Q", 9))

有没有办法用 Rails 和 ActiveRecord 的 ORM 来做到这一点,而不使用原始 SQL?

如果重要的话,我正在使用 Postgres 运行 Rails 4.2.9。

* 你为什么不... *

添加一个字段:我无法控制数据库架构。如果我这样做了,我会添加一个新列作为该组的标志。或者将值连接成字符串的计算列。或者别的什么……但我做不到。

使用原始 SQL:是的……如果我不能通过 ORM 做到这一点,我可能会这样做。

【问题讨论】:

    标签: sql ruby-on-rails postgresql activerecord


    【解决方案1】:

    如果你想要这个结构,那么你可以这样做:

    pairs = [['A', 1], ['A', 3], ['Q', 9]]
    RejectionCode.where('(code1, code2) in ((?), (?), (?))', *pairs)
    

    当然,pairs.length 可能不会总是三个,所以你可以说:

    pairs = [['A', 1], ['A', 3], ['Q', 9]]
    placeholders = (%w[(?)] * pairs.length).join(', ')
    RejectionCode.where("(code1, code2) in (#{placeholders})", *pairs)
    

    是的,这是使用字符串插值来构建 SQL sn-p,但在这种情况下它是非常安全的,因为您正在构建所有字符串并且您确切地知道其中的内容。如果你把它放到一个范围内,那么至少丑陋的东西会被隐藏起来,你可以很容易地用你的测试套件来覆盖它。

    或者,您可以利用一些等价物。 in 是一个花哨的 or,所以它们的作用大致相同:

    c in (x, y, z)
    c = x or c = y or c = z
    

    记录(甚至是匿名记录)是逐列比较的,所以它们是等价的:

    (a, b) = (x, y)
    a = x and b = y
    

    意思是这样的:

    pairs = [['A', 1], ['A', 3], ['Q', 9]]
    and_pair = ->(a) { RejectionCode.where('code1 = ? and code2 = ?', *a) }
    and_pair[pairs[0]].or(and_pair[pairs[1]]).or(and_pair[pairs[2]])
    

    应该给你同样的结果。或更笼统地说:

    pairs = [['A', 1], ['A', 3], ['Q', 9], ... ]
    and_pair = ->(a) { RejectionCode.where('code1 = ? and code2 = ?', *a) }
    query = pairs[1..-1].inject(and_pair[pairs.first]) { |q, a| q.or(and_pair[a]) }
    

    同样,您希望将这种丑陋隐藏在一个范围内。

    【讨论】:

    • 我喜欢你使用 *pairs 和占位符的方式。
    • 我也更喜欢第一个版本,第二个版本可能更“Arely”,但它是一种丑陋、过于复杂和冗长 (IMO) 的方法,可以避免完全安全的字符串插值。
    【解决方案2】:

    * 这是一个不错的解决方法,但不完全是 ORM 问题的解决方案 *

    未能在 ActiveRecord 中找到正确的方法,我只是猜测,希望最好:

    class ApprovalCode < ActiveRecord::Base
    
      REJECTION_CODES = [
        ['A', '0'],
        ['R', '1'],
        ['R', '5'],
        ['R', '6'],
        ['X', 'F'],
        ['X', 'G']
      ]
    
      scope :rejection_allowed, -> { where([:code, :sub_code], REJECTION_CODES) }  # This didn't work.
    
    end
    

    那没有用。所以,我在范围内使用了原始 SQL,这确实有效:

      scope :rejection_allowed, -> { where("(code, sub_code) in (#{rejection_list})") }
    
      def self.rejection_list
        REJECTION_CODES
          .map{|code, sub_code| "('#{code}', '#{sub_code}')"}
          .join(', ')
      end
    

    我仍然希望在 ORM 中找到如何做到这一点,或者阅读有关解决问题的完全不同方法的建议。由于它们都封装在一个作用域和一个常量中,以后重构将是微不足道的,并且保持常量和作用域分开将允许无痛测试。

    【讨论】:

    • 您可能想要使用self.class.connection.quote(code) 等而不是手动添加单引号。如果您知道代码是安全的,应该没问题,但养成良好的习惯并没有什么坏处。
    猜你喜欢
    • 2016-09-27
    • 1970-01-01
    • 1970-01-01
    • 2016-04-14
    • 1970-01-01
    • 1970-01-01
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多