一位 Magento 开发人员将 300 个独立的 ProductRepositoryInterface::getById() 调用替换为单个集合查询(collection query)。数据库查询次数从 300 次降至 1 次,分类页面的加载速度明显提升。代价是什么?部分产品从前台商店中消失了,令购物者和店主感到困惑。

Magento 分类页面上的 N+1 问题

该页面列出了 300 个产品。对于每一个产品,代码都通过 repository 的 getById() 方法获取两个自定义属性,从而为每个产品生成一次查询——这就是经典的 N+1 模式。通过使用集合(collection)一次性拉取所有所需行,取代了这些逐个产品的加载方式,大幅减少了查询次数。前端延迟得到了改善,但产品列表现在缺失了之前会出现的项目。

为什么集合会隐藏缺货商品

Magento 的 CatalogInventory 模块会自动为任何为前端构建的产品集合添加“有库存”过滤器。当商店设置中的 Display Out of Stock Products(显示缺货产品)被关闭时,该过滤器就会生效,这是不希望展示不可用商品的商家的常见选择。

相比之下,repository 永远不会应用库存过滤器。它只是检查产品是否存在并将其返回,而不管库存水平如何。当代码从 repository 调用切换到集合时,库存过滤器悄无声息地剔除了所有数量为零的产品。没有记录任何错误;集合返回的行数只是比开发人员预期的要少。

这对商家意味着什么

一个悄悄丢弃缺货 SKU 的前台商店会带来几个问题:

  • 前端组件接收到缺失的条目,导致标签为空或价格错误。
  • 调试变得更加困难,因为“产品缺失”这一症状并不会触发警告。

由于大多数自动化测试使用有库存的测试数据(fixtures),这个 bug 通常会一直隐藏,直到网站在真实库存环境下运行。

如何在不隐藏库存的情况下保持速度

  1. 跳过默认库存过滤器 – 如果你需要获取所有产品而不管其是否有货,请显式地从集合中移除该过滤器。Magento 提供了针对每个查询禁用或替换库存插件的方法。
  2. 在准确性至关重要时使用 repository – Repository 保证会返回每一个请求的产品 ID,即使它已缺货。可以通过缓存结果或仅加载所需的 ID 来控制额外的查询。
  3. 使用 resource model 或原生 SQL – 直接查询产品表以获取所需的属性。这可以绕过所有的集合插件,让你完全控制包含哪些行。

速度与正确性

将嘈杂的 N+1 循环替换为单个集合查询看起来很自然;延迟的降低是显而易见的。然而,一个返回了错误产品集的快速查询是一个缺陷,而不是优化。开发人员必须在原始速度与展示不完整目录的风险之间进行权衡。

下一步需要注意什么

  • 配置漂移 – 验证 Display Out of Stock Products 标志是否与任何自定义集合代码的预期行为相匹配。
  • 插件副作用 – 其他模块可能会向集合添加它们自己的过滤器。每当你更改查询策略时,请检查插件栈(plugin stack)。

教训很明确:使用集合修复 N+1 问题只有在你也审计了集合的默认过滤器时才算成功。忽略 Magento 内置的库存过滤器可能会悄悄修剪你的目录,将性能提升转化为收入损失。