Explorar el Código

perf(core): Optimize Order-related field resolvers

Relates to #1727. The main issue turned out to be field resolvers
running for each OrderItem. The primary solution is to make use of
the RequestContextCache to turn n calls into 1 call.

In testing, the query used on the OrderDetail view reduced from
10.5s to 2.7s with 5000 OrderItems.
Michael Bromley hace 3 años
padre
commit
03d2b2c359

+ 11 - 2
packages/core/src/api/resolvers/entity/fulfillment-entity.resolver.ts

@@ -1,17 +1,26 @@
 import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
 
+import { RequestContextCacheService } from '../../../cache/index';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
+import { RequestContextService } from '../../../service/index';
 import { FulfillmentService } from '../../../service/services/fulfillment.service';
 import { RequestContext } from '../../common/request-context';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Fulfillment')
 export class FulfillmentEntityResolver {
-    constructor(private fulfillmentService: FulfillmentService) {}
+    constructor(
+        private fulfillmentService: FulfillmentService,
+        private requestContextCache: RequestContextCacheService,
+    ) {}
 
     @ResolveField()
     async orderItems(@Ctx() ctx: RequestContext, @Parent() fulfillment: Fulfillment) {
-        return this.fulfillmentService.getOrderItemsByFulfillmentId(ctx, fulfillment.id);
+        return this.requestContextCache.get(
+            ctx,
+            `FulfillmentEntityResolver.orderItems(${fulfillment.id})`,
+            () => this.fulfillmentService.getOrderItemsByFulfillmentId(ctx, fulfillment.id),
+        );
     }
 }
 

+ 14 - 2
packages/core/src/api/resolvers/entity/order-item-entity.resolver.ts

@@ -1,5 +1,6 @@
 import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
 
+import { RequestContextCacheService } from '../../../cache/index';
 import { Fulfillment, OrderItem } from '../../../entity';
 import { FulfillmentService } from '../../../service';
 import { RequestContext } from '../../common/request-context';
@@ -7,7 +8,10 @@ import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('OrderItem')
 export class OrderItemEntityResolver {
-    constructor(private fulfillmentService: FulfillmentService) {}
+    constructor(
+        private fulfillmentService: FulfillmentService,
+        private requestContextCache: RequestContextCacheService,
+    ) {}
 
     @ResolveField()
     async fulfillment(
@@ -17,6 +21,14 @@ export class OrderItemEntityResolver {
         if (orderItem.fulfillment) {
             return orderItem.fulfillment;
         }
-        return this.fulfillmentService.getFulfillmentByOrderItemId(ctx, orderItem.id);
+        const lineFulfillments = await this.requestContextCache.get(
+            ctx,
+            `OrderItemEntityResolver.fulfillment(${orderItem.lineId})`,
+            () => this.fulfillmentService.getFulfillmentsByOrderLineId(ctx, orderItem.lineId),
+        );
+        const otherResult = lineFulfillments.find(({ orderItemIds }) =>
+            orderItemIds.has(orderItem.id),
+        )?.fulfillment;
+        return otherResult;
     }
 }

+ 8 - 2
packages/core/src/api/resolvers/entity/payment-entity.resolver.ts

@@ -1,6 +1,7 @@
 import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
 import { pick } from '@vendure/common/lib/pick';
 
+import { RequestContextCacheService } from '../../../cache/index';
 import { PaymentMetadata } from '../../../common/types/common-types';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
@@ -13,14 +14,19 @@ import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Payment')
 export class PaymentEntityResolver {
-    constructor(private orderService: OrderService) {}
+    constructor(
+        private orderService: OrderService,
+        private requestContextCache: RequestContextCacheService,
+    ) {}
 
     @ResolveField()
     async refunds(@Ctx() ctx: RequestContext, @Parent() payment: Payment): Promise<Refund[]> {
         if (payment.refunds) {
             return payment.refunds;
         } else {
-            return this.orderService.getPaymentRefunds(ctx, payment.id);
+            return this.requestContextCache.get(ctx, `PaymentEntityResolver.refunds(${payment.id})`, () =>
+                this.orderService.getPaymentRefunds(ctx, payment.id),
+            );
         }
     }
 

+ 34 - 8
packages/core/src/service/services/fulfillment.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { isObject } from '@vendure/common/lib/shared-utils';
+import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../api/common/request-context';
 import {
@@ -115,6 +116,31 @@ export class FulfillmentService {
         return fulfillment.orderItems;
     }
 
+    async getFulfillmentsByOrderLineId(
+        ctx: RequestContext,
+        orderLineId: ID,
+    ): Promise<Array<{ fulfillment: Fulfillment; orderItemIds: Set<ID> }>> {
+        const itemIdsQb = await this.connection
+            .getRepository(ctx, OrderItem)
+            .createQueryBuilder('item')
+            .select('item.id', 'id')
+            .where('item.lineId = :orderLineId', { orderLineId });
+
+        const fulfillments = await this.connection
+            .getRepository(ctx, Fulfillment)
+            .createQueryBuilder('fulfillment')
+            .leftJoinAndSelect('fulfillment.orderItems', 'item')
+            .where(`item.id IN (${itemIdsQb.getQuery()})`)
+            .andWhere('fulfillment.state != :cancelledState', { cancelledState: 'Cancelled' })
+            .setParameters(itemIdsQb.getParameters())
+            .getMany();
+
+        return fulfillments.map(fulfillment => ({
+            fulfillment,
+            orderItemIds: new Set(fulfillment.orderItems.map(i => i.id)),
+        }));
+    }
+
     /**
      * @description
      * Returns the Fulfillment for the given OrderItem (if one exists).
@@ -147,14 +173,14 @@ export class FulfillmentService {
           }
         | FulfillmentStateTransitionError
     > {
-        const fulfillment = await this.findOneOrThrow(ctx, fulfillmentId, [
-            'orderItems',
-            'orderItems.line',
-            'orderItems.line.order',
-        ]);
-        // Find orders based on order items filtering by id, removing duplicated orders
-        const ordersInOrderItems = fulfillment.orderItems.map(oi => oi.line.order);
-        const orders = ordersInOrderItems.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i);
+        const fulfillment = await this.findOneOrThrow(ctx, fulfillmentId, ['orderItems']);
+        const lineIds = unique(fulfillment.orderItems.map(item => item.lineId));
+        const orders = await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder('order')
+            .leftJoinAndSelect('order.lines', 'line')
+            .where('line.id IN (:...lineIds)', { lineIds })
+            .getMany();
         const fromState = fulfillment.state;
         try {
             await this.fulfillmentStateMachine.transition(ctx, fulfillment, orders, state);

+ 17 - 17
packages/core/src/service/services/order.service.ts

@@ -1243,23 +1243,23 @@ export class OrderService {
      * Returns an array of all Fulfillments associated with the Order.
      */
     async getOrderFulfillments(ctx: RequestContext, order: Order): Promise<Fulfillment[]> {
-        let lines: OrderLine[];
-        if (order.lines?.[0]?.items?.[0]?.fulfillments !== undefined) {
-            lines = order.lines;
-        } else {
-            lines = await this.connection.getRepository(ctx, OrderLine).find({
-                where: {
-                    order: order.id,
-                },
-                relations: ['items', 'items.fulfillments'],
-            });
-        }
-        const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
-        const fulfillments = items.reduce(
-            (acc, i) => [...acc, ...(i.fulfillments || [])],
-            [] as Fulfillment[],
-        );
-        return unique(fulfillments, 'id');
+        const itemIdsQb = await this.connection
+            .getRepository(ctx, OrderItem)
+            .createQueryBuilder('item')
+            .select('item.id', 'id')
+            .leftJoin('item.line', 'line')
+            .leftJoin('line.order', 'order')
+            .where('order.id = :orderId', { orderId: order.id });
+
+        const fulfillments = await this.connection
+            .getRepository(ctx, Fulfillment)
+            .createQueryBuilder('fulfillment')
+            .leftJoinAndSelect('fulfillment.orderItems', 'item')
+            .where(`item.id IN (${itemIdsQb.getQuery()})`)
+            .setParameters(itemIdsQb.getParameters())
+            .getMany();
+
+        return fulfillments;
     }
 
     /**

+ 7 - 10
packages/core/src/service/services/stock-movement.service.ts

@@ -154,19 +154,16 @@ export class StockMovementService {
     async createSalesForOrder(ctx: RequestContext, orderItems: OrderItem[]): Promise<Sale[]> {
         const sales: Sale[] = [];
         const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
-        const orderItemsWithVariants = await this.connection.getRepository(ctx, OrderItem).findByIds(
-            orderItems.map(i => i.id),
-            {
-                relations: ['line', 'line.productVariant'],
-            },
-        );
         const orderLinesMap = new Map<ID, { line: OrderLine; items: OrderItem[] }>();
 
-        for (const orderItem of orderItemsWithVariants) {
-            let value = orderLinesMap.get(orderItem.line.id);
+        for (const orderItem of orderItems) {
+            let value = orderLinesMap.get(orderItem.lineId);
             if (!value) {
-                value = { line: orderItem.line, items: [] };
-                orderLinesMap.set(orderItem.line.id, value);
+                const line = await this.connection.getEntityOrThrow(ctx, OrderLine, orderItem.lineId, {
+                    relations: ['productVariant'],
+                });
+                value = { line, items: [] };
+                orderLinesMap.set(orderItem.lineId, value);
             }
             value.items.push(orderItem);
         }