Переглянути джерело

feat(server): Optimize product queries with variant resolver

The huge SQL join across so many entities was making the request for 20 Products take ~900ms - 1000ms. Splitting the variants into a separate property resolver brings it down to around 200 - 500ms
Michael Bromley 7 роки тому
батько
коміт
1d4e870525

+ 12 - 1
server/src/api/resolvers/product.resolver.ts

@@ -1,4 +1,4 @@
-import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Args, Mutation, Parent, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
 import {
     AddOptionGroupToProductMutationArgs,
     ApplyFacetValuesToProductVariantsMutationArgs,
@@ -22,6 +22,7 @@ import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { ProductVariantService } from '../../service/services/product-variant.service';
 import { ProductService } from '../../service/services/product.service';
+import { IdCodecService } from '../common/id-codec.service';
 import { RequestContext } from '../common/request-context';
 import { Allow } from '../decorators/allow.decorator';
 import { Decode } from '../decorators/decode.decorator';
@@ -33,6 +34,7 @@ export class ProductResolver {
         private productService: ProductService,
         private productVariantService: ProductVariantService,
         private facetValueService: FacetValueService,
+        private idCodecService: IdCodecService,
     ) {}
 
     @Query()
@@ -53,6 +55,15 @@ export class ProductResolver {
         return this.productService.findOne(ctx, args.id);
     }
 
+    @ResolveProperty()
+    async variants(
+        @Ctx() ctx: RequestContext,
+        @Parent() product: Product,
+    ): Promise<Array<Translated<ProductVariant>>> {
+        const productId = this.idCodecService.decode(product.id);
+        return this.productVariantService.getVariantsByProductId(ctx, productId);
+    }
+
     @Mutation()
     @Allow(Permission.CreateCatalog)
     @Decode('assetIds', 'featuredAssetId')

+ 17 - 0
server/src/service/services/product-variant.service.ts

@@ -44,6 +44,23 @@ export class ProductVariantService {
             });
     }
 
+    getVariantsByProductId(ctx: RequestContext, productId: ID): Promise<Array<Translated<ProductVariant>>> {
+        return this.connection
+            .getRepository(ProductVariant)
+            .find({
+                where: {
+                    product: { id: productId } as any,
+                },
+                relations: ['options', 'facetValues', 'taxCategory'],
+            })
+            .then(variants =>
+                variants.map(variant => {
+                    const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);
+                    return translateDeep(variantWithPrices, ctx.languageCode, ['options', 'facetValues']);
+                }),
+            );
+    }
+
     async create(
         ctx: RequestContext,
         product: Product,

+ 6 - 49
server/src/service/services/product.service.ts

@@ -37,31 +37,15 @@ export class ProductService {
         ctx: RequestContext,
         options?: ListQueryOptions<Product>,
     ): Promise<PaginatedList<Translated<Product>>> {
-        const relations = [
-            'featuredAsset',
-            'assets',
-            'variants',
-            'optionGroups',
-            'variants.options',
-            'variants.facetValues',
-            'variants.taxCategory',
-            'channels',
-        ];
+        const relations = ['featuredAsset', 'assets', 'optionGroups', 'channels'];
 
         return this.listQueryBuilder
             .build(Product, options, relations, ctx.channelId)
             .getManyAndCount()
             .then(async ([products, totalItems]) => {
-                const items = products
-                    .map(product =>
-                        translateDeep(product, ctx.languageCode, [
-                            'optionGroups',
-                            'variants',
-                            ['variants', 'options'],
-                            ['variants', 'facetValues'],
-                        ]),
-                    )
-                    .map(product => this.applyPriceAndTaxToVariants(product, ctx));
+                const items = products.map(product =>
+                    translateDeep(product, ctx.languageCode, ['optionGroups']),
+                );
                 return {
                     items,
                     totalItems,
@@ -70,26 +54,12 @@ export class ProductService {
     }
 
     async findOne(ctx: RequestContext, productId: ID): Promise<Translated<Product> | undefined> {
-        const relations = [
-            'featuredAsset',
-            'assets',
-            'variants',
-            'optionGroups',
-            'variants.options',
-            'variants.facetValues',
-            'variants.taxCategory',
-        ];
+        const relations = ['featuredAsset', 'assets', 'optionGroups'];
         const product = await this.connection.manager.findOne(Product, productId, { relations });
         if (!product) {
             return;
         }
-        const translated = translateDeep(product, ctx.languageCode, [
-            'optionGroups',
-            'variants',
-            ['variants', 'options'],
-            ['variants', 'facetValues'],
-        ]);
-        return this.applyPriceAndTaxToVariants(translated, ctx);
+        return translateDeep(product, ctx.languageCode, ['optionGroups']);
     }
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
@@ -167,19 +137,6 @@ export class ProductService {
         }
     }
 
-    /**
-     * The price of a ProductVariant depends on the current channel and the priceWithTax further
-     * depends on the currently-active zone and applicable TaxRates.
-     * This method uses the RequestContext to determine these values and apply them to each
-     * ProductVariant of the given Product.
-     */
-    private applyPriceAndTaxToVariants<T extends Product>(product: T, ctx: RequestContext): T {
-        product.variants = product.variants.map(variant => {
-            return this.productVariantService.applyChannelPriceAndTax(variant, ctx);
-        });
-        return product;
-    }
-
     private async getProductWithOptionGroups(productId: ID): Promise<Product> {
         const product = await this.connection
             .getRepository(Product)