あるMagento開発者が、300個の個別の ProductRepositoryInterface::getById() 呼び出しを、単一のコレクションクエリに置き換えました。データベースへのアクセス数は300から1へと激減し、カテゴリーページは目に見えて高速化しました。しかし、その代償として、一部の商品がストアフロントから消えてしまい、買い物客やオーナーを困惑させることになりました。
MagentoのカテゴリーページにおけるN+1問題
そのページには300個の商品がリストされていました。コードは各商品に対して、リポジトリの getById() メソッドを介して2つのカスタム属性を取得しており、商品ごとに1つのクエリが発生していました。これが典型的なN+1パターンです。これらの商品ごとのロードを、必要なすべての行を1つのクエリで取得するコレクションに置き換えたことで、クエリ数は大幅に削減されました。フロントエンドのレイテンシは改善しましたが、商品リストには、以前は表示されていたはずのアイテムが欠落していました。
なぜコレクションは在庫切れの商品を隠してしまうのか
MagentoのCatalogInventoryモジュールは、フロントエンド用に構築されたあらゆる商品コレクションに対して、「在庫あり(in-stock)」フィルターを自動的に追加します。このフィルターは、ストア設定の Display Out of Stock Products(在庫切れ商品の表示)がオフになっている場合に有効になります。これは、在庫のない商品を表示したくないマーチャントにとって一般的な設定です。
対照的に、リポジトリは在庫フィルターを適用しません。リポジトリは単に商品が存在するかどうかを確認して返すだけであり、在庫レベルに関わらず結果を返します。コードがリポジトリの呼び出しからコレクションへと切り替わった際、在庫フィルターによって、数量がゼロの商品が静かに除外されてしまいました。エラーログは出力されず、コレクションは開発者が想定していたよりも少ない行を返しただけでした。
マーチャントにとっての意味
在庫切れのSKUが静かにドロップされるストアフロントは、いくつかの問題を引き起こします。
- フロントエンドのコンポーネントに欠落したエントリが渡され、ラベルが空になったり、誤った価格が表示されたりする。
- 「商品が消えている」という症状自体が警告を生成しないため、デバッグが困難になる。
ほとんどの自動テストは在庫がある状態のフィクスチャを使用するため、このバグはサイトが実際の在庫で稼働するまで隠れたままになることがよくあります。
在庫を隠さずに速度を維持する方法
- デフォルトの在庫フィルターをスキップする – 在庫状況に関わらずすべての商品が必要な場合は、コレクションから明示的にフィルターを削除します。Magentoでは、クエリごとに在庫プラグインを無効化または置換する方法が用意されています。
- 正確性が重要な場合はリポジトリを使用する – リポジトリは、たとえ在庫切れであっても、リクエストされたすべての商品IDが返されることを保証します。追加のクエリを抑えるために、結果をキャッシュするか、必要なIDのみをロードしてください。
- リソースモデルまたはRaw SQLを採用する – 必要な属性を取得するために、商品テーブルに直接クエリを実行します。これにより、すべてのコレクションプラグインをバイパスし、どの行を含めるかを完全に制御できます。
速度 vs 正確性
煩雑なN+1ループを単一のコレクションクエリに置き換えることは自然に感じられ、レイテンシの低下も明白です。しかし、誤った商品セットを返す高速なクエリは、最適化ではなく欠陥です。開発者は、生の速度と、不完全なカタログを表示してしまうリスクを天秤にかける必要があります。
次に注意すべき点
- 設定の乖離(Configuration drift) – Display Out of Stock Products フラグが、カスタムコレクションコードの意図した動作と一致していることを確認してください。
- プラグインの副作用 – 他のモジュールがコレクションに独自のフィルターを追加している可能性があります。クエリ戦略を変更する際は、常にプラグインスタックを確認してください。
教訓は明確です。コレクションを使ってN+1問題を解決しても、そのコレクションのデフォルトフィルターを監査しなければ意味がありません。Magentoの組み込み在庫フィルターを無視すると、カタログが静かに削ぎ落とされ、パフォーマンスの向上が収益の損失に変わってしまう可能性があります。
