最近我做了一些 rails 数据导出的工作,就是把一些特定的 ActiveRecord 数据挖出来,保存到表格里面。 这里分享一些习得的经验~
需要注意这些事情:执行速度,内存消耗,以及调试速度。
导数据的程序基本上就是一个循环体,外部获取数据集,内部把一条数据转换成表格。 在内部,往往需要通过一条记录作为主体,通过数据库逻辑关系顺藤摸瓜挖出一批数据, 这样会形成一批短查询,因为是在循环体里面,会带来很大时间上的消耗,比如:
out = []
Unit.where("active_at" < Time.now).each do |unit|
out << [unit.company.name]
end
可以先把这些查询汇总起来,一次查询掉,然后在循环体内部筛选出对应的数据:
out = []
units = Unit.where("active_at" < Time.now)
company_names = Company.where(id: units.pluck(:company_id).uniq).pluck(:id, :name).to_h
units.each do |unit|
out << [company_names[unit.company_id]]
end
查询大量数据的时候,可以首先查所有的 ID,然后分批查询,这样防止序列化大量的数据库对象:
unit_ids = Unit.where("active_at" < Time.now).pluck(:id)
group_size = 100
unit_ids.in_groups_of(group_size, false) do |ids|
Unit.where(id: ids).each do |unit|
...
end
end
数据全部缓存在一个 array 中的话,会占用大量内存,最好是通过数据流的方式一个个输出处理,用后即丢:
def export
Unit.where("active_at" < Time.now).each do |unit|
yield([unit.name])
end
end
CSV.open('out.csv', wb) do |csv|
export { |row| csv << row }
end
在循环体内部,尽量用局部变量,不用的资源会更早释放。
跑数据导出的时候,最好同时注意一下服务器的剩余内存。不要把其它服务搞挂了。
导数据最花费时间的往往还是调试过程。
调试的时候,可以只返回几条数据,检查完毕之后再全部跑。
debug = true
index = 0
Unit.where("active_at" < Time.now).each do |unit|
yield([unit.name])
index += 1
break if debug and index >= 10
end
需要考虑数据并不是很规整,做好预防性编程。
Unit.where("active_at" < Time.now).each do |unit|
yield([unit.company.try(:name)]) # 公司可能不存在
end
很多时候难免特定数据不符合预设状况,最好循环体内部记录 log,出现问题可以跟踪。 比如下面的例子,unit 没有 oldest_driver 的时候,会报错,记录了日志,就知道在哪条上面出现了问题。
logs = []
Unit.where("active_at" < Time.now).each do |unit|
logs << "unit: #{unit.id}"
oldest_driver = unit.drivers.order('age desc').first
#
yield([ordest_driver.name])
end
导出数据可以生成两个版本,一个给客户,另外一个加上一些 debug 数据方便自己分析。
def export
Unit.where("active_at" < Time.now).each do |unit|
yield([unit.name, 'debug', unit.id])
end
end
CSV.open('out.csv', wb) do |csv|
CSV.open('out_debug.csv', wb) do |debug_csv|
export { |row|
debug_csv << row
csv << row[1..1]
}
end
end
代码架构上面最重要的是职责清晰。导出数据的逻辑比较简单,分离出导数据类,以及处理数据类就可以了:
module Exporter
def iterator_to_csv(filename, iterator)
CSV.open(filename, wb) do |csv|
iterator.call { |row| csv << row }
end
end
class ExportActiveUnits
attr_accessor :time
def export
Unit.where("active_at" < time).each do |unit|
yield([unit.name])
end
end
end
eau = ExportActiveUnits.new
eau.time = Time.now
Exporter.iterator_to_csv "out.csv", eau.method(:export)