Получить заказы с корзинами, свойствами, доставками и оплатами через getList-ы

Просмотров: 140

Задача: получить заказы со всеми дополнительными данными: корзинами, свойствами, доставками и оплатами- через getLis-ы, для контроля получаемых данных.

На самом деле хотел написать заметку про составление оптимизированных запросов при получении больших массивов информации из БД.

Но решил не делать пример в вакууме, поэтому взял наиболее частую задачу- работу с заказами.
В любом случае, в примере ниже вы получите то, за чем пришли :)- всю информацию о заказах.

Почему именно через getList-ы? Потому что это низкоуровневый инструмент с полным контролем над запросом: можно явно указать нужные поля через select, задать условия через filter, отсортировать через order. Именно эта гибкость позволяет писать эффективные запросы и избежать распространённой ловушки.

У заметки есть аналог:
Заказы через ORM

Проблема N+1 в запросах

Есть соблазн получать данные порциями: в цикле по заказам дополнительно подгружать свойства, доставки, оплаты и т.д., дозаполняя массив с заказами прямо на месте.

При таком подходе можно не столкнуться с проблемой в процессе разработки на небольшом количестве заказов, но на продакшене, когда потребуется обработать сотни или тысячи заказов, это обернётся серьёзной потерей производительности.

Посмотрим на конкретный пример. Допустим, в базе 100 заказов. Код ниже выполнит:

  • 1 запрос = список заказов (Order::getList)
  • 100 запросов = название статуса для каждого заказа (CSaleStatus::GetByID)
  • 100 запросов = товары корзины для каждого заказа (Basket::getList)
  • 100 запросов = свойства для каждого заказа (OrderPropsValueTable::getList)
  • 100 запросов = доставки для каждого заказа (ShipmentTable::getList)
  • 100 запросов = оплаты для каждого заказа (PaymentTable::getList)

Итого: 1 + 100 × 5 = 501 запрос вместо 6. Это и есть проблема N+1: вместо одного запроса на каждый тип данных выполняется по N запросов - по одному на каждую запись из первого запроса.

use Bitrix\Main\Loader,
    Bitrix\Sale\Order,
    Bitrix\Sale\Internals\OrderPropsValueTable,
    Bitrix\Sale\Internals\ShipmentTable,
    Bitrix\Sale\Internals\PaymentTable,
    Bitrix\Sale\Basket;

Loader::includeModule('sale');

$dbRes = Order::getList([
    'select' => [
        "*"
    ],
    'order' => [
        'ID' => 'DESC'
    ]
]);

while ($order = $dbRes->fetch()) {

    $arResult['ORDERS'][$order['ID']] = $order;

    // Название статуса
    $arResult['ORDERS'][$order['ID']]['STATUS_NAME'] = CSaleStatus::GetByID($order['STATUS_ID']);

    // Получаем товары корзины заказа
    $arResult['ORDERS'][$order['ID']]['BASKET_ITEMS'] = Basket::getList(
        [
            'select' => ['*'],
            'filter' => [
                '=ORDER_ID' => $order['ID']
            ]
        ]
    )->FetchAll();

    // Получаем свойства заказа
    $arResult['ORDERS'][$order['ID']]['ORDER_PROPS'] = OrderPropsValueTable::getList(
        [
            'select' => ['*'],
            'filter' => [
                '=ORDER_ID' => $order['ID']
            ]
        ]
    )->fetchAll();

    // Получаем информацию о доставках
    $arResult['ORDERS'][$order['ID']]['DELIVERY'] = ShipmentTable::getList([
        'select' => ['*'],
        'filter' => [
            '=ORDER_ID' => $order['ID'],
            '=SYSTEM' => 'N'   // исключаем системную «виртуальную» отгрузку
        ]
    ])->fetchAll();

    // Получаем информацию об оплатах
    $arResult['ORDERS'][$order['ID']]['PAYMENTS'] = PaymentTable::getList([
        'select' => ['*'],
        'filter' => [
            '=ORDER_ID' => $order['ID']
        ]
    ])->fetchAll();
}

// Все о заказе
var_dump($arResult['ORDERS']);

Чем больше заказов- тем хуже ситуация. При 1000 заказах это уже 5001 запрос вместо 6. Каждый запрос- это отдельное обращение к базе данных: установка соединения, передача запроса, ожидание ответа. Всё это многократно увеличивает время ответа страницы и нагрузку на сервер БД.

Правильное решение- выполнить по одному запросу на каждый тип данных, собрав сразу все нужные ID заказов, а затем "склеить" результаты в PHP.

Оптимизированный пример

Идея проста: сначала одним запросом получаем все заказы и собираем массив их ID, а затем делаем по одному запросу на каждый тип связанных данных, передавая сразу весь массив ID в фильтр. Результаты разбрасываем по нужным заказам уже в PHP- без единого лишнего обращения к базе.

use Bitrix\Main\Loader,
    Bitrix\Sale\Order,
    Bitrix\Sale\Internals\OrderPropsValueTable,
    Bitrix\Sale\Internals\ShipmentTable,
    Bitrix\Sale\Internals\PaymentTable,
    Bitrix\Sale\Basket;

Loader::includeModule('sale');

$dbRes = \Bitrix\Sale\Order::getList([
    'select' => [
        "*"
    ],
    'order' => [
        'ID' => 'DESC'
    ]
]);

// Шаг 1: собираем заказы и их ID
$orderIds = [];
while ($order = $dbRes->fetch()) {
    $arResult['ORDERS'][$order['ID']] = $order;
    $arResult['ORDERS'][$order['ID']]['STATUS_NAME'] = CSaleStatus::GetByID($order['STATUS_ID']);
    $arResult['ORDERS'][$order['ID']]['BASKET_ITEMS'] = [];
    $arResult['ORDERS'][$order['ID']]['ORDER_PROPS'] = [];
    $arResult['ORDERS'][$order['ID']]['DELIVERY'] = [];
    $orderIds[] = $order['ID'];
}

if (!empty($orderIds)) {

    // Шаг 2: корзины - 1 запрос
    $dbBasket = Basket::getList([
        'select' => ['*'],
        'filter' => ['=ORDER_ID' => $orderIds]
    ]);
    while ($item = $dbBasket->fetch()) {
        $arResult['ORDERS'][$item['ORDER_ID']]['BASKET_ITEMS'][] = $item;
    }

    // Шаг 3: свойства - 1 запрос
    $dbProps = \Bitrix\Sale\Internals\OrderPropsValueTable::getList([
        'select' => ['*'],
        'filter' => ['=ORDER_ID' => $orderIds]
    ]);
    while ($prop = $dbProps->fetch()) {
        $arResult['ORDERS'][$prop['ORDER_ID']]['ORDER_PROPS'][] = $prop;
    }

    // Шаг 4: доставки - 1 запрос
    $dbShipments = \Bitrix\Sale\Internals\ShipmentTable::getList([
        'select' => ['*'],
        'filter' => [
            '=ORDER_ID' => $orderIds,
            '=SYSTEM' => 'N'
        ]
    ]);
    while ($shipment = $dbShipments->fetch()) {
        $arResult['ORDERS'][$shipment['ORDER_ID']]['DELIVERY'][] = $shipment;
    }

    // Шаг 5: оплаты - 1 запрос
    $dbPayments = \Bitrix\Sale\Internals\PaymentTable::getList([
        'select' => ['*'],
        'filter' => ['=ORDER_ID' => $orderIds]
    ]);
    while ($payment = $dbPayments->fetch()) {
        $arResult['ORDERS'][$payment['ORDER_ID']]['PAYMENTS'][] = $payment;
    }
}

// Все о заказе
var_dump($arResult['ORDERS']);

Теперь независимо от количества заказов выполняется ровно 6 запросов: по одному на каждый тип данных. При 100 заказах — 6 запросов, при 1000 — всё те же 6.

  • 1 запрос = список заказов
  • 1 запрос = корзины всех заказов
  • 1 запрос = свойства всех заказов
  • 1 запрос = доставки всех заказов
  • 1 запрос = оплаты всех заказов
  • N запросов = названия статусов (их тоже можно вынести отдельно, получив все нужные статусы одним запросом через CSaleStatus::GetList)

Ключевое отличие от плохого варианта - фильтр принимает массив ID ('=ORDER_ID' => $orderIds), что транслируется в один SQL-запрос с конструкцией IN (...). Разбивка полученных данных по заказам происходит в PHP через простой while с присвоением по ключу.

Стоимость разработки на 1С-Битрикс:

Индивидуальная разработка магазина

от 350 000 руб. от 5-ти недель

Разработка магазина на 1С-Битрикс с нуля. Дизайн, сборка и оптимизация производительности под конкретный проект и требования. Реализация любого функционала без ограничений готовых решений.

Запуск сайта на готовом решении

от 150 000 руб. от 7-ми дней

Вариант для тех, кто не хочет тратить много средств на индивидуальный проект, и не имеет серьезных требований к сайту. Магазин, быстро запускается на базе одного из 200-та готовых решений.

Мобильное приложение

от 400 000 руб. от 5-ти недель

Разработка кроссплатформенного мобильного приложения, которое не уступает нативным решениям как в производительности, так и пользовательском опыте. Публикуется в AppStore, GooglePlay и RuStore

Сайт компании

от 300 000 руб. от 2-х недель

Корпоративный сайт с информационными разделами, каталогом товаров или услуг. Включает формы обратной связи карточек каталога, любое количество статичных и динамичных разделов.

Инфоресурс

от 300 000 руб. от 4-х недель

Информационный ресурс любой сложности. Сайт для СМИ, городской портал или многопользовательская доска объявлений. Внутренние форумы, блоги- по необходимости.

3D‑моделирование, визуализация

от 25 000 руб. от 3-х дней

По вашим фото, чертежам или описанию создадим 3D‑модели и отрендерим набор изображений для каталога товаров: общий вид, крупные планы и технические ракурсы или 360°‑обзор товара.