【问题标题】:Try to update, if update fails then delete (RubyOnRails-PostgreSQL)尝试更新,如果更新失败则删除(RubyOnRails-PostgreSQL)
【发布时间】:2019-10-21 10:03:09
【问题描述】:

我正在尝试更新记录,但是如果由于唯一验证而失败, 那么我正在删除该记录,因为预期的记录已经存在。

def update_dependent_model(records, foreign_key, foreign_key_id)
  records.each do |record|
    begin
      ActiveRecord::Base.transaction do
        record.update_column(foreign_key.to_sym, foreign_key_id)
      end
    rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation => e
      ActiveRecord::Base.connection.execute 'ROLLBACK'
      record.delete
    end
  end
end

我不确定这段代码有什么问题。 一些将被删除或不会被删除的记录。 日志也无济于事。显示没有错误。

另外,关于日志的奇怪之处在于:

  SQL (2.4ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 124080]]
  SQL (0.6ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 153032]]
  SQL (0.2ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 124099]]
  SQL (0.4ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 176549]]
  SQL (0.5ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 162725]]
  SQL (0.8ms)  UPDATE "records" SET "foriegn_key_id" = 220 WHERE "records"."id" = $1  [["id", 124109]]
   (0.1ms)  ROLLBACK
  SQL (0.3ms)  DELETE FROM "records" WHERE "records"."id" = $1  [["id", 124109]]

实际上,id为124080的记录已经更新,但奇怪的是在数据库中这条记录仍然存在。

【问题讨论】:

  • 124080 has gotten updated, but strangely in DB this record is still present. 为什么不应该这样?更新 foriegn_key_id 不会删除该行。
  • 您不应该显式地执行回滚,因为让事务带有异常应该已经为您完成了。很难看出这里出了什么问题:从日志看来它似乎在工作,只有 id=124109 的记录引发了异常,所以只有该记录被删除?您确定您的验证定义正确吗? (它们是在哪里定义的:在数据库中还是在 Rails 中?)
  • 一边? foriegn_key_id?错字? foreign_key_id?

标签: ruby-on-rails postgresql activerecord


【解决方案1】:

您可以将逻辑封装在服务器端函数中:

CREATE OR REPLACE FUNCTION f_update_or_delete(_id int, _value int) AS
$func$
BEGIN
   UPDATE records
   SET    foriegn_key_id = _value
   WHERE  id = _id;

EXCEPTION WHEN unique_violation THEN   -- error code 23505
   DELETE FROM records WHERE id _id;

END
$func$ LANGUAGE plpgsql;

但一开始就避免引发异常会更便宜:

WITH cte AS (
   UPDATE records
   SET    foreign_key_id = _value
   WHERE  id = _id
   AND    NOT EXISTS (
      SELECT FROM records
      WHERE  id = _id
      AND    foreign_key_id = _value
      )
   RETURNING 1
   )
DELETE FROM records
WHERE  id = _id
AND    NOT EXISTS (SELECT FROM cte);

这些和您的原始文件都不是并发安全

EXISTS 子查询中的查找与实际 UPDATE 或以下 DELETE 之间存在竞争条件。您的事务未提交的时间越长,您最终错误删除行的可能性就越大 - 因为在此期间并发事务已删除阻塞行,或者导致唯一违规的并发事务已回滚。

由于您无法锁定 Postgres 中不存在的行,因此完全安全的方法是使用 SERIALIZABLE transaction isolation(并在序列化失败时重试)或锁定整个表或使用 advisory locks以一致的方式。

【讨论】:

    【解决方案2】:

    将整个操作包装在单个事务中,如下所示

    def update_dependent_model(records, foreign_key, foreign_key_id)
      ActiveRecord::Base.transaction do
        records.each do |record|
          record.update_column(foreign_key.to_sym, foreign_key_id)
        rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation => e
          record.delete
        end
      end
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-09-12
      • 1970-01-01
      • 1970-01-01
      • 2013-01-09
      • 1970-01-01
      • 2016-09-25
      • 1970-01-01
      相关资源
      最近更新 更多