将唯一性验证的值与活动记录交换



给定一个定义如下的模型:

class Foo < ActiveRecord::Base
validates_uniqueness_of :bar
end

以及对它们执行特定操作的服务:

class FooService
class << self
def run(bars_by_foo_id)
foos = Foo.find(bars_by_foo_id.keys).index_by(&:id)
ActiveRecord::Base.transaction do
bars_by_foo_id.each do |foo_id, bar|
foo = foos[foo_id]
foo.bar = bar
foo.save!
end
end
end
end
end

当我尝试这个时:

# Works
FooService.run({ 1: "a", 2: "b" })
# Breaks
FooService.run({ 1: "b", 2: "a" })

.run的第一次调用为每个Foo分配了正确的bar,但显然,第二次调用中断了,因为当尝试将"b"分配给第一个Foo时,这个值不再是唯一的。

我试过了:

def run(bars_by_foo_id)
Foo.update(
bars_by_foo_id.keys,
bars_by_foo_id.values.map do |bar|
{ bar: bar }
end
)
end

但显然这只进行了几次单独的更新,并且仍然中断。

实现这一目标的正确方法是什么?我可以在单个 SQL 语句中执行此操作吗?我的数据库中没有唯一的约束,只有在我的模型中。

我能想到的最不丑陋的方法是:

def run(bars_by_foo_id)
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute(sql_update(bars_by_foo_id))
foos = Foo.find(bars_by_foo_id.keys)
foos.each(&:validate!)
end
end
private
def sql_update(bars_by_foo_id)
<<-SQL
UPDATE foos SET
bar = bars_by_foo_id.bar
FROM (VALUES
#{update_values(bars_by_foo_id)}
) AS bars_by_foo_id(id, bar)
WHERE bars_by_foo_id.id = foos.id;
SQL
end
def update_values(bars_by_foo_id)
bars_by_foo_id.map do |id, bar|
"(#{id}, #{bar})"
end.join(",")
end