Skip to content

Commit

Permalink
perf(core): Optimizations to the addItemToOrder path
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelbromley committed Sep 5, 2024
1 parent c591432 commit 70ad853
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export class ProductVariant
@ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
featuredAsset: Asset;

@EntityId({ nullable: true })
featuredAssetId: ID;

@OneToMany(type => ProductVariantAsset, productVariantAsset => productVariantAsset.productVariant, {
onDelete: 'SET NULL',
})
Expand All @@ -116,6 +119,9 @@ export class ProductVariant
@ManyToOne(type => TaxCategory, taxCategory => taxCategory.productVariants)
taxCategory: TaxCategory;

@EntityId({ nullable: true })
taxCategoryId: ID;

@OneToMany(type => ProductVariantPrice, price => price.variant, { eager: true })
productVariantPrices: ProductVariantPrice[];

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/entity/product/product.entity.ts
Original file line number Diff line number Diff line change
@@ -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 { ChannelAware, SoftDeletable } from '../../common/types/common-types';
Expand All @@ -8,6 +8,7 @@ import { Asset } from '../asset/asset.entity';
import { VendureEntity } from '../base/base.entity';
import { Channel } from '../channel/channel.entity';
import { CustomProductFields } from '../custom-entity-fields';
import { EntityId } from '../entity-id.decorator';
import { FacetValue } from '../facet-value/facet-value.entity';
import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
import { ProductVariant } from '../product-variant/product-variant.entity';
Expand Down Expand Up @@ -47,6 +48,9 @@ export class Product
@ManyToOne(type => Asset, asset => asset.featuredInProducts, { onDelete: 'SET NULL' })
featuredAsset: Asset;

@EntityId({ nullable: true })
featuredAssetId: ID;

@OneToMany(type => ProductAsset, productAsset => productAsset.product)
assets: ProductAsset[];

Expand Down
54 changes: 29 additions & 25 deletions packages/core/src/service/helpers/order-modifier/order-modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';
import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
import { IsNull } from 'typeorm';

import { RequestContext } from '../../../api/common/request-context';
import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
Expand Down Expand Up @@ -164,12 +165,13 @@ export class OrderModifier {
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(
new OrderLine({
productVariant,
taxCategory: productVariant.taxCategory,
featuredAsset: productVariant.featuredAsset ?? productVariant.product.featuredAsset,
featuredAsset: featuredAssetId ? { id: featuredAssetId } : undefined,
listPrice: productVariant.listPrice,
listPriceIncludesTax: productVariant.listPriceIncludesTax,
adjustments: [],
Expand All @@ -189,26 +191,15 @@ export class OrderModifier {
.set(orderLine.sellerChannel);
}
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;
}

/**
Expand Down Expand Up @@ -896,11 +887,24 @@ export class OrderModifier {
private async getProductVariantOrThrow(
ctx: RequestContext,
productVariantId: ID,
order: Order,
): 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);
}
return productVariant;
}
}
66 changes: 61 additions & 5 deletions packages/core/src/service/services/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ import {
CancelPaymentError,
EmptyOrderLineSelectionError,
FulfillmentStateTransitionError,
RefundStateTransitionError,
InsufficientStockOnHandError,
ItemsAlreadyFulfilledError,
ManualPaymentStateError,
MultipleOrderError,
NothingToRefundError,
PaymentOrderMismatchError,
RefundOrderStateError,
RefundStateTransitionError,
SettlePaymentError,
} from '../../common/error/generated-graphql-admin-errors';
import {
Expand Down Expand Up @@ -561,6 +561,7 @@ export class OrderService {
enabled: true,
deletedAt: IsNull(),
},
loadEagerRelations: false,
});
if (variant.product.enabled === false) {
throw new EntityNotFoundError('ProductVariant', productVariantId);
Expand Down Expand Up @@ -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(
ctx,
order,
promotions,
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
.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, ShippingLine).save(order.shippingLines, { reload: false });
await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre);

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);
}
}
}

0 comments on commit 70ad853

Please sign in to comment.