表达SQL UNION查询Rails的方式
Express SQL UNION query Rails way
我得到一个运行良好的查询,但在 SQL 中表示。我希望使用 ActiveRecord 查询接口表达相同的查询(Arel 也可以)。查询最好 return ActiveRecord::Relation 或者,至少,它的结果应该可以转换为客户模型数组。
目标是获取 company's
customers
没有关联 import_logs
与 remote_type = 'account'
,以及 customers
有 import_log
remote_type = 'account'
和 status = 'pending'
.
一个 customer
可以根本没有 import_logs
关联,或者每个 remote_type
都有一个 import_log
,或者只有一些 remote_types
。只能有一个关联的 import_log
具有特定的 remote_type
值。
这反映了 customer
可以导入为 account
或 contact
或两者的要求,并且 import_log
跟踪导入状态。
虽然import_log
与customer
有多态关联,但这与任务无关。
现有查询:
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
merge
协会
事实上,只有一个关联是由查询常量驱动的。
"customers"."company_id" = #{@company.id}
等于:
.merge(@company.customers)
...这样看起来更安全更明智
阿瑞尔tables
我们很快就会需要它。
customers = Customer.arel_table
NOT EXISTS ...
子查询
Arel 可以做到,唯一不太明显的是如何引用外部 table:
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
class
- 将可重复使用的部分拆分为范围和关联
根据@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" = \
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" = \
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)
是显式应用的,first
和 second
查询开始未限定范围。
我得到一个运行良好的查询,但在 SQL 中表示。我希望使用 ActiveRecord 查询接口表达相同的查询(Arel 也可以)。查询最好 return ActiveRecord::Relation 或者,至少,它的结果应该可以转换为客户模型数组。
目标是获取 company's
customers
没有关联 import_logs
与 remote_type = 'account'
,以及 customers
有 import_log
remote_type = 'account'
和 status = 'pending'
.
一个 customer
可以根本没有 import_logs
关联,或者每个 remote_type
都有一个 import_log
,或者只有一些 remote_types
。只能有一个关联的 import_log
具有特定的 remote_type
值。
这反映了 customer
可以导入为 account
或 contact
或两者的要求,并且 import_log
跟踪导入状态。
虽然import_log
与customer
有多态关联,但这与任务无关。
现有查询:
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
merge
协会
事实上,只有一个关联是由查询常量驱动的。
"customers"."company_id" = #{@company.id}
等于:
.merge(@company.customers)
...这样看起来更安全更明智
阿瑞尔tables
我们很快就会需要它。
customers = Customer.arel_table
NOT EXISTS ...
子查询
Arel 可以做到,唯一不太明显的是如何引用外部 table:
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
class - 将可重复使用的部分拆分为范围和关联
根据@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" = \
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" = \
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)
是显式应用的,first
和 second
查询开始未限定范围。