Rails Transaction 更新多个数据库列

Posted

技术标签:

【中文标题】Rails Transaction 更新多个数据库列【英文标题】:Rails Transaction update multiple database columns 【发布时间】:2021-10-25 19:22:37 【问题描述】:

您好,我需要创建一个通过数据库运行的 rake 任务,并更新我在数据库中的 91,000 条记录。该任务需要更新 4 个参数并使它们大写,但是,其中一些参数可能是 nil 或空字符串,我试图遍历 params 数组并在它们的值与 nil 或“不同”时更新它们。关于如何使它工作的任何想法? 谢谢

task :data_uppercase => [ :environment ] do
  file = File.open("caVoters.txt", "w")
  cali_voter = CaVoter.where(tag: "ventura-d-2")
  params = [:name_first, :name_last, :city]
  updated_params = 

  CaVoter.transaction do
    cali_voter.each do |cv|
      params.each do |attribute|
        unless cv[attribute] == '' || cv[attribute] == nil
          new_param = cv[attribute].upcase
          updated_params[attribute] = new_param
        end
      end
      cv.update!(updated_params)
      puts updated_params
      file.puts("#updated_params\n")
      updated_params = 
    end
  end
  file.close
end

没有错误,但记录根本没有更新

【问题讨论】:

为什么不直接创建一条 SQL UPDATE 语句并执行它呢? SQL 可以优雅地处理空值,并且比遍历每条记录要快得多。 UPDATE some_table SET first_name = upper(first_name) 【参考方案1】:

几厘米……

    在活动记录中加载 91k 对象速度很慢并且会占用大量内存。您最好使用批处理(例如find_eachfind_in_batches)随时释放内存。否则,这可能会使用大量内存/关闭服务器(或在 SQL 中执行)。 更新数据库事务中的记录将获取更新行的排他锁。在同一事务中更新数千行将在事务期间锁定所有这些行(这可能需要一段时间,尤其是在 ruby​​ 中完成时)。如果此数据库被其他进程/线程使用,这可能会导致其他线程被阻塞,直到事务提交或中止,从而有效地导致中断。 像 postgres 这样的数据库中的update 实际上是作为插入和删除实现的。每次插入/删除都会导致对表上所有索引的更新,并用死/删除的行使表膨胀。如果这是更新表中大部分行并且性能成为问题,那么使用转换后的数据创建新表、重命名表并删除旧表比单独更新每一行要高效得多。或者,您可以删除任何索引,进行更新,然后重新创建索引并节省大量时间。如果您不创建新表,则应在执行此类操作后清理数据库以删除死行。

所以 - 总而言之,我建议使用 dbugger 建议的纯 SQL 方法,例如

task :data_uppercase => [ :environment ] do
  cali_voters = CaVoter.where(tag: "ventura-d-2")
  cali_voters.update_all("name_first = UPPER(name_first), name_last = UPPER(name_last), city = UPPER(city), street_name = UPPER(street_name)")
end

空值将保持为空,空字符串不会受到影响。 在 SQL 中更新 91k 行可能会比较高效,但这取决于表的大小、索引的数量、其他负载等。如果同时使用数据库,即使这样也可能导致性能问题,而更新被执行。如果需要性能,您可以将其分成批次,例如 5-10k,例如:

task :data_uppercase => [ :environment ] do
  cali_voters = CaVoter.where(tag: "ventura-d-2")
  cali_voters.in_batches(of: 5000) do |batch|
    batch.update_all("name_first = UPPER(name_first), name_last = UPPER(name_last), city = UPPER(city), street_name = UPPER(street_name)")
  end
end

如果您需要将名称日志写入文件(如当前代码那样)并且您不想在 SQL 中运行更新,我会采用如下方法:

task :data_uppercase => [ :environment ] do
  file = File.open("caVoters.txt", "w")
  cali_voter = CaVoter.where(tag: "ventura-d-2")
  param_names = [:name_first, :name_last, :city, :street_name]

  cali_voter.find_each do |cv|
    attributes_to_upcase = cv.attributes.slice(*param_names).compact
    updated_attributes = attributes_to_upcase.transform_values(&:upcase)
    if updated_attributes != attributes_to_upcase
      cv.update!(updated_attributes)
      file.puts("#cv.name_first\n")
    end
  end
  file.close
end

该操作是幂等的,因此您实际上不需要在事务中运行。如果您担心在失败的情况下需要重新运行并且不想不必要地第二次更新记录,您可以在查询中添加一个检查,如CaVoter.where(tag: "ventura-d-2").where("name_first != UPPER(name_first) OR name_last != UPPER(name_last)") 等,以跳过已经更新的记录。

【讨论】:

hello @melcher 感谢您的广泛回复,我尝试了您提供的第二个示例,但此错误提示 NoMethodError: undefined method `update_all' for #<0x000056369a1f6bf8>

以上是关于Rails Transaction 更新多个数据库列的主要内容,如果未能解决你的问题,请参考以下文章

Rails Rollback Transaction 尝试使用carrierwave上传图片时

表 Transaction 和 User - Ruby on Rails 应该使用啥样的关系?

事务(Transaction)

SQLIte Transaction

如何使用flex在Rails上提交多个模型?

在 Ruby on Rails 中使用 ActionCable 更新多个 div 的最佳实践是啥?