Rails 对 csv 格式的原始查询,通过控制器返回

Posted

技术标签:

【中文标题】Rails 对 csv 格式的原始查询,通过控制器返回【英文标题】:Rails raw query for csv format, to be returned via controller 【发布时间】:2014-02-03 06:23:45 【问题描述】:

我使用活动记录来获取我的故事,然后生成一个 CSV,这是在 rails cast 中完成的标准方式。但是我有很多行,这需要几分钟。我想如果我可以让 posgresql 进行 csv 渲染,那么我可以节省一些时间。

这是我现在拥有的:

query = "COPY stories TO STDOUT WITH CSV HEADER;"
results = ActiveRecord::Base.connection.execute(query);

但是这个查询的结果是空的:

 => #<PG::Result:0x00000006ea0488 @connection=#<PG::Connection:0x00000006c62fb8 @socket_io=nil, @notice_receiver=nil, @notice_processor=nil>> 
2.0.0-p247 :053 > result.count
 => 0 

更好的了解方式:

2.0.0-p247 :059 >   result.to_json
 => "[]" 

我怀疑我的控制器看起来像这样:

format.csv  send_data raw_results 

这适用于普通查询,我只是无法弄清楚将 CSV 结果返回到 rails 的 SQL 语法。

更新

将 CSV 导出从 120000 毫秒降至 290 毫秒

我的模特:

def self.to_csv(story_ids)

    csv  = []
    conn = ActiveRecord::Base.connection.raw_connection
    conn.copy_data("COPY (SELECT * FROM stories WHERE stories.id IN (#story_ids.join(','))) TO STDOUT WITH (FORMAT CSV, HEADER TRUE, FORCE_QUOTE *, ESCAPE E'\\\\');") do
      while row = conn.get_copy_data
        csv.push(row)
      end
    end
    csv.join("\r\n")
  end

我的控制器:

send_data Story.to_csv(Story.order(:created_at).pluck(:id))

【问题讨论】:

有什么办法可以直接从 DB 到 send_data 吗?我的意思是,不将其保存到 csv 数组中? @FernandoFabreti 听起来 copy_data 函数会返回需要合并到一个文件中的行。我认为没有某种变量分配的情况下没有任何方法可以组合行。您可能可以从头开始使用字符串并附加到循环中。会对性能差异感兴趣。 我必须将 csv.join("\r\n") 更改为 csv.join("\n") 才能正确生成行。它最初是添加一个额外的换行符。不确定这是否会影响其他非 *nix 机器... @penner 对我来说也很有魅力,感谢您的更新!不过,有两个简单的问题: 1. 当一行由多个涉及关联的复杂 AR 查询生成时,情况如何?然后我们如何生成单个 SQL 查询并在上面的示例中传递它? 2. 虽然它肯定会影响时间方面的性能,但它是否也会影响操作使用的内存? @FernandoFabreti 我最终将答案包装到 Enumerator 中,然后传递给 self.response_body,就像使用的 here 一样。链接的示例不完整,需要lines &lt;&lt; "#row.length.to_s(16)\r\n" 才能产生一行以使分块响应起作用。 【参考方案1】:

这个答案建立在@mu-is-too-short 提供的the answer 之上,但没有使用streaming 的临时对象。

headers['X-Accel-Buffering'] = 'no'
headers["Cache-Control"] = 'no-cache'
headers["Transfer-Encoding"] = 'chunked'
headers['Content-Type'] = 'text/csv; charset=utf-8'
headers['Content-Disposition'] = 'inline; filename="data.csv"'
headers.delete('Content-Length')
sql = "SELECT * FROM stories WHERE stories.id IN (#story_ids.join(','))"
self.response_body = Enumerator.new do |chunk|
  conn = ActiveRecord::Base.connection.raw_connection
  conn.copy_data("COPY (#sql.chomp(';')) TO STDOUT WITH (FORMAT CSV, HEADER TRUE, RCE_QUOTE *, ESCAPE E'\\\\');") do
    while row = conn.get_copy_data
      chunk << "#row.length.to_s(16)\r\n"
      chunk << row
      chunk << "\r\n"
    end
    chunk << "0\r\n\r\n"
  end
end

您还可以将gz = Zlib::GzipWriter.new(Stream.new(chunk))gz.write row 与类似的类一起使用

class Stream
  def initialize(block)
    @block = block
  end
  def write(row)
    @block << "#row.length.to_s(16)\r\n"
    @block << row
    @block << "\r\n"
  end
end

记住headers['Content-Encoding'] = 'gzip'。另见this gist。

【讨论】:

【参考方案2】:

AFAIK 您需要在底层 PostgreSQL 数据库连接上使用 copy_data 方法:

-(对象)copy_data(sql)

调用序列:

conn.copy_data( sql ) |sql_result| ...  -> PG::Result

执行复制过程以将 [sic] 数据传输到服务器或从服务器传输。

这会通过#exec 发出SQL COPY 命令。对此的响应(如果命令中没有错误)是传递给块的 PG::Result 对象,带有 PGRES_COPY_OUT 或 PGRES_COPY_IN 的状态代码(取决于指定的复制方向)。然后应用程序应使用#put_copy_data#get_copy_data 接收或传输数据行,并在完成后从块中返回。

还有一个例子:

conn.copy_data "COPY my_table TO STDOUT CSV" do
  while row=conn.get_copy_data
    p row
  end
end

ActiveRecord 的原始数据库连接包装器不知道 copy_data 是什么,但您可以使用 raw_connection 来解开它:

conn = ActiveRecord::Base.connection.raw_connection
csv  = [ ]
conn.copy_data('copy stories to stdout with csv header') do
  while row = conn.get_copy_data
    csv.push(row)
  end
end

这将在csv 中留下一组 CSV 字符串(每个数组条目一个 CSV 行),您可以通过csv.join("\r\n") 获得最终的 CSV 数据。

【讨论】:

最终不得不使用不同的查询,它可以更好地转义数据。 conn.copy_data("将故事复制到标准输出 (格式 CSV, HEADER TRUE, FORCE_QUOTE *, ESCAPE E'\\\\');").感谢您的帮助!

以上是关于Rails 对 csv 格式的原始查询,通过控制器返回的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 中链接原始 SQL 查询或如何从 Rails 中的原始 SQL 查询返回 ActiveRecord_Relation?

Rails 中的原始数据库查询

刷新 CSV 导入时 Power Query 无法添加列

RSpec在请求后发送原始JSON参数

在rails迭代CSV,记录错误但不停止循环

FasterCSV 格式错误