游标也是Enumerable:postgresql_cursor 链式调用 map、lazy 与 Rails partial 渲染实战
【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursor
postgresql_cursor 是一个面向 Rails 的 ActiveRecord PostgreSQL 游标扩展 gem:它让数据库按块(默认 1000 行)返回大数据量结果集,避免把几十万甚至上百万行一次性加载进内存。而它最容易被低估的一面是——游标对象本身就是一个 Ruby Enumerable,你可以像操作普通数组一样,对它链式调用map、lazy、inject,甚至直接把它交给 Rails 的 partial 渲染。本文用最短的篇幅带你吃透这套"游标 + 链式调用"的玩法。
一、为什么大数据量查询需要 PostgreSQL 游标
先弄清楚它解决了什么问题。当你执行Product.where("id>0").each { |p| ... }时,数据库会把全部匹配行一次性返回给 ActiveRecord,每一行都被实例化成对象后堆成一个大数组——数据量一大,内存直接"膨胀",严重的会 OOM 崩溃。
Rails 自带的find_each/find_in_batches虽然也是分块处理,但有明显短板:
- 只能按主键顺序遍历,无法指定任意排序;
- 主键必须是数值类型;
- 每取一块数据,查询会重新执行一遍。
而 PostgreSQL 的游标(Cursor)由数据库端保持结果集位置,程序每次只FETCH一小块行(比如 1000 行),处理完再取下一块,直到取完。排序随便写,复杂查询只执行一次,内存占用始终平稳。postgresql_cursor 就是把这个机制无缝接入了 ActiveRecord。
二、安装 postgresql_cursor:一行 Gemfile 即可
在项目的Gemfile中加入一行,即可使用全部游标能力:
gem 'postgresql_cursor'该 gem 依赖activerecord >= 6.0,需要 PostgreSQL 数据库支持(依赖关系可参考 postgresql_cursor.gemspec)。如果你想先把源码拉到本地研究,执行:
git clone https://gitcode.com/gh_mirrors/po/postgresql_cursor装好后,任意 Model 的查询链上就多了each_row、each_instance、each_row_by_sql等一系列方法(由 lib/postgresql_cursor/active_record/sql_cursor.rb 和 lib/postgresql_cursor/active_record/relation/cursor_iterators.rb 注入)。
三、核心机制:游标为什么"天生"是 Enumerable
秘密在核心文件 lib/postgresql_cursor/cursor.rb 的第 26 行——Cursor类直接include Enumerable:
module PostgreSQLCursor class Cursor include Enumerable # 游标即集合 # ... end end再配合一个精巧的设计:each_row/each_instance等方法如果不传 block,返回的就是游标对象本身(见 lib/postgresql_cursor/active_record/relation/cursor_iterators.rb 中return cursor的写法)。
这意味着你可以把游标"传着走":先创建、再赋值给变量、在任意时机迭代,还能自由拼接 Enumerable 全家桶——map、select、reduce、any?、first、count……数据流本身依然是从数据库按块拉取,只是操作接口完全变成了集合风格。
四、实战一:对游标链式调用 map
最直观的用法,是"查询 → 游标 → 映射"一气呵成,官方 README 中的示例非常经典:
# 每行返回 Hash(字符串键值),速度最快 Product.each_row.map { |r| r["id"].to_i } #=> [1, 2, 3, ...] # 每行返回 ActiveRecord 实例 Product.each_instance.map { |r| r.id }.each { |id| p id }对比一下不用游标时的写法,链式调用让"批量取字段"这件事变得极其顺手。官方测试 test/test_postgresql_cursor.rb 里的test_enumerables也验证了any?这类 Enumerable 方法可以直接用在游标上。
⚠️ 注意:
map最终会把结果收集成一个数组。如果只是想"取某一列的值",小数据量完全够用;但数据量极大时,map本身就吃内存,此时请看下一节的lazy方案。
五、实战二:lazy 惰性游标,把内存压到最低
游标"取数"本来就是惰性的(一次只装 1000 行),而再链上一个lazy,可以让整条处理链都保持惰性:
# 遍历全表累加库存,任何时刻内存里只有"一块"数据 Product.each_instance.lazy.inject(0) { |sum, p| sum + p.quantity } #=> 499500lazy.inject不会先把所有行物化,而是边取边算,处理完一块就释放一块——这正是"游标 + lazy"组合的威力。同理,lazy.map { ... }.first、lazy.take(5)这类操作也能在只取少量结果时提前终止,避免无谓拉取。
六、实战三:把游标直接交给 Rails partial 渲染
很多人不知道:游标还能直接当作渲染集合丢进 view:
render partial: "product", collection: Product.each_instance render partial: "product", collection: Product.each_rowRails 渲染集合时会先调用size,而游标专门处理了这一点——lib/postgresql_cursor/cursor.rb 第 74-78 行的注释写得很直白:"ActiveRecord 渲染集合时会调用 #size,这里定义并返回一个占位值"(返回 -1),于是渲染流程顺利走通,partial 会按渲染引擎的节奏逐行消费游标。对于"把大结果集分页展示给前端"这类场景,这是少见的优雅。
七、新手必看的实用技巧与常见坑
- 🔹小结果集别用游标:游标有额外的 DECLARE/FETCH/CLOSE 开销,几百行的查询直接用
select/pluck更快。 - 🔹调
block_size:每次从数据库取多少行由block_size控制(默认 1000)。配合lock(FOR UPDATE)逐行更新时,官方建议block_size <= 10,避免长时间锁住大批行引发死锁。 - 🔹Hash vs Instance:
each_row返回字符串值的 Hash,不做类型转换,处理量最大时最快;需要时间、数值等 Ruby 类型时改用each_instance(惰性转换,只转你读到的属性)。 - 🔹用
select裁列:Product.select(:id, :name).each_instance { ... },只取需要的列,网络和内存双赢。 - 🔹
while:/until:提前退出:所有游标方法都支持选项 hash,例如each_row(until: :done),block 返回值命中即停止拉取。 - 🔹手动
fetch需包在事务里:如果你自己持有游标对象逐行cursor.fetch,要放在Product.transaction do ... end内,或用with_hold: true让游标跨事务存活。 - 🔹
pluck_rows/pluck_instances:想要"plucked 数组"又要游标的分块取数时,它们是 pluck 的游标版替代。
八、结语:源码值得十分钟
postgresql_cursor 的核心就三个文件,十分钟能读透:
- lib/postgresql_cursor/cursor.rb:游标核心,
Enumerable混入、open/fetch/close、block_size、while/until控制; - lib/postgresql_cursor/active_record/relation/cursor_iterators.rb:Relation 上的
each_row、each_instance、批处理与pluck_rows; - lib/postgresql_cursor/active_record/sql_cursor.rb:类级原始 SQL 入口,如
each_row_by_sql。
想动手实验,可以跑起 test-app/app.rb 里的迷你示例应用(先rake setup建库,再rake console)。一句话总结:把游标当集合用,map 管方便,lazy 管内存,partial 管展示——这就是 postgresql_cursor 处理大数据量的完整姿势。
【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考