Bitrix getList: объединить UserProps с PROFILE_DATA без циклов
Как в Bitrix Sale одним запросом getList объединить UserPropsTable и UserPropsValueTable в объект с вложенным массивом PROFILE_DATA через ReferenceField и JSON_ARRAYAGG. Избегайте дубликатов и циклов в ORM Bitrix D7.
Как в Bitrix Sale объединить результаты getList из UserPropsTable и UserPropsValueTable в один объект с вложенным массивом PROFILE_DATA без циклов?
Пытаюсь выполнить одним запросом к БД, чтобы избежать дублирования данных. Текущий код:
\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();
Текущий результат (дублируется основной объект для каждого профиля):
[
{
"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):
[
{
"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).
Содержание
- Почему появляются дубликаты при getList
- Решение: JSON_ARRAYAGG + ReferenceField в bitrix getList
- Альтернатива: GROUP_CONCAT как фоллбек (orm bitrix)
- Полный рабочий пример и разбор кода
- Совместимость и производительность
- Источники
- Заключение
Почему при 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_* функции):
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.
Пример (упрощённый):
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, если важен порядок элементов.
Полный рабочий пример и разбор кода
Резюме шагов для безопасного и детерминированного результата:
- В select явно перечислите поля родителя (не ‘*’).
- Добавьте ReferenceField в runtime для связи с UserPropsValueTable.
- Добавьте ExpressionField, который агрегирует значения ребёнка (JSON_ARRAYAGG предпочтительнее).
- Укажите ‘group’ по полям родителя.
- После fetch() выполните json_decode($row[‘PROFILE_DATA’], true). Это — единственное лёгкое преобразование на 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 и раздел по фильтрации Фильтрация.
Источники
- Сила связей в D7 ORM: JOIN’ы и ReferenceField на практике — Горошко Андрея
- getList — документация Bitrix
- Фильтрация — документация Bitrix
Заключение
Чтобы получить один объект UserProps с вложенным массивом PROFILE_DATA одним вызовом getList, используйте ReferenceField в runtime и агрегирование через ExpressionField: лучше всего JSON_ARRAYAGG/JSON_OBJECT (если СУБД поддерживает), в противном случае GROUP_CONCAT + json_decode как фоллбек. Такой подход устраняет дублирование строк при JOIN и даёт чистый результат без циклов по строкам на PHP. Если СУБД или объёмы данных не позволяют — рассматривайте двухзапросный паттерн (parent + children IN(…)) как более надёжный вариант.