在 Camping 这类轻量 Rack 框架里,控制器动作通常直接映射到一个 HTTP 处理方法。数据库事务逻辑如果散落在各个 get、post 块中,提交与回滚代码会反复出现,还容易在异常分支里漏掉 rollback。Camping::Filters::Around 通过动作环绕过滤器把请求处理封装成可传递的块,开发者可以在进入动作前开启事务,在动作正常结束后提交,在异常抛出时回滚。本文会从执行模型、事务集成、异常边界和性能取舍四个角度展开。

Around 过滤器如何改变动作执行流程
要理解 Camping::Filters::Around 的用途,先要把它和 before、after 过滤器放在一起比较。before 过滤器在控制器动作执行前运行,适合做参数清理、登录态校验;after 过滤器在动作执行后运行,适合写日志、设置响应头。它们只能插入到动作的前面或后面,无法直接控制在动作返回之后的清理步骤。例如,在 after 里执行事务提交并不安全,因为动作内部可能已经抛出异常,此时 after 仍然被调用的话,容易提交一个半完成的状态。
Around 过滤器则不同,它拿到的不是动作本身,而是一个可以主动调用的 yield 控制权。过滤器方法在 yield 之前执行的代码位于动作之前,在 yield 之后执行的代码位于动作之后。即使动作抛出异常,只要过滤器使用 ensure 或事务块,也能保证清理逻辑运行。多个 Around 过滤器会像洋葱一样逐层包裹,外层先执行 yield 前的代码,内层先执行 yield 后的代码。
下面是一个最简的执行模型,用来展示控制流:
around do |action| puts "outer before" action.call puts "outer after" end
在 Camping 控制器中,实际使用时通常会注册一个符号指向实例方法,而不是直接写块。这样可以让事务代码与业务动作分离。
在 Camping 控制器中注册事务 Around 过滤器
假设项目使用 Sequel 作为数据库访问层,控制器里有创建文章、更新文章、删除文章等写操作。传统做法会在每个动作方法里包一层 DB.transaction。这会导致同样的提交回滚逻辑多次出现。借助 Camping::Filters::Around,可以把事务边界提升到控制器前一层,动作方法不再关心事务。
注册方式通常是在控制器模块中扩展过滤器,并使用 around 方法指定一个过滤器名称。过滤器方法通过 yield 把执行权交给下一个过滤器或最终动作。完整的示例可以是:
require 'sequel'
DB = Sequel.connect('sqlite://blog.db')
Camping.goes :Blog
module Blog::Models
class Post < Sequel::Model(:posts)
end
end
module Blog::Controllers
extend Camping::Filters::Around
around :with_db_transaction
def index
@posts = Post.order(:created_at).all
render :index
end
def create
post = Post.create(title: @input.title, body: @input.body)
redirect '/posts'
end
private
def with_db_transaction
DB.transaction do
yield
end
end
end
这里 with_db_transaction 是事务过滤器。所有进入控制器动作的请求都会先经过它。请求执行到 yield 时,会继续调用后续过滤器或 create、index 等动作。若动作正常结束,块正常返回,Sequel 会提交事务;若动作抛出异常,Sequel 会回滚事务并把异常继续向上抛。业务方法因此能够保持简单。
但要注意,这种统一包裹会把只读动作也包含进来。对于 index 这类查询,开启事务并非总是必要,后续会讨论如何按 HTTP 方法或动作名做条件控制。
异常回滚与块内 return 的边界问题
事务过滤器的核心价值是异常安全。Sequel 的 DB.transaction 本身已经在块抛出异常时执行回滚,开发者不必在每个动作里手动调用 rollback。但如果需要在回滚的同时记录错误日志,可以在 around 过滤器外层再包裹一层 rescue,记录完毕后重新把异常抛出去,避免上层框架误以为请求成功。
def with_db_transaction
DB.transaction do
yield
end
rescue => e
Camping.logger.error "transaction rolled back: #{e.class} - #{e.message}"
raise
end
还有一个容易被忽略的问题是块内使用 return。在 Ruby 中,yield 传递的块不是 lambda,块中的 return 会尝试从定义该块的方法直接返回。如果动作方法中用 return 提前结束,可能绕过 around 过滤器里的 ensure 或者让事务块以一种不直观的方式结束。最稳妥的做法是动作方法不要使用 return,改用条件分支的自然返回,或者把需要提前中断的逻辑拆成独立方法。
如果动作确实需要跳过后续步骤,建议使用 next 或通过返回值让调用方判断。这样控制流仍会回到 yield 调用点,事务过滤器能正常提交或回滚。
只读接口与过滤器顺序的优化
事务并非越宽越好。读多写少的应用中,如果 index、show 等动作只是单条查询,不需要多语句一致性快照,开启事务反而增加连接占用时间。可以为不同动作配置不同的过滤器集合,或者让事务过滤器根据请求方法判断是否需要开启事务。例如:
def with_db_transaction
if @request.post? || @request.put? || @request.delete?
DB.transaction { yield }
else
yield
end
end
这种方法保留了统一入口,又能避免只读请求承担事务开销。另一个常见做法是根据动作名加白名单,例如只对 create、update、destroy 开启事务。无论哪种方式,都要保证过滤器的条件判断不会漏掉写操作,否则可能造成数据不一致。
当多个 Around 过滤器同时存在时,注册顺序决定了包裹层级。假设有身份验证过滤器和事务过滤器,通常应把事务放在最外层,因为只有确认身份后才能进行数据操作,但事务边界的开启与提交需要覆盖整个动作。错误的顺序会让身份验证逻辑跑进事务内部,扩大锁持有时间。一个较合理的顺序是:事务、身份验证、参数清洗、动作。这样事务在最外层,动作在最内层。
另外,如果项目使用 ActiveRecord,也可以用同样的结构替换成 ActiveRecord::Base.transaction。过滤器本身的职责不依赖具体 ORM,只要传递 yield 即可。这个特点让 Camping::Filters::Around 成为轻量框架中实现横切关注点的实用工具。
Camping::Filters::Around事务管理动作环绕过滤器修改时间:2026-09-20 03:36:58