Просмотр исходного кода

perf(core): Optimizations to the addItemToOrder path

There are a number of DB operations that are quite expensive. This commit
makes optimizations so that the DB does less work when adding an item to
an Order.
Michael Bromley 1 год назад
Родитель
Сommit
70ad853163

+ 6 - 0
packages/core/src/entity/product-variant/product-variant.entity.ts

@@ -107,6 +107,9 @@ export class ProductVariant
     @ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
     @ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
     featuredAsset: Asset;
 
 
+    @EntityId({ nullable: true })
+    featuredAssetId: ID;
+
     @OneToMany(type => ProductVariantAsset, productVariantAsset => productVariantAsset.productVariant, {
     @OneToMany(type => ProductVariantAsset, productVariantAsset => productVariantAsset.productVariant, {
         onDelete: 'SET NULL',
         onDelete: 'SET NULL',
     })
     })
@@ -116,6 +119,9 @@ export class ProductVariant
     @ManyToOne(type => TaxCategory, taxCategory => taxCategory.productVariants)
     @ManyToOne(type => TaxCategory, taxCategory => taxCategory.productVariants)
     taxCategory: TaxCategory;
     taxCategory: TaxCategory;
 
 
+    @EntityId({ nullable: true })
+    taxCategoryId: ID;
+
     @OneToMany(type => ProductVariantPrice, price => price.variant, { eager: true })
     @OneToMany(type => ProductVariantPrice, price => price.variant, { eager: true })
     productVariantPrices: ProductVariantPrice[];
     productVariantPrices: ProductVariantPrice[];
 
 

+ 5 - 1
packages/core/src/entity/product/product.entity.ts

@@ -1,4 +1,4 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
 import { Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
 
 
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
@@ -8,6 +8,7 @@ import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
 import { Channel } from '../channel/channel.entity';
 import { Channel } from '../channel/channel.entity';
 import { CustomProductFields } from '../custom-entity-fields';
 import { CustomProductFields } from '../custom-entity-fields';
+import { EntityId } from '../entity-id.decorator';
 import { FacetValue } from '../facet-value/facet-value.entity';
 import { FacetValue } from '../facet-value/facet-value.entity';
 import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
 import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
 import { ProductVariant } from '../product-variant/product-variant.entity';
 import { ProductVariant } from '../product-variant/product-variant.entity';
@@ -47,6 +48,9 @@ export class Product
     @ManyToOne(type => Asset, asset => asset.featuredInProducts, { onDelete: 'SET NULL' })
     @ManyToOne(type => Asset, asset => asset.featuredInProducts, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
     featuredAsset: Asset;
 
 
+    @EntityId({ nullable: true })
+    featuredAssetId: ID;
+
     @OneToMany(type => ProductAsset, productAsset => productAsset.product)
     @OneToMany(type => ProductAsset, productAsset => productAsset.product)
     assets: ProductAsset[];
     assets: ProductAsset[];
 
 

+ 29 - 25
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -10,6 +10,7 @@ import {
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
 import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
+import { IsNull } from 'typeorm';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
 import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
@@ -164,12 +165,13 @@ export class OrderModifier {
             return existingOrderLine;
             return existingOrderLine;
         }
         }
 
 
-        const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
+        const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId, order);
+        const featuredAssetId = productVariant.featuredAssetId ?? productVariant.featuredAssetId;
         const orderLine = await this.connection.getRepository(ctx, OrderLine).save(
         const orderLine = await this.connection.getRepository(ctx, OrderLine).save(
             new OrderLine({
             new OrderLine({
                 productVariant,
                 productVariant,
                 taxCategory: productVariant.taxCategory,
                 taxCategory: productVariant.taxCategory,
-                featuredAsset: productVariant.featuredAsset ?? productVariant.product.featuredAsset,
+                featuredAsset: featuredAssetId ? { id: featuredAssetId } : undefined,
                 listPrice: productVariant.listPrice,
                 listPrice: productVariant.listPrice,
                 listPriceIncludesTax: productVariant.listPriceIncludesTax,
                 listPriceIncludesTax: productVariant.listPriceIncludesTax,
                 adjustments: [],
                 adjustments: [],
@@ -189,26 +191,15 @@ export class OrderModifier {
                 .set(orderLine.sellerChannel);
                 .set(orderLine.sellerChannel);
         }
         }
         await this.customFieldRelationService.updateRelations(ctx, OrderLine, { customFields }, orderLine);
         await this.customFieldRelationService.updateRelations(ctx, OrderLine, { customFields }, orderLine);
-        const lineWithRelations = await this.connection.getEntityOrThrow(ctx, OrderLine, orderLine.id, {
-            relations: [
-                'taxCategory',
-                'productVariant',
-                'productVariant.productVariantPrices',
-                'productVariant.taxCategory',
-            ],
-        });
-        lineWithRelations.productVariant = this.translator.translate(
-            await this.productVariantService.applyChannelPriceAndTax(
-                lineWithRelations.productVariant,
-                ctx,
-                order,
-            ),
-            ctx,
-        );
-        order.lines.push(lineWithRelations);
-        await this.connection.getRepository(ctx, Order).save(order, { reload: false });
-        await this.eventBus.publish(new OrderLineEvent(ctx, order, lineWithRelations, 'created'));
-        return lineWithRelations;
+        order.lines.push(orderLine);
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder()
+            .relation('lines')
+            .of(order)
+            .add(orderLine);
+        await this.eventBus.publish(new OrderLineEvent(ctx, order, orderLine, 'created'));
+        return orderLine;
     }
     }
 
 
     /**
     /**
@@ -896,11 +887,24 @@ export class OrderModifier {
     private async getProductVariantOrThrow(
     private async getProductVariantOrThrow(
         ctx: RequestContext,
         ctx: RequestContext,
         productVariantId: ID,
         productVariantId: ID,
+        order: Order,
     ): Promise<ProductVariant> {
     ): Promise<ProductVariant> {
-        const productVariant = await this.productVariantService.findOne(ctx, productVariantId);
-        if (!productVariant) {
+        const variant = await this.connection.findOneInChannel(
+            ctx,
+            ProductVariant,
+            productVariantId,
+            ctx.channelId,
+            {
+                relations: ['product', 'productVariantPrices', 'taxCategory'],
+                loadEagerRelations: false,
+                where: { deletedAt: IsNull() },
+            },
+        );
+
+        if (variant) {
+            return await this.productVariantService.applyChannelPriceAndTax(variant, ctx, order);
+        } else {
             throw new EntityNotFoundError('ProductVariant', productVariantId);
             throw new EntityNotFoundError('ProductVariant', productVariantId);
         }
         }
-        return productVariant;
     }
     }
 }
 }

+ 61 - 5
packages/core/src/service/services/order.service.ts

@@ -53,7 +53,6 @@ import {
     CancelPaymentError,
     CancelPaymentError,
     EmptyOrderLineSelectionError,
     EmptyOrderLineSelectionError,
     FulfillmentStateTransitionError,
     FulfillmentStateTransitionError,
-    RefundStateTransitionError,
     InsufficientStockOnHandError,
     InsufficientStockOnHandError,
     ItemsAlreadyFulfilledError,
     ItemsAlreadyFulfilledError,
     ManualPaymentStateError,
     ManualPaymentStateError,
@@ -61,6 +60,7 @@ import {
     NothingToRefundError,
     NothingToRefundError,
     PaymentOrderMismatchError,
     PaymentOrderMismatchError,
     RefundOrderStateError,
     RefundOrderStateError,
+    RefundStateTransitionError,
     SettlePaymentError,
     SettlePaymentError,
 } from '../../common/error/generated-graphql-admin-errors';
 } from '../../common/error/generated-graphql-admin-errors';
 import {
 import {
@@ -561,6 +561,7 @@ export class OrderService {
                 enabled: true,
                 enabled: true,
                 deletedAt: IsNull(),
                 deletedAt: IsNull(),
             },
             },
+            loadEagerRelations: false,
         });
         });
         if (variant.product.enabled === false) {
         if (variant.product.enabled === false) {
             throw new EntityNotFoundError('ProductVariant', productVariantId);
             throw new EntityNotFoundError('ProductVariant', productVariantId);
@@ -1776,22 +1777,77 @@ export class OrderService {
             }
             }
         }
         }
 
 
+        // Get the shipping line IDs before doing the order calculation
+        // step, which can in some cases change the applied shipping lines.
+        const shippingLineIdsPre = order.shippingLines.map(l => l.id);
+
         const updatedOrder = await this.orderCalculator.applyPriceAdjustments(
         const updatedOrder = await this.orderCalculator.applyPriceAdjustments(
             ctx,
             ctx,
             order,
             order,
             promotions,
             promotions,
             updatedOrderLines ?? [],
             updatedOrderLines ?? [],
         );
         );
+
+        const shippingLineIdsPost = updatedOrder.shippingLines.map(l => l.id);
+        await this.applyChangesToShippingLines(ctx, updatedOrder, shippingLineIdsPre, shippingLineIdsPost);
+
+        // Explicitly omit the shippingAddress and billingAddress properties to avoid
+        // a race condition where changing one or the other in parallel can
+        // overwrite the other's changes. The other omissions prevent the save
+        // function from doing more work than necessary.
         await this.connection
         await this.connection
             .getRepository(ctx, Order)
             .getRepository(ctx, Order)
-            // Explicitly omit the shippingAddress and billingAddress properties to avoid
-            // a race condition where changing one or the other in parallel can
-            // overwrite the other's changes.
-            .save(omit(updatedOrder, ['shippingAddress', 'billingAddress']), { reload: false });
+            .save(
+                omit(updatedOrder, [
+                    'shippingAddress',
+                    'billingAddress',
+                    'lines',
+                    'shippingLines',
+                    'aggregateOrder',
+                    'sellerOrders',
+                    'customer',
+                    'modifications',
+                ]),
+                {
+                    reload: false,
+                },
+            );
         await this.connection.getRepository(ctx, OrderLine).save(updatedOrder.lines, { reload: false });
         await this.connection.getRepository(ctx, OrderLine).save(updatedOrder.lines, { reload: false });
         await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
         await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
         await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre);
         await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre);
 
 
         return assertFound(this.findOne(ctx, order.id));
         return assertFound(this.findOne(ctx, order.id));
     }
     }
+
+    /**
+     * Applies changes to the shipping lines of an order, adding or removing the relations
+     * in the database.
+     */
+    private async applyChangesToShippingLines(
+        ctx: RequestContext,
+        order: Order,
+        shippingLineIdsPre: ID[],
+        shippingLineIdsPost: ID[],
+    ) {
+        const removedShippingLineIds = shippingLineIdsPre.filter(id => !shippingLineIdsPost.includes(id));
+        const newlyAddedShippingLineIds = shippingLineIdsPost.filter(id => !shippingLineIdsPre.includes(id));
+
+        for (const idToRemove of removedShippingLineIds) {
+            await this.connection
+                .getRepository(ctx, Order)
+                .createQueryBuilder()
+                .relation('shippingLines')
+                .of(order)
+                .remove(idToRemove);
+        }
+
+        for (const idToAdd of newlyAddedShippingLineIds) {
+            await this.connection
+                .getRepository(ctx, Order)
+                .createQueryBuilder()
+                .relation('shippingLines')
+                .of(order)
+                .add(idToAdd);
+        }
+    }
 }
 }