Merge getBySku into single SQL query with FOR JSON sub-queries
Replaces 6 round-trips (1 sequential resolve + 5 parallel detail) with one query. Auth resolution, article detail, categories, photos, eans, and prices are all fetched in a single round-trip.
This commit is contained in:
@@ -159,3 +159,152 @@ SELECT id
|
||||
FROM brand
|
||||
WHERE id = @brandId AND company_id = @companyId
|
||||
`;
|
||||
|
||||
/**
|
||||
* Single-query replacement for resolveArticleBySku + queryBySku.
|
||||
* Resolves SKU → article + company + auth, then loads full article detail
|
||||
* including categories, photos, eans, and prices via FOR JSON sub-queries.
|
||||
* Returns one row with articleJson, or empty if not found/unauthorized.
|
||||
*/
|
||||
export const GET_ARTICLE_BY_SKU_FULL = `
|
||||
;WITH article_cat AS (
|
||||
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId
|
||||
FROM article a
|
||||
INNER JOIN categoryarticle ca ON ca.article_id = a.id
|
||||
WHERE a.sku = @sku
|
||||
),
|
||||
ancestors AS (
|
||||
SELECT c.id, c.parent_id
|
||||
FROM category c
|
||||
INNER JOIN article_cat ac ON c.id = ac.categoryId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id, c.parent_id
|
||||
FROM category c
|
||||
INNER JOIN ancestors a ON c.id = a.parent_id
|
||||
),
|
||||
root AS (
|
||||
SELECT TOP 1 id AS rootCategoryId
|
||||
FROM ancestors
|
||||
WHERE parent_id IS NULL
|
||||
),
|
||||
auth AS (
|
||||
SELECT
|
||||
ac.articleId,
|
||||
ac.categoryId,
|
||||
r.rootCategoryId,
|
||||
co.id AS companyId,
|
||||
co.default_language_id AS languageId,
|
||||
co.poscustomergroup_id AS posCustomerGroupId,
|
||||
cu.id AS companyuserId
|
||||
FROM article_cat ac
|
||||
LEFT JOIN root r ON 1 = 1
|
||||
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId
|
||||
LEFT JOIN companyuser cu
|
||||
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId
|
||||
)
|
||||
SELECT
|
||||
auth.articleId,
|
||||
auth.categoryId,
|
||||
auth.rootCategoryId,
|
||||
auth.companyId,
|
||||
auth.languageId,
|
||||
auth.posCustomerGroupId,
|
||||
auth.companyuserId,
|
||||
(
|
||||
SELECT
|
||||
a.id,
|
||||
a.sku,
|
||||
at.name,
|
||||
at.slug,
|
||||
a.unitamount,
|
||||
a.weight,
|
||||
u.si AS unitSi,
|
||||
u.basesi AS unitBasesi,
|
||||
u.basesifactor AS unitBasesifactor,
|
||||
ut.symbol AS unitSymbol,
|
||||
ut.name AS unitName,
|
||||
b.name AS brandName,
|
||||
bp.thumbw AS brandThumbW,
|
||||
bp.thumbh AS brandThumbH,
|
||||
bp.thumbhash AS brandThumbHash,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM optionslistarticle ola
|
||||
WHERE ola.optionslist_id = a.optionslist_id
|
||||
) AS optionsCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM articlelistarticle ala
|
||||
WHERE ala.articlelist_id = a.articlelist_id
|
||||
) AS articleListCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM article child
|
||||
WHERE child.fatherarticle_id = a.id
|
||||
) AS childCount,
|
||||
a.fatherarticle_id AS fatherArticleId,
|
||||
fa.sku AS fatherSku,
|
||||
(
|
||||
SELECT ca2.category_id
|
||||
FROM categoryarticle ca2
|
||||
WHERE ca2.article_id = a.id
|
||||
ORDER BY ca2.category_id
|
||||
FOR JSON PATH
|
||||
) AS categoryIdsJson,
|
||||
(
|
||||
SELECT
|
||||
ap.sort,
|
||||
p.hash AS fullHash,
|
||||
p.w AS fullW,
|
||||
p.h AS fullH,
|
||||
p.size AS fullSize,
|
||||
p.thumbw AS thumbW,
|
||||
p.thumbh AS thumbH,
|
||||
p.thumbhash AS thumbHash
|
||||
FROM articlepic ap
|
||||
INNER JOIN pic p ON p.id = ap.pic_id
|
||||
WHERE ap.article_id = a.id
|
||||
ORDER BY ap.sort ASC, ap.id ASC
|
||||
FOR JSON PATH
|
||||
) AS photosJson,
|
||||
(
|
||||
SELECT ae.ean
|
||||
FROM articleean ae
|
||||
WHERE ae.article_id = a.id
|
||||
ORDER BY ae.ean
|
||||
FOR JSON PATH
|
||||
) AS eansJson,
|
||||
(
|
||||
SELECT
|
||||
pr.minmultiplier,
|
||||
pr.net,
|
||||
t.rate AS taxRate,
|
||||
pr.customergroup_id AS customergroupId
|
||||
FROM price pr
|
||||
INNER JOIN tax t ON t.id = pr.tax_id
|
||||
WHERE pr.article_id = a.id
|
||||
ORDER BY pr.customergroup_id, pr.minmultiplier
|
||||
FOR JSON PATH
|
||||
) AS pricesJson
|
||||
FROM article a
|
||||
LEFT JOIN articletext at
|
||||
ON at.article_id = a.id AND at.language_id = auth.languageId
|
||||
LEFT JOIN unit u ON u.id = a.unit_id
|
||||
LEFT JOIN unittext ut
|
||||
ON ut.unit_id = u.id AND ut.language_id = auth.languageId
|
||||
LEFT JOIN brand b ON b.id = a.brand_id
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 bp2.pic_id
|
||||
FROM brandpic bp2
|
||||
WHERE bp2.brand_id = b.id
|
||||
ORDER BY bp2.sort ASC, bp2.id ASC
|
||||
) first_brand_pic
|
||||
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
|
||||
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
|
||||
WHERE a.id = auth.articleId
|
||||
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
|
||||
) AS articleJson
|
||||
FROM auth
|
||||
`;
|
||||
|
||||
@@ -1,22 +1,70 @@
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import { GET_ARTICLE_BY_SKU_FULL } from '../../sql/article.js';
|
||||
|
||||
function parseJsonArray(raw) {
|
||||
if (!raw) return [];
|
||||
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
}
|
||||
|
||||
export default function getBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
return async (googleAuthId, sku) => {
|
||||
let t0 = performance.now();
|
||||
const { articleId, languageId } = await resolve(googleAuthId, sku);
|
||||
console.log(`[getBySku] resolveArticleBySku ${(performance.now() - t0).toFixed(1)}ms`);
|
||||
const t0 = performance.now();
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('sku', sku)
|
||||
.input('googleAuthId', googleAuthId)
|
||||
.query(GET_ARTICLE_BY_SKU_FULL);
|
||||
console.log(`[getBySku] single query ${(performance.now() - t0).toFixed(1)}ms`);
|
||||
|
||||
t0 = performance.now();
|
||||
const article = await load(articleId, languageId);
|
||||
console.log(`[getBySku] queryBySku (parallel) ${(performance.now() - t0).toFixed(1)}ms`);
|
||||
if (!article) {
|
||||
const row = result.recordset[0];
|
||||
if (!row) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
if (!row.articleId || !row.categoryId) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
if (!row.companyId || !row.rootCategoryId || !row.languageId) {
|
||||
throw new Error('Parent not found');
|
||||
}
|
||||
if (!row.companyuserId) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
return article;
|
||||
const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson;
|
||||
|
||||
return {
|
||||
article: {
|
||||
id: a.id,
|
||||
sku: a.sku,
|
||||
name: a.name,
|
||||
slug: a.slug,
|
||||
unitamount: a.unitamount,
|
||||
weight: a.weight,
|
||||
unit: {
|
||||
si: a.unitSi,
|
||||
basesi: a.unitBasesi,
|
||||
basesifactor: a.unitBasesifactor,
|
||||
symbol: a.unitSymbol ?? null,
|
||||
name: a.unitName ?? null,
|
||||
},
|
||||
brand: a.brandName
|
||||
? {
|
||||
name: a.brandName,
|
||||
thumbW: a.brandThumbW,
|
||||
thumbH: a.brandThumbH,
|
||||
thumbHash: a.brandThumbHash,
|
||||
}
|
||||
: null,
|
||||
categoryIds: parseJsonArray(a.categoryIdsJson).map((r) => r.category_id),
|
||||
photos: parseJsonArray(a.photosJson),
|
||||
eans: parseJsonArray(a.eansJson).map((r) => r.ean),
|
||||
prices: parseJsonArray(a.pricesJson),
|
||||
optionsCount: a.optionsCount,
|
||||
articleListCount: a.articleListCount,
|
||||
childCount: a.childCount,
|
||||
fatherArticleId: a.fatherArticleId,
|
||||
fatherSku: a.fatherSku,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user