使用ActiveRecord交换唯一性验证的值

时间:2018-06-24 13:34:53

标签: ruby-on-rails activerecord

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

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的第一次调用将正确的bar分配给每个Foo,但是显然第二个中断了,因为尝试将"b"分配给第一个{ {1}},此值不再唯一。

我尝试过:

Foo

但是显然,这只会进行几次单独的更新,但仍然会中断。

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

1 个答案:

答案 0 :(得分:0)

我想出的最丑陋的方法是:

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