将Rails 5模型的includes移至default_scope替代控制器调用的潜在弊端咨询
includes放入Rails模型的default_scope的重大弊端 首先得明确:把includes塞进default_scope是非常不推荐的做法,会带来一堆隐性的坑,我结合你的例子给你拆解几个核心问题:
1. 无差别预加载=性能浪费
你现在遇到的Bullet提示反转就是最直接的信号:之前是你用到car_model却没预加载,Bullet提醒你加includes;现在是default_scope强制预加载了car_model,但很多场景下你根本用不到这个关联(比如只查询品牌名称、ID的列表页),所以Bullet反过来提醒你“取消预加载”。
举个具体的例子:如果你的后台有个品牌统计页面,只需要统计每个品牌的数量,执行Brand.count的时候,default_scope会强制触发SELECT * FROM car_models WHERE brand_id IN (...)的查询,但这个查询完全是多余的——你根本不需要用到car_model的数据,却平白增加了数据库的查询压力和内存占用。
2. 破坏查询的灵活性与可预测性
default_scope是全局生效的,这意味着所有查询都会默认带上这个includes,不管你需不需要。比如:
- 你想写
Brand.where(country: "Japan").select(:id, :name)来只获取品牌的ID和名称,结果因为default_scope的存在,还是会把所有car_model的字段都查出来,完全违背了select优化的初衷。 - 如果某天你需要取消这个预加载,得手动用
unscope或者reload,比如Brand.unscope(:includes).find(1),这不仅增加了代码复杂度,还容易让其他开发者踩坑——没人会想到一个基础的Brand.find居然会默认加载关联。
3. 关联嵌套时的意外灾难
假设后续你给CarModel也加了default_scope { includes(:cars) },那么查询Brand的时候会触发连锁预加载:Brand→CarModel→Cars,哪怕你只需要品牌的名字,也会把所有关联的车型、车辆数据都查出来,结果集可能变得异常庞大,甚至出现笛卡尔积问题(比如一个品牌有100个车型,每个车型有100辆车,查询一个品牌就会返回10000条冗余数据)。
4. 测试复杂度飙升
写测试的时候,你经常需要创建孤立的Brand实例来测试逻辑,但default_scope会强制加载car_model,这意味着你每次创建测试数据都得同时创建关联的car_model,否则可能触发查询错误;而且测试运行速度会变慢,因为每次查询都要额外加载关联数据。
更优的替代方案
既然你想避免在控制器里重复写includes(:car_model),不如用**命名作用域(named scope)**来实现按需预加载:
# 在Brand模型中 scope :with_car_models, -> { includes(:car_model) }
然后在需要预加载的控制器里用:
# 控制器中 @brands = Brand.with_car_models.all
不需要预加载的时候直接用Brand.all即可,清晰可控,完全避免了default_scope带来的各种问题。
内容的提问来源于stack exchange,提问作者Ahmed Elkoussy

