From 1eca8260e9d8aa88ba1c8cced07cdde1dc301feb Mon Sep 17 00:00:00 2001 From: Ikraam Ghoor Date: Fri, 26 Jun 2026 05:01:20 +0200 Subject: [PATCH] Fixed N+1 adjustment loading in promotion recalculation Recalculating promotions loaded each line item's and shipment's adjustments with a separate query, so the adjustment query count grew with the number of items in the order. This runs on every order recalculation. Preloading the adjustments for all items in one batch keeps the query count constant regardless of order size. The same fix applies to both the legacy and the new promotion adjusters. --- .../promotion/order_adjustments_recalculator.rb | 1 + .../order_adjustments_recalculator_spec.rb | 10 ++++++++++ .../models/solidus_promotions/order_adjuster.rb | 2 ++ .../solidus_promotions/order_adjuster_spec.rb | 17 +++++++++++++++++ 4 files changed, 30 insertions(+) diff --git a/legacy_promotions/app/models/spree/promotion/order_adjustments_recalculator.rb b/legacy_promotions/app/models/spree/promotion/order_adjustments_recalculator.rb index 8d1e9d2c5b..fe57edb88e 100644 --- a/legacy_promotions/app/models/spree/promotion/order_adjustments_recalculator.rb +++ b/legacy_promotions/app/models/spree/promotion/order_adjustments_recalculator.rb @@ -15,6 +15,7 @@ def initialize(order) def call(persist: true) all_items = line_items + shipments + ActiveRecord::Associations::Preloader.new(records: all_items, associations: :adjustments).call all_items.each do |item| promotion_adjustments = item.adjustments.select(&:promotion?) diff --git a/legacy_promotions/spec/models/spree/promotion/order_adjustments_recalculator_spec.rb b/legacy_promotions/spec/models/spree/promotion/order_adjustments_recalculator_spec.rb index 06218ee391..6dbf0c4120 100644 --- a/legacy_promotions/spec/models/spree/promotion/order_adjustments_recalculator_spec.rb +++ b/legacy_promotions/spec/models/spree/promotion/order_adjustments_recalculator_spec.rb @@ -18,6 +18,16 @@ def initialize(_adjustments) let(:order) { create(:order_with_line_items, line_items_count: 1, line_items_price: 10) } let(:line_item) { order.line_items[0] } + context "with multiple line items" do + let(:order) { create(:order_with_line_items, line_items_count: 3) } + + before { order.reload } + + it "loads line item adjustments in a single query" do + expect { subject }.to make_database_queries(matching: /from .spree_adjustments..*adjustable_id. IN \(/im, count: 1) + end + end + context "when the quantity changes with a CreateQuantityAdjustments promotion" do let(:promotion) { create(:promotion, promotion_actions: [promotion_action]) } let(:promotion_action) { Spree::Promotion::Actions::CreateQuantityAdjustments.new(calculator:, preferred_group_size: 2) } diff --git a/promotions/app/models/solidus_promotions/order_adjuster.rb b/promotions/app/models/solidus_promotions/order_adjuster.rb index d9b1f53fa3..e639253110 100644 --- a/promotions/app/models/solidus_promotions/order_adjuster.rb +++ b/promotions/app/models/solidus_promotions/order_adjuster.rb @@ -22,6 +22,8 @@ def initialize(order, dry_run_promotion: nil) def call(persist: true) # rubocop:disable Lint/UnusedMethodArgument return order unless SolidusPromotions::Promotion.order_activatable?(order) + ActiveRecord::Associations::Preloader.new(records: order.line_items + order.shipments, associations: :adjustments).call + SetDiscountsToZero.call(order) DiscountOrder.new(order, promotions, dry_run: dry_run).call diff --git a/promotions/spec/models/solidus_promotions/order_adjuster_spec.rb b/promotions/spec/models/solidus_promotions/order_adjuster_spec.rb index 8429aec10f..1ced4f61d2 100644 --- a/promotions/spec/models/solidus_promotions/order_adjuster_spec.rb +++ b/promotions/spec/models/solidus_promotions/order_adjuster_spec.rb @@ -237,4 +237,21 @@ end end end + + context "with multiple line items" do + let(:order) { create(:order_with_line_items, line_items_count: 3) } + let!(:benefit) do + SolidusPromotions::Benefits::AdjustLineItem.create(promotion: promotion, calculator: calculator) + end + + before do + order_adjuster.call + order.save! + order.reload + end + + it "loads line item adjustments in a single query" do + expect { order_adjuster.call }.to make_database_queries(matching: /from .spree_adjustments..*adjustable_id. IN \(/im, count: 1) + end + end end