【问题标题】:Express SQL UNION query Rails wayExpress SQL UNION查询Rails方式
【发布时间】:2015-06-11 19:04:29
【问题描述】:

我得到了一个运行良好的查询,但用 SQL 表示。我想使用 ActiveRecord 查询接口表达相同的查询(Arel 也可以)。查询最好返回 ActiveRecord::Relation,或者至少,它的结果应该可以转换为 Customer 模型的数组。

目标是获取company's customersremote_type = 'account' 没有关联的import_logs,以及具有import_logremote_type = 'account'status = 'pending'customers

customer 可以根本没有关联的import_logs,或者每个remote_type 都有一个import_log,或者只有一些remote_types。只能有一个关联的import_log 与特定的remote_type 值。

这反映了customer 可以作为accountcontact 或两者导入的要求,并且import_log 跟踪导入的状态。

虽然import_logcustomer 具有多态关联,但这与任务无关。

现有查询:

Customer.find_by_sql(
  <<-SQL
    SELECT
      customers.*
    FROM
      customers
    WHERE
      company_id = #{@company.id}
      AND NOT EXISTS
          ( SELECT *
            FROM import_logs
            WHERE import_logs.importable_id = customers.id
              AND import_logs.importable_type = 'Customer'
              AND import_logs.remote_type = 'account'
          )
    UNION
    SELECT
      customers.*
    FROM
      customers,
      import_logs
    WHERE
      import_logs.importable_id = customers.id AND
      import_logs.importable_type = 'Customer' AND
      company_id = #{@company.id} AND
      import_logs.remote_type = 'account' AND
      import_logs.status = 'pending';
  SQL
)

ImportLog 模型的相关部分:

create_table "import_logs", force: true do |t|
  t.integer  "importable_id"
  t.string   "importable_type"
  t.string   "status",          default: "pending", null: false
  t.string   "remote_type"
  ...
end

add_index "import_logs", ["importable_id", "importable_type", "remote_type"], unique: true ...

class ImportLog < ActiveRecord::Base
  ...
  belongs_to :importable, polymorphic: true
  ...
end

客户模型的相关部分:

create_table "customers", force: true do |t|
  t.integer  "company_id"
  ...
end

class Customer < ActiveRecord::Base
  ...
  belongs_to :company
  has_many :import_logs, as: :importable
  ...
end

公司模式,以防万一:

class Company < ActiveRecord::Base
  ...
  has_many :customers
  ...
end

【问题讨论】:

  • 工会中的第二个查询对我来说似乎很奇怪。可能缺少加入。或EXISTS。我想知道它是否能达到预期的效果。
  • 第二个查询获取 customers 存在 import_log。它的顶部WHERE 条件与INNER JOIN 是同义词,因此它的风格不同。它确实按预期工作。
  • 只是 Rails 通常通过INNER JOINs 获取关联。好吧,我看到了它背后的逻辑。那么唯一的问题是UNION。我得想一想。与此同时,请查看scuttle.io,这可能会提供一些关于从哪里开始的提示。
  • Scuttle.io 在此查询上跳闸。这是我在此处发布之前检查的内容。

标签: sql ruby-on-rails postgresql activerecord arel


【解决方案1】:

merge协会

事实上,只有一个关联是由查询常量驱动的。

"customers"."company_id" = #{@company.id}

同理:

.merge(@company.customers)

...这看起来更安全,更明智

Arel 表

我们很快就会需要它。

customers = Customer.arel_table

NOT EXISTS ...子查询

Arel 可以做到这一点,唯一不太明显的是如何引用外部表:

ne_subquery = ImportLog.where(
                importable_type: Customer.to_s,
                  importable_id: customers[:id],
                    remote_type: 'account'
              ).exists.not

这会产生大量的 Arel AST,我们可以将其提供给 Rails 的where-statement。

现在两个查询都变得很明显了:

first  = @company.customers.where(ne_subquery)
second = @company.customers.joins(:import_logs).merge(
           ImportLog.where(
           # importable_id: customers[:id], # `joins` already does it
           importable_type: Customer.to_s,
               remote_type: 'acoount',
                    status: 'pending'
           )
         )

这几乎是一对一的转换。

联合

这是一个棘手的部分,我发现的唯一解决方案具有非常丑陋的语法并输出一些不同的查询。给定A union B,我们只能构建select X.* from (A union B) X。效果是一样的。

好吧,让我们开始吧:

Customer.from(
  customers.create_table_alias(
    first.union(second),
    Customer.table_name
  )
)

当然,为了使这个查询更具可读性,您应该:

  • 将其作为范围放在Customer 类中
  • 将可重用部分拆分为范围和关联

【讨论】:

  • 感谢您的努力!这一切都起到了建立工会的作用。此时,它会显示以下消息:ActiveRecord::StatementInvalid: PG::ProtocolViolation: ERROR: bind message supplies 0 parameters, but prepared state ment "" requires 1。显然,company_id 没有被绑定,我还没有找到解决这个问题的方法。
  • 一种肮脏的方式是在where调用中自己对company_id应用条件,而不是将其委托给关联。好吧,那会起作用。我就是不喜欢那种风格。
  • 我试过了,但没有用。也许我做错了什么,你能举个例子吗?
  • 我可以试试。导致此错误的 SQL 是什么?
  • 嗯...这很奇怪,两个$1s。看起来你必须自己绑定一个值。我不知道该怎么做,但我会看看。可能会起作用的是带有条件字符串的单独 where 子句。试试那个。
【解决方案2】:

根据@D-side 建议的代码,我能够找到一个可行的解决方案。这是最初建议的代码:

customers = Customer.arel_table

ne_subquery = ImportLog.where(
  importable_type: Customer.to_s,
  importable_id: customers['id'],
  remote_type: 'account'
).exists.not

first  = @company.customers.where(ne_subquery)
second = @company.customers.joins(:import_logs).merge(
  ImportLog.where(
    importable_type: Customer.to_s,
    remote_type: 'account',
    status: 'pending'
  )
)

Customer.from(
  customers.create_table_alias(
    first.union(second),
    Customer.table_name
  )
)

运行它会导致这个错误:

PG::ProtocolViolation: ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 1

: SELECT "customers".* FROM ( SELECT "customers".* FROM "customers"  WHERE "customers"."company_id" = $1 \
AND (NOT (EXISTS (SELECT "import_logs".* FROM "import_logs"  WHERE "import_logs"."importable_type" = 'Customer' \
AND "import_logs"."importable_id" = "customers"."id"))) UNION SELECT "customers".* FROM "customers" \
INNER JOIN "import_logs" ON "import_logs"."importable_id" = "customers"."id" \
AND "import_logs"."importable_type" = 'Customer' WHERE "customers"."company_id" = $1 \
AND "import_logs"."importable_type" = 'Customer' AND "import_logs"."remote_type" = 'contact' \
AND "import_logs"."status" = 'pending' ) "customers"

我认为这个错误是Rails issue #20077 的表现,目前尚未解决。由于该问题与参数绑定有关,因此使该绑定更加明确会有所帮助。这是一个可行的解决方案:

customers = Customer.arel_table

ne_subquery = ImportLog.where(
  importable_type: Customer.to_s,
  importable_id: customers['id'],
  remote_type: 'account'
).exists.not

first  = Customer.where(ne_subquery).where(company_id: @company.id)
second = Customer.joins(:import_logs).merge(
  ImportLog.where(
    importable_type: Customer.to_s,
    remote_type: 'account',
    status: 'pending'
  )
).where(company_id: @company.id)

Customer.from(
  customers.create_table_alias(
    first.union(second),
    Customer.table_name
  )
)

请注意,.where(company_id: @company.id) 是显式应用的,firstsecond 查询开始时是无范围的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-04
    • 1970-01-01
    • 1970-01-01
    • 2011-04-13
    • 2013-06-17
    • 1970-01-01
    • 2020-03-15
    相关资源
    最近更新 更多