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 << "#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
发出SQLCOPY
命令。对此的响应(如果命令中没有错误)是传递给块的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?