首页 > 解决方案 > 使用 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给 each Foo,但第二次调用显然会中断,因为当尝试分配"b"给 firstFoo时,该值不再是唯一的。

我试过了:

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 语句中进行此操作吗?我的数据库中没有唯一约束,只有我的模型中。

标签: ruby-on-railsactiverecord

解决方案


我能想出的最不丑的方法是:

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

推荐阅读