Программирование

Bitrix getList: объединить UserProps с PROFILE_DATA без циклов

Как в Bitrix Sale одним запросом getList объединить UserPropsTable и UserPropsValueTable в объект с вложенным массивом PROFILE_DATA через ReferenceField и JSON_ARRAYAGG. Избегайте дубликатов и циклов в ORM Bitrix D7.

1 ответ 1 просмотр

Как в Bitrix Sale объединить результаты getList из UserPropsTable и UserPropsValueTable в один объект с вложенным массивом PROFILE_DATA без циклов?

Пытаюсь выполнить одним запросом к БД, чтобы избежать дублирования данных. Текущий код:

php
\Bitrix\Sale\Internals\UserPropsTable::getList([
 "select" => ["*", "PROFILE_" => "PROFILE.*"],
 "filter" => ["PERSON_TYPE_ID" => 49163, "ID" => 12],
 "runtime" => [
 new \Bitrix\Main\Entity\ReferenceField(
 'PROFILE',
 \Bitrix\Sale\Internals\UserPropsValueTable::class,
 array('=this.ID' => 'ref.USER_PROPS_ID')
 ),
 ]
])->fetchAll();

Текущий результат (дублируется основной объект для каждого профиля):

json
[
 {
 "ID": "12",
 "NAME": "Проверка заголовка профиля",
 "USER_ID": "49163",
 "PERSON_TYPE_ID": "1",
 "DATE_UPDATE": {},
 "XML_ID": null,
 "VERSION_1C": null,
 "PROFILE_ID": "51",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "2",
 "PROFILE_NAME": "ФИО",
 "PROFILE_VALUE": "Проверка Работ Профиля"
 },
 {
 "ID": "12",
 "NAME": "Проверка заголовка профиля",
 "USER_ID": "49163",
 "PERSON_TYPE_ID": "1",
 "DATE_UPDATE": {},
 "XML_ID": null,
 "VERSION_1C": null,
 "PROFILE_ID": "52",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "3",
 "PROFILE_NAME": "E-mail",
 "PROFILE_VALUE": "test2@example.com"
 },
 {
 "ID": "12",
 "NAME": "Проверка заголовка профиля",
 "USER_ID": "49163",
 "PERSON_TYPE_ID": "1",
 "DATE_UPDATE": {},
 "XML_ID": null,
 "VERSION_1C": null,
 "PROFILE_ID": "53",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "4",
 "PROFILE_NAME": "Телефон",
 "PROFILE_VALUE": "+79370000000"
 }
]

Желаемый результат (один объект с массивом PROFILE_DATA):

json
[
 {
 "ID": "12",
 "NAME": "Проверка заголовка профиля",
 "USER_ID": "49163",
 "PERSON_TYPE_ID": "1",
 "DATE_UPDATE": {},
 "XML_ID": null,
 "VERSION_1C": null,
 "PROFILE_DATA": [
 {
 "PROFILE_ID": "51",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "2",
 "PROFILE_NAME": "ФИО",
 "PROFILE_VALUE": "Проверка Работ Профиля"
 },
 {
 "PROFILE_ID": "52",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "3",
 "PROFILE_NAME": "E-mail",
 "PROFILE_VALUE": "test2@example.com"
 },
 {
 "PROFILE_ID": "53",
 "PROFILE_USER_PROPS_ID": "12",
 "PROFILE_ORDER_PROPS_ID": "4",
 "PROFILE_NAME": "Телефон",
 "PROFILE_VALUE": "+79370000000"
 }
 ]
 }
]

Как настроить запрос с runtime, ReferenceField или другим способом в Bitrix для получения агрегированных данных одним вызовом getList?

Да — проблему дублирования при JOIN в D7/bitrix getList решают агрегирующие поля: используйте ReferenceField + ExpressionField с JSON_ARRAYAGG (если СУБД поддерживает) или GROUP_CONCAT в runtime, чтобы получить одно поле PROFILE_DATA в виде JSON и затем выполнить json_decode() — без циклов по строкам. Этот подход работает в ORM Bitrix и устраняет множественные строки для одного UserProps (bitrix getlist, orm bitrix, bitrix d7 getlist).


Содержание


Почему при bitrix getList дублируются строки и как ReferenceField работает

Вы подключили UserPropsValueTable через ReferenceField — это обычный SQL JOIN. При наличии N записей в UserPropsValueTable для одного UserPropsTable вы получите N строк результата: каждая строка — комбинация полей родителя + одной строки ребёнка. Корень проблемы — не сам ReferenceField, а отсутствие агрегации: ORM просто отражает результат JOIN.

Чтобы вернуть один объект с вложенным массивом PROFILE_DATA, нужно агрегировать строки ребёнка в одно поле на стороне СУБД, а не собирать массив в PHP через циклы. Примеры использования ReferenceField и ExpressionField для агрегации показаны в статье о D7 ORM и ReferenceField Горошко — Сила связей в D7 ORM. Официальная справка по getList и ExpressionField — в документации Bitrix getList.


Решение: JSON_ARRAYAGG + ReferenceField в bitrix getList

Идея — в ExpressionField использовать агрегирующую JSON-функцию СУБД: JSON_ARRAYAGG(JSON_OBJECT(…)). В результате поле PROFILE_DATA будет содержать JSON-массив объектов, а в PHP вы просто делаете json_decode — без объединяющих циклов.

Пример (рекомендуется, если MySQL/MariaDB поддерживает JSON_* функции):

php
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Sale\Internals\UserPropsTable;
use Bitrix\Sale\Internals\UserPropsValueTable;

$res = UserPropsTable::getList([
 'select' => [
 'ID',
 'NAME',
 'USER_ID',
 'PERSON_TYPE_ID',
 'DATE_UPDATE',
 // агрегируем все значения профиля в JSON-массив
 'PROFILE_DATA' => new ExpressionField(
 'PROFILE_DATA',
 "JSON_ARRAYAGG(JSON_OBJECT(
 'PROFILE_ID', %s,
 'PROFILE_USER_PROPS_ID', %s,
 'PROFILE_ORDER_PROPS_ID', %s,
 'PROFILE_NAME', %s,
 'PROFILE_VALUE', %s
 ))",
 [
 'PROFILE.ID',
 'PROFILE.USER_PROPS_ID',
 'PROFILE.ORDER_PROPS_ID',
 'PROFILE.NAME',
 'PROFILE.VALUE'
 ]
 )
 ],
 'runtime' => [
 new ReferenceField(
 'PROFILE',
 UserPropsValueTable::class,
 ['=this.ID' => 'ref.USER_PROPS_ID']
 )
 ],
 'filter' => ['PERSON_TYPE_ID' => 49163, 'ID' => 12],
 // обязательно группировать по полям родителя, которые не агрегируются
 'group' => ['ID', 'NAME', 'USER_ID', 'PERSON_TYPE_ID', 'DATE_UPDATE']
]);

$row = $res->fetch();

if ($row) {
 $row['PROFILE_DATA'] = $row['PROFILE_DATA'] !== null
 ? json_decode($row['PROFILE_DATA'], true)
 : [];
}

Пояснения и советы:

  • JSON_ARRAYAGG / JSON_OBJECT — просты и безопасны, база сама корректно экранирует строки.
  • В SELECT перечислены явные поля родителя: лучше не использовать ‘*’ при агрегировании; указывайте только нужные поля и добавляйте их в ‘group’.
  • Если ExpressionField или ReferenceField находятся в другом неймспейсе в вашей версии Bitrix, используйте \Bitrix\Main\Entity\ExpressionField / \Bitrix\Main\Entity\ReferenceField — в документации это показано на примерах getList и в статье Горошко.

Альтернатива: GROUP_CONCAT как фоллбек (orm bitrix)

Если в вашей СУБД нет JSON_* функций (старые MySQL/MariaDB), можно собрать JSON вручную через GROUP_CONCAT и потом json_decode в PHP. Минусы — нужно аккуратно экранировать кавычки и следить за ограничением GROUP_CONCAT_MAX_LEN.

Пример (упрощённый):

php
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\Application;

// увеличить лимит GROUP_CONCAT
Application::getConnection()->queryExecute("SET SESSION group_concat_max_len = 1000000");

$res = UserPropsTable::getList([
 'select' => [
 'ID','NAME','USER_ID','PERSON_TYPE_ID','DATE_UPDATE',
 'PROFILE_DATA' => new ExpressionField(
 'PROFILE_DATA',
 "CONCAT('[', GROUP_CONCAT(CONCAT(
 '{\"PROFILE_ID\":\"', COALESCE(%s,''), '\",',
 '\"PROFILE_USER_PROPS_ID\":\"', COALESCE(%s,''), '\",',
 '\"PROFILE_ORDER_PROPS_ID\":\"', COALESCE(%s,''), '\",',
 '\"PROFILE_NAME\":\"', REPLACE(REPLACE(COALESCE(%s,''),'\"','\\\\\"'),'\\\\','\\\\\\\\'), '\",',
 '\"PROFILE_VALUE\":\"', REPLACE(REPLACE(COALESCE(%s,''),'\"','\\\\\"'),'\\\\','\\\\\\\\'), '\"}'
 ) SEPARATOR ','), ']')",
 [
 'PROFILE.ID',
 'PROFILE.USER_PROPS_ID',
 'PROFILE.ORDER_PROPS_ID',
 'PROFILE.NAME',
 'PROFILE.VALUE'
 ]
 )
 ],
 'runtime' => [
 new ReferenceField('PROFILE', UserPropsValueTable::class, ['=this.ID' => 'ref.USER_PROPS_ID'])
 ],
 'filter' => ['PERSON_TYPE_ID' => 49163, 'ID' => 12],
 'group' => ['ID','NAME','USER_ID','PERSON_TYPE_ID','DATE_UPDATE']
]);

$row = $res->fetch();
$row['PROFILE_DATA'] = $row['PROFILE_DATA'] ? json_decode($row['PROFILE_DATA'], true) : [];

Важные замечания:

  • GROUP_CONCAT может обрезать результат — проверяйте и увеличивайте group_concat_max_len при необходимости.
  • Экранирование через REPLACE — хрупкое и сложное; лучше применять этот метод только если JSON-функции недоступны.
  • Можно добавить ORDER BY внутри GROUP_CONCAT, если важен порядок элементов.

Полный рабочий пример и разбор кода

Резюме шагов для безопасного и детерминированного результата:

  1. В select явно перечислите поля родителя (не ‘*’).
  2. Добавьте ReferenceField в runtime для связи с UserPropsValueTable.
  3. Добавьте ExpressionField, который агрегирует значения ребёнка (JSON_ARRAYAGG предпочтительнее).
  4. Укажите ‘group’ по полям родителя.
  5. После fetch() выполните json_decode($row[‘PROFILE_DATA’], true). Это — единственное лёгкое преобразование на PHP; циклов для собирания массива не нужно.

Пример минимального теста:

php
// выполнить запрос (JSON_ARRAYAGG пример)
$res = UserPropsTable::getList([...]); // см. раздел выше
$row = $res->fetch();
// $row теперь содержит PROFILE_DATA как массив (после json_decode)
// можно отдать в API или сразу json_encode($row) для ответа в формате JSON
echo json_encode([$row], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);

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


Совместимость и производительность (bitrix d7 getlist filter)

  • Проверка СУБД: JSON_OBJECT/JSON_ARRAYAGG доступны не во всех версиях MySQL/MariaDB — проверьте версию и попробуйте вручную SQL‑запрос в консоли. Если нет — используйте GROUP_CONCAT или двухзапросный вариант.
  • Ограничения GROUP_CONCAT: по умолчанию длина ограничена; решается SET SESSION group_concat_max_len.
  • Группировка: указывайте ‘group’ в getList, иначе SQL с агрегатами вернёт ошибку или недетерминированные значения.
  • Нагрузки: агрегирование большого количества строк в одну JSON-строку увеличивает нагрузку на БД и память; при больших объёмах может быть быстрее два запроса (сначала parent, затем children WHERE USER_PROPS_ID IN (…)) и маппинг в PHP. Иногда две простых быстрых query + один PHP-группинг (array_group_by) лучше одного тяжёлого JOIN+AGG.
  • Безопасность: JSON_ARRAYAGG безопаснее с точки зрения экранирования, чем ручная конкатенация.

Для примеров работы с фильтрами и getList смотрите официальную документацию getList и раздел по фильтрации Фильтрация.


Источники


Заключение

Чтобы получить один объект UserProps с вложенным массивом PROFILE_DATA одним вызовом getList, используйте ReferenceField в runtime и агрегирование через ExpressionField: лучше всего JSON_ARRAYAGG/JSON_OBJECT (если СУБД поддерживает), в противном случае GROUP_CONCAT + json_decode как фоллбек. Такой подход устраняет дублирование строк при JOIN и даёт чистый результат без циклов по строкам на PHP. Если СУБД или объёмы данных не позволяют — рассматривайте двухзапросный паттерн (parent + children IN(…)) как более надёжный вариант.

Авторы
Проверено модерацией
Модерация
Bitrix getList: объединить UserProps с PROFILE_DATA без циклов