☰
Bullet 版本演进全解析:从 8.x 到 4.x 的 N+1 查询检测能力变迁
2026/10/15 15:55:43 网站建设 项目流程
  • 代码质量
  • 后端

【免费下载链接】bullet

help to kill N+1 queries and unused eager loading

项目地址:https://gitcode.com/gh_mirrors/bu/bullet
点击查看免费下载

本文以 Bullet 开源项目仓库根目录下的 CHANGELOG.md 为绝对主体,结合仓库内源码(如 lib/bullet.rb、lib/bullet/detector/n_plus_one_query.rb、lib/bullet/rack.rb、lib/bullet/active_record81.rb 等)逐条解读每个版本的关键变更,梳理 Bullet 从 MongoID 支持到 Rails 8.1 的完整能力演进,帮助读者理解当前版本各项配置、检测器与中间件行为的来源和设计动机。读完本文,你将能够依据版本历史判断升级影响、排查升级后的行为差异,并理解新版 N+1、unused eager loading、counter cache 检测在源码层面的具体实现。

一、关联文档与版本概览

CHANGELOG.md 记录了 Bullet 从 4.0.0 到 8.1.3(以及 Unreleased)的全部发布历史。当前仓库版本为 8.1.3(见 lib/bullet/version.rb),最新发布条目是 8.1.3(06/02/2026)。

Bullet 的定位是「帮助消灭 N+1 查询与无用的 eager loading」:它在开发过程中监视你的查询,并在出现以下三类问题时发出通知(见 README.md):

  • N+1 查询(N+1 queries):应当使用 eager loading(includes/preload)而没使用;
  • 无用的 eager loading(unused eager loading):预加载了但从未被使用的关联;
  • 多余的 COUNT 查询(counter cache):本可用 counter cache 避免的计数查询。

从版本跨度看,其支持矩阵随版本逐步变化:

版本里程碑关键支持变更
4.0.0引入 MongoID 支持
4.x ~ 5.x支持 Rails 4.x / 5.x,MongoID 4.x/5.x
5.9.0要求 Ruby 2.3+,支持 Mongo 7.x
6.x支持 Rails 6.x,新增 ActiveJob / XHR 支持
7.x支持 Rails 7.x,whitelist 更名 safelist
8.0.0支持 Rails 8,放弃 Rails 4.0/4.1,要求 Ruby 2.7+
8.1.x支持 Rails 8.1,持续修复多态、复合主键等误报

下文按时间轴自近及远逐版本解读(与 CHANGELOG 顺序一致),每个版本条目结合仓库源码解释其背后的实现动机。

二、8.1.x 系列:Rails 8.1 与多态 belongs_to 的误报修复

2.1 8.1.3(06/02/2026)

Handle inversed polymorphic belongs_to false positives(处理 inversed 多态 belongs_to 的误报)

多态关联(polymorphic belongs_to)配合inverse_of时,ActiveRecord 可能通过内存中的 inverse 记录直接返回关联对象而不发 SQL。Bullet 的call_association若把这些「本就不需要查询」的访问误判为 N+1,就会产生误报。修复逻辑在 lib/bullet/detector/n_plus_one_query.rb:调用关联时先记录call_object_associations与 call stack,然后立即return if inversed、return if inversed_objects.include?(...),保证 inversed 访问不会被当作未预加载访问。

2.2 8.1.2(05/25/2026)

  • Skip N+1 detection for optional polymorphicbelongs_towhose*_typecolumn is nil...
  • FixSet#<<corruption inUnusedEagerLoading#add_eager_loadingssplit branch

可空多态belongs_to的*_type为空时跳过检测:当多态关联的*_type列为 nil 时,ActiveRecord 会直接短路返回 nil、不执行 SQL——既然根本不发查询,预加载也毫无意义。源码中的optional_polymorphic_belongs_to_with_nil_type?正是为此引入(lib/bullet/detector/n_plus_one_query.rb):它校验关联是否为 Symbol/String、对象类是否可reflect_on_association、反射是否polymorphic?且存在foreign_type,最后检查object[foreign_type].nil?,命中即提前 return,不产生通知。

修复UnusedEagerLoading#add_eager_loadings拆分分支中Set#<<的破坏问题:add_eager_loadings在对象集合有重叠时需要拆分键(to_add/to_merge/to_delete三组操作),8.1.2 之前拆分分支可能破坏 Set 结构(lib/bullet/detector/unused_eager_loading.rb)。该方法的拆分逻辑可见:key_objects_overlap == k时整组追加,否则把重叠部分与未重叠部分拆成两个键分别合并,再删除原键。

2.3 8.1.1(04/23/2026)

  • Fix ActiveRecord 8.1 patch-level method signature compatibility; test against Rails 8.1.3.
  • Handle string associations in safelist for Action Text
  • Enhance N+1 query detection by including caller stack in association calls
  • Update external links in README.md

patch 级方法签名兼容:Rails 8.1 中find_by_sql、records、预加载相关方法的签名在不同 patch 版本间有细微差异,Bullet 必须按各版本签名 prepend 补丁,这一点从仓库中active_record5/active_record60/active_record80/active_record81等按版本拆分的补丁文件(如 lib/bullet/active_record80.rb 显式声明find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)签名)可以印证——版本化补丁正是为了对齐每个 minor 版本的精确方法签名。

Action Text 的字符串关联 safelist:Action Text 内部以字符串形式访问关联,若 safelist 只按 Symbol 匹配会漏放行。get_safelist_associations用Array.wrap(...).flat_map { |a| [a, a.to_s] }同时返回 Symbol 与其字符串形态(lib/bullet.rb),检测器计算notify_associations时即可命中(如 lib/bullet/detector/n_plus_one_query.rb)。

N+1 检测纳入 caller stack:call_association现在接受caller_stack参数并存入CallStack注册表(lib/bullet/detector/n_plus_one_query.rb),这让caller_in_project能利用注册表里的精确调用栈(见 lib/bullet/registry/call_stack.rb)而非重新采集。

2.4 8.1.0(10/23/2025)

  • Makeget_relationprivate
  • Support Rails 8.1

get_relation被设为 private,属于 API 收敛;同时正式支持 Rails 8.1,对应新增的 lib/bullet/active_record81.rb 补丁文件,以及 lib/bullet/dependency.rb 中active_record81?的版本判定(MAJOR == 8 && MINOR == 1)。

三、8.0.x 系列:Rails 8、CSP 中间件与 OpenTelemetry

3.1 8.0.8 / 8.0.7 / 8.0.6

  • 8.0.8:middleware 在 initializers 之后插入(initializer 'bullet.add_middleware', after: :load_config_initializers,见 lib/bullet.rb);修复复合主键(composite primary key)的取值——bullet_join_potential_composite_primary_key会把多个主键值 join 成"1,2"形式的 bullet_key(lib/bullet/ext/object.rb)。
  • 8.0.7:尝试把Bullet::Rack插入到正确位置。
  • 8.0.6:为 footer 样式也加 CSP nonce(见 lib/bullet/rack.rb 的footer_style(nonce));新增 OpenTelemetry 上报——这是 8.0.6 通过Bullet.opentelemetry = true接入的通道,由uniform_notifier提供能力,配置示例见 README.md。

3.2 8.0.5 / 8.0.4 / 8.0.3 / 8.0.2 / 8.0.1

  • 8.0.5:正确插入 ContentSecurityPolicy 中间件、正确解析 query string。query string 的简单解析器simple_parse_query_string位于 lib/bullet/rack.rb,支持skip_html_injection=true等参数。
  • 8.0.4:在ContentSecurityPolicy之前插入 Bullet middleware;支持 URL queryskip_html_injection=true;标记 inversed 对象更新后为 impossible。
  • 8.0.3:更新未持久化的inversed_objects——update_inversed_object会把对象从「未持久化的 object_id 键」迁移到「持久化后的 bullet_key 键」(lib/bullet/detector/n_plus_one_query.rb)。
  • 8.0.2:对象未持久化时不缓存bullet_key——lib/bullet/ext/object.rb 中bullet_key对!persisted?对象直接返回"ClassName:"而不写缓存,避免脏缓存。
  • 8.0.1:benchmark 改用 sqlite(见 perf/benchmark.rb);减少内存分配;先 require active_support 的 inflections 模块再 require delegation 模块(lib/bullet.rb)。

3.3 8.0.0(11/10/2024):大版本重构

  • Support Rails 8
  • Drop Rails 4.0 and 4.1 support
  • Require Ruby at least 2.7.0
  • Store global objects into thread-local variables
  • Avoid globally polluting::Stringand::Objectclasses

这是 CHANGELOG 中最重要的一次架构级变更:

  1. 支持 Rails 8 / 放弃 Rails 4.0、4.1:新增active_record80/active_record81补丁文件,dependency.rb不再有active_record40?/active_record41?分支(对比 lib/bullet/dependency.rb 已移除这两档)。
  2. 要求 Ruby 2.7+。
  3. 全局对象改为 thread-local 变量:所有运行时注册表(possible_objects、impossible_objects、eager_loadings、call_stacks、inversed_objects、object_associations、call_object_associations、notification_collector)全部存入Thread.current.thread_variable_*,由start_request初始化、end_request清理(lib/bullet.rb)。这是 8.0.0 让 Bullet 具备线程安全能力的根基,也是 README 中「不要运行时切换Bullet.enable,请用Bullet.skip/Bullet.pause」建议的技术原因(README.md)。
  4. 不再全局污染::String与::Object:8.0.0 把bullet_key、bullet_primary_key_value等扩展从直接 monkey-patch 改为Ruby 2.0+ refinement(using Bullet::Ext::Object/using Bullet::Ext::String,见 lib/bullet/ext/object.rb、lib/bullet/ext/string.rb),只在明确using的模块内生效,不再污染全局命名空间。

四、7.x 系列:Rails 7 支持、safelist 与配置 API 成型

4.1 7.2.0 / 7.1.6 / 7.1.5 / 7.1.4 / 7.1.3 / 7.1.2 / 7.1.1 / 7.1.0

7.1.0(10/06/2023)是配置 API 的重要里程碑:

  • 支持 Rails 7.1;
  • 新增别名:Bullet.enable?与enabled?、Bullet.enable=与enabled=互为别名(lib/bullet.rb);
  • 新增always_append_html_body选项:即使没有通知也总是追加 HTML 片段,适用于 SPA 首屏无通知的场景(需配合console或add_footer,见 README.md),实现在 lib/bullet/rack.rb 的Bullet.notification? || Bullet.always_append_html_body条件中;
  • 新增count方法的 N+1 计数查询检测:CollectionProxy#count同时触发CounterCache.add_counter_cache与NPlusOneQuery.call_association(lib/bullet/active_record81.rb);
  • counter cache 通知标题改为建议使用size:即Notification::CounterCache#title中的"Need Counter Cache with Active Record size"(lib/bullet/notification/counter_cache.rb)。

后续小版本:7.1.1 补Content-Security-Policy-Report-Onlynonce 支持与 count 方法签名;7.1.2 处理 Rails 7.1 复合主键;7.1.3 在集合关联上调用count时也触发call_association;7.1.4 在 through reflection 上也调用关联;7.1.5 修复 mongoid8;7.1.6 允许应用在通知中不包含用户(Bullet.skip_user_in_notification)。

4.2 7.0.x 系列:Rails 7.0 的误报歼灭战

  • 7.0.7 / 7.0.6:插入Bullet::Rack前检查Rails.application.config.content_security_policy与ActionDispatch::ContentSecurityPolicy::Middleware是否存在(lib/bullet.rb),避免 API-only 应用被误插中间件;
  • 7.0.5:修复 AR 7.0 的 N+1 误报、嵌套has_many :through的 eager_load 误报;respect CSP nonce;新增 avoid eager loading 的 CallStacks 支持(Bullet::Registry::CallStack记录查询对象的调用栈,见 lib/bullet/registry/call_stack.rb);减少对象迭代次数;
  • 7.0.4:修复eager_loadhas_many :through误报;mongoid7x 增加动态方法;
  • 7.0.3:Array()替换为Array.wrap()——Array.wrap对nil返回[]、对单元素对象不做数组化,语义更安全,仓库中所有add_possible_objects、get_safelist_associations均使用它;
  • 7.0.2:放弃 growl 支持(uniform_notifier 1.16.0 起移除,见 README.md);Bullet::Rack不再检查 HTML tag;
  • 7.0.1:删除*_whitelist系列方法(whitelist → safelist 过渡收尾);hack Rails 7 的ActiveRecord::Associations::Preloader::Batch;
  • 7.0.0:支持 Rails 7;修复 Mongoid 7 视图迭代;CI 从 Travis 迁到 GitHub Actions。

五、6.x 系列:ActiveJob、XHR 与 safelist 更名

  • 6.1.5:whitelist 更名为 safelist(Bullet.add_safelist/Bullet.delete_safelist,见 lib/bullet.rb);修复onload被调用两次;支持Rack::Files::Iterator响应;确保 HABTM 关联不被误标为 N+1(源码中records.first.class.name !~ /^HABTM_/的防护随处可见,如 lib/bullet/active_record81.rb)。
  • 6.1.4:新增skip_http_headers选项——不再为 API 请求添加 HTTP 头(X-bullet-footer-text/X-bullet-console-text的写入受其控制,见 lib/bullet/rack.rb)。
  • 6.1.3:ThroughAssociation在 SingularAssociation 上与 CollectionAssociation 同等对待;仅当add_footer启用时才注入 xhr_script。
  • 6.1.2 / 6.1.1:回滚「whitelist 线程安全」改动(后由 8.0.0 的 thread-local 方案彻底解决);支持 Rails 6.1。
  • 6.1.0:新增skip_html_injection标志;移除 active_record6 的 writer hack;告警使用现代includes语法(即通知 body 中的.includes([:comments])提示,见 lib/bullet/notification/base.rb)。
  • 6.0.2 / 6.0.1:完全支持 Rails 6.0;新增Bullet::ActiveJob——在around_perform中包一层Bullet.profile(lib/bullet/active_job.rb),让后台任务也能被检测;防止与 Turbolinks 配合时出现「Maximum call stack exceeded」。
  • 6.0.0:新增 XHR 支持——通过 lib/bullet/bullet_xhr.js 在前端捕获 XHR 响应头并更新 footer;支持 Rails 6.0;处理未持久化记录手动设置 ID 的情况。

六、5.x 系列:Rails 5、stack trace 过滤与生成器

6.1 5.9.0 / 5.8.0

  • 5.9.0:要求 Ruby 2.3+;支持 Mongo 7.x。
  • 5.8.0:修复 Rails 5.x 的 through reflection;修复after_save/after_create回调误报(SaveWithBulletSupport#_create_record在新记录创建时将其标记为 impossible 对象,见 lib/bullet/active_record81.rb);不对手动 preload 触发 preload 错误;mongoid6 避免多余查询;支持 mongoid6.x 的#first/#last选项;修复 mongoid 4.x/5.x 重复日志;Ruby 2.0+ 用caller_locations;stack trace 匹配支持子文件级精度;排除配置的 bundler 路径(Bundler.bundle_path,见 lib/bullet/stack_trace_filter.rb);更新uniform_notifier依赖以支持 Sentry。

6.2 5.7.0 / 5.6.0 / 5.5.0

  • 5.7.0:支持 Rails 5.2;新增Bullet.delete_whitelist(后随 safelist 更名演变为Bullet.delete_safelist,lib/bullet.rb)。
  • 5.6.0:alias_method迁移到Module#prepend——这是当前所有 ORM 补丁的技术形态(整个 lib/bullet/active_record81.rb 全部以prepend(Module.new ...)实现);新增 install generator——bundle exec rails g bullet:install会自动写入 development/test 环境配置(见 lib/generators/bullet/install_generator.rb,generator 会生成Bullet.enable = true、Bullet.bullet_logger = true、Bullet.raise = true等默认配置);新增 stack trace filter。
  • 5.5.0:展示 HTTP 请求方法(build_request_uri拼出"GET /posts?..."格式的 URL,见 lib/bullet.rb);footer 增加关闭按钮;Bullet 不支持当前 AR/Mongoid 版本时抛错(dependency.rb中的raise "Bullet does not support ...",见 lib/bullet/dependency.rb);修复 counter cache 在关联已加载时的误报、Rails 5 误报;不再支持 ActiveRecord 3.x 与 Mongoid 3.x/4.x。

6.3 5.4.0 ~ 5.0.0

  • 5.4.0:支持 Rails 5.1;把 stack trace 过滤抽成模块(lib/bullet/stack_trace_filter.rb)。
  • 5.3.0:修复带 join SQL 的 through 关联误报、through_associationtarget 为单数的情况;支持find_by_sql;修复REQUEST_URIenv。
  • 5.2.0:修复 HABTM 缺少has_cached_counter?、预加载关联无记录时的误报;支持 Rails 5.0.0。
  • 5.1.0:修复empty?与 counter cache 组合时的误报;Rails 5 的alias_method_chain弃用处理;支持非 Rails 的 Rack 响应;创建后立即查询的误报修复;多个 eager loading 查询包含相同对象时的 UnusedEagerLoading bug 修复。
  • 5.0.0(大版本):支持 Rails 5.0.0.beta1;修复has_many :through无限循环;支持 mongoid 5.0.0;对象创建后立即访问关联不要求 preload;has_many :through的 counter_cache 检测;兼容composite_primary_keysgem;修复 AR 4.2 单数关联 reader 返回 nil、belongs_to→belongs_to误报;只在 Bullet 启动时才激活 AR hacks(enable=中由orm_patches_applied保证补丁只打一次,lib/bullet.rb);运行to_sql时不执行查询;向uniform_notifier发送 backtrace;修复 sse 响应检查;动态委托可用通知器到 UniformNotifier(delegate(*available_notifiers, to: UniformNotifier),lib/bullet.rb);修复has_one接has_many、HTML body 注入位置。

七、4.x 系列:Rails 4 与 MongoID 时代的根基

  • 4.14.0:支持 Rails 4.2;打磨通知输出。
  • 4.13.0:支持 AR 关联上的include?调用(CollectionAssociation#include?触发call_association,见 lib/bullet/active_record81.rb)。
  • 4.12.0:修复 inversed 对象导致的 N+1 误报;.id替换为.primary_key_value(当前bullet_primary_key_value的前身);bullet_ar_key更名bullet_key;修复 Rails SSE 检测、测试环境使用问题。
  • 4.11.0:支持 AR 关联的empty?;跳过新记录(new record)的检测。
  • 4.10.0:更智能的 join 查询处理;支持 mongoid 4.0;线程安全(4.x 早期方案,后被 8.0.0 的 thread-local 取代);新增 debug 模式(BULLET_DEBUG=trueenv,见 lib/bullet.rb)。
  • 4.9.0:新增Bullet.stacktrace_includes选项;Ruby 2.2.0 关键字参数修复;新增 bugsnag notifier;支持 Rails 4.1.0。
  • 4.8.0:支持 Rails 4.1.0.beta1;spec 升级到 RSpec 3.0。
  • 4.7.0:新增Bullet.raise通知(测试环境抛错,UnoptimizedQueryError,见 lib/bullet.rb);新增Bullet.add_footer通知;新增 activesupport 依赖。
  • 4.6.0:修复Bullet::Rack以支持 sinatra(见 README.md 的 sinatra 配置示例)。
  • 4.5.0:提供 API 方式访问捕获的关联;允许分别禁用n_plus_one_query、unused_eager_loading、counter_cache三个检测器(对应今日的Bullet.n_plus_one_query_enable等开关,lib/bullet.rb);新增 whitelist。
  • 4.4.0:移除disable_browser_cache选项;兼容 Rails 4.0.0.beta1。
  • 4.3.0:修复非 ASCII HTML 的 content-length;支持 mongoid 2.5.x。
  • 4.2.0:新增Bullet::Dependency以检查 AR 与 mongoid 版本;支持 Rails 4;新增 airbrake notifier。
  • 4.1.0:支持 mongoid 3。
  • 4.0.0:支持 MongoID——从此 Bullet 覆盖 ActiveRecord 与 MongoID 两大 ORM 体系(当前版本支持 mongoid 4.x~9.x,见 lib/bullet/dependency.rb)。

八、从版本历史提炼的实战要点

8.1 版本选择与升级路线

  • 使用 Rails 8.x 请选8.x 系列(当前 8.1.3),Rails 4.0/4.1 应用只能停留在 8.0.0 之前;Ruby 2.7 以下必须使用 8.0.0 之前版本(lib/bullet/dependency.rb 会按::ActiveRecord::VERSION自动选择对应补丁文件)。
  • 从 6.x 升级到 8.x 时注意两个破坏性变化:whitelist相关 API 已全部移除(改用safelist),::String/::Object的全局补丁改为 refinement,自定义代码若依赖旧的 monkey-patch 行为需调整。
  • 从 7.x 升级到 8.x:检测状态改为 thread-local,若你的代码直接调用Bullet.start_request/end_request需确认线程语义。

8.2 配置项来源速查(对应 CHANGELOG 中的新增条目)

配置项引入版本源码位置
Bullet.n_plus_one_query_enable/unused_eager_loading_enable/counter_cache_enable4.5.0lib/bullet.rb
Bullet.raise4.7.0lib/bullet.rb
Bullet.add_footer4.7.0lib/bullet.rb
Bullet.stacktrace_includes4.9.0lib/bullet.rb
Bullet.add_safelist/delete_safelist4.5.0 / 5.7.0(6.1.5 更名)lib/bullet.rb
Bullet.skip_html_injection6.1.0lib/bullet/rack.rb
Bullet.skip_http_headers6.1.4lib/bullet/rack.rb
Bullet.always_append_html_body7.1.0lib/bullet/rack.rb
Bullet.enabled?/enabled=别名7.1.0lib/bullet.rb
Bullet.skip_user_in_notification7.1.6lib/bullet.rb
Bullet.opentelemetry8.0.6lib/bullet.rb
Bullet.footer_position4.7.0 后(当前默认bottom_left)lib/bullet.rb

8.3 用版本知识定位问题

  • 遇到多态belongs_to或inverse_of的误报,确认是否已升级到8.1.3(inversed 多态修复)与8.1.2(可空多态*_type修复)。
  • 遇到复合主键关联检测异常,确认使用8.0.8+(复合主键取值修复)或7.1.2+(Rails 7.1 复合主键支持)。
  • 遇到 CSP 阻止 footer/console 注入,确认使用8.0.4+且 middleware 位于 ContentSecurityPolicy 之前,并启用 nonce 传递(lib/bullet/rack.rb)。
  • 遇到 XHR 场景下 footer 不更新,确认Bullet.add_footer为 true 且未开skip_http_headers(XHR 支持始于 6.0.0,lib/bullet/bullet_xhr.js)。
  • 遇到测试环境中「无 N+1 却抛错」,检查是否启用了Bullet.raise(4.7.0 引入)并把模型测试用Bullet.start_request/end_request包裹(README.md)。

九、总结

CHANGELOG 是理解 Bullet 设计演进的最佳索引:从 4.0.0 的 MongoID 支持、4.5.0 的三大检测器独立开关与 whitelist、5.6.0 的Module#prepend重构与 install generator、6.0.0 的 XHR/ActiveJob、6.1.5 的 safelist 更名、7.1.0 的always_append_html_body与 count 检测,到 8.0.0 的 thread-local 架构与 refinement 改造,再到 8.1.x 对多态/复合主键误报的持续收敛——每一条变更都能在当前源码中找到对应实现。建议读者在升级 Bullet 时以本清单为参考,结合 README.md 的配置文档与仓库内的版本化补丁文件(lib/bullet/active_record*.rb)逐一验证行为差异,让 N+1 检测始终为你的应用把好查询性能这一关。

  • 代码质量
  • 后端

【免费下载链接】bullet

help to kill N+1 queries and unused eager loading

项目地址:https://gitcode.com/gh_mirrors/bu/bullet
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询