В процессе разработки в среде 1С:Предприятие часто возникает задача обогащения уже существующей таблицы значений новыми данными. Например, у нас есть готовая таблица товаров, полученная из типовых механизмов (например, из общего модуля обеспечения), и нам необходимо «приклеить» к ней ещё несколько колонок со значениями, которые мы выбрали из базы данных с помощью запроса. После этого обновленная таблица загружается в табличную часть документа.
На первый взгляд задача кажется простой, но при её реализации начинающие разработчики часто совершают критические ошибки, которые приводят либо к сильному замедлению работы системы, либо к порче данных (когда все строки заполняются одинаковыми значениями или данные перепутываются). В этой статье мы подробно разберем, почему возникают эти ошибки, и рассмотрим три надежных и быстрых способа решения этой задачи.
Для начала проанализируем ситуацию, с которой столкнулся автор темы. Он попытался решить задачу с помощью вложенного цикла следующего вида:
Для Каждого Строка Из ТаблицаТовары Цикл
Для Каждого Элемент Из ВыборкаДетальныеЗаписи Цикл
ЗаполнитьЗначенияСвойств(Строка, Элемент);
КонецЦикла;
КонецЦикла;
Выясним причину, почему этот код работает некорректно. При выполнении такого алгоритма для каждой строки исходной таблицы ТаблицаТовары запускается полный перебор выборки запроса ВыборкаДетальныеЗаписи. Метод ЗаполнитьЗначенияСвойств последовательно копирует свойства текущего элемента выборки в текущую строку таблицы. В результате на каждой итерации внутреннего цикла данные предыдущего элемента перезаписываются последующим. Когда внутренний цикл завершается, в текущей строке таблицы остаются данные исключительно последней записи из выборки. Так происходит со всеми строками таблицы, и на выходе мы получаем полностью идентичные строки с некорректной информацией.
Вторая распространенная ошибка — попытка сопоставлять строки «по порядку». Некоторые разработчики надеются, что если количество строк в таблице и в выборке одинаковое, можно просто обходить их параллельно по индексу. Это крайне опасное заблуждение. Без явного указания предложения УПОРЯДОЧИТЬ ПО в запросе СУБД возвращает результат в произвольном порядке. Даже применение ключевого слова АВТОУПОРЯДОЧИВАНИЕ не гарантирует совпадения индексов строк в двух разных коллекциях данных. Любое несовпадение приведет к тому, что значения «приклеятся» не к тем товарам.
Третья ошибка — попытка использовать метод НайтиСтроки() непосредственно у объекта выборки запроса (ВыборкаИзРезультатаЗапроса). У этого объекта нет такого метода, он предназначен исключительно для последовательного обхода данных (курсора СУБД). Использование несуществующего метода приведет к аварийному завершению программы.
Теперь, когда мы понимаем суть проблем, давайте рассмотрим правильные архитектурные подходы к решению этой задачи.
Это классический и очень надежный способ сопоставления данных в коде. Мы выгружаем результат запроса в отдельную таблицу значений, строим по ней специальный индекс для быстрого поиска и затем осуществляем поиск строк по ключевым полям (например, по номенклатуре и характеристике).
Рассмотрим этот процесс по шагам:
ТаблицаТовары с обязательным указанием типов данных.Посмотрим на пример программного кода, реализующий этот алгоритм:
// Шаг 1: Подготавливаем структуру исходной таблицы
// Добавляем новые колонки с явным указанием типов данных для корректной последующей загрузки
ОписаниеТипаСправочник = Новый ОписаниеТипов("СправочникСсылка.КлассификаторFSC");
ОписаниеТипаЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3));
ТаблицаТовары.Колонки.Добавить("FSC", ОписаниеТипаСправочник);
ТаблицаТовары.Колонки.Добавить("КубМетровВПачке", ОписаниеТипаЧисло);
ТаблицаТовары.Колонки.Добавить("КубМетровВсего", ОписаниеТипаЧисло);
// Шаг 2: Выгружаем результат запроса в таблицу значений
ТаблицаЗапроса = РезультатЗапроса.Выгрузить();
// Шаг 3: Создаем индекс по ключевым полям для ускорения поиска
// Без индекса метод НайтиСтроки() при большом объеме данных будет работать медленно (O(N))
ТаблицаЗапроса.Индексы.Добавить("Номенклатура, Характеристика");
// Шаг 4: Обходим исходную таблицу и заполняем новые колонки
ПараметрыПоиска = Новый Структура("Номенклатура, Характеристика");
Для Каждого СтрокаТовары Из ТаблицаТовары Цикл
// Заполняем структуру поиска значениями из текущей строки исходной таблицы
ЗаполнитьЗначенияСвойств(ПараметрыПоиска, СтрокаТовары);
// Быстро находим подходящие строки благодаря индексу
НайденныеСтроки = ТаблицаЗапроса.НайтиСтроки(ПараметрыПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
// Переносим значения только для добавленных нами колонок
ЗаполнитьЗначенияСвойств(СтрокаТовары, НайденныеСтроки[0], "FSC, КубМетровВПачке, КубМетровВсего");
КонецЕсли;
КонецЦикла;
Если вам требуется достичь максимального быстродействия при слиянии коллекций на стороне сервера приложений без обращения к СУБД, отличным решением станет использование объекта Соответствие. Поиск элемента по ключу в объекте Соответствие происходит практически мгновенно — за константное время $O(1)$.
Важный нюанс платформы 1С: Мы не можем использовать объект типа Структура в качестве ключа для объекта Соответствие, так как сравнение сложных объектов в платформе идет по ссылке на сам объект в памяти, а не по его содержимому. Чтобы обойти это ограничение, мы будем формировать уникальный строковый ключ на основе уникальных идентификаторов (GUID) ключевых полей связи.
Проанализируем пример реализации данного метода:
// Добавляем новые колонки в исходную таблицу значений
ОписаниеТипаСправочник = Новый ОписаниеТипов("СправочникСсылка.КлассификаторFSC");
ОписаниеТипаЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3));
ТаблицаТовары.Колонки.Добавить("FSC", ОписаниеТипаСправочник);
ТаблицаТовары.Колонки.Добавить("КубМетровВПачке", ОписаниеТипаЧисло);
ТаблицаТовары.Колонки.Добавить("КубМетровВсего", ОписаниеТипаЧисло);
// Выгружаем данные запроса
ТаблицаЗапроса = РезультатЗапроса.Выгрузить();
// Заполняем Соответствие, где ключом будет уникальная строка связи, а значением - строка таблицы запроса
СоответствиеДанных = Новый Соответствие;
Для Каждого СтрокаЗапроса Из ТаблицаЗапроса Цикл
УИДНоменклатуры = Строка(СтрокаЗапроса.Номенклатура.УникальныйИдентификатор());
УИДХарактеристики = ?(ЗначениеЗаполнено(СтрокаЗапроса.Характеристика),
Строка(СтрокаЗапроса.Характеристика.УникальныйИдентификатор()),
"ПустаяХарактеристика");
КлючСвязи = УИДНоменклатуры + "_" + УИДХарактеристики;
СоответствиеДанных.Вставить(КлючСвязи, СтрокаЗапроса);
КонецЦикла;
// Быстро обогащаем исходную таблицу
Для Каждого СтрокаТовары Из ТаблицаТовары Цикл
УИДНоменклатуры = Строка(СтрокаТовары.Номенклатура.УникальныйИдентификатор());
УИДХарактеристики = ?(ЗначениеЗаполнено(СтрокаТовары.Характеристика),
Строка(СтрокаТовары.Характеристика.УникальныйИдентификатор()),
"ПустаяХарактеристика");
КлючСвязи = УИДНоменклатуры + "_" + УИДХарактеристики;
СтрокаИсточника = СоответствиеДанных.Получить(КлючСвязи);
Если СтрокаИсточника <> Неопределено Тогда
ЗаполнитьЗначенияСвойств(СтрокаТовары, СтрокаИсточника, "FSC, КубМетровВПачке, КубМетровВсего");
КонецЕсли;
КонецЦикла;
С точки зрения стандартов разработки фирмы «1С», наиболее правильным, производительным и «чистым» решением является перенос операции объединения данных непосредственно на уровень СУБД. Процессор запросов SQL оптимизирует эту операцию гораздо эффективнее, чем любые циклы в коде 1С.
Для реализации этого способа мы передаем исходную таблицу ТаблицаТовары в качестве параметра в новый запрос, помещаем её во временную таблицу и выполняем операцию ЛЕВОЕ СОЕДИНЕНИЕ с физической таблицей или результатом другого запроса.
Важное требование: Чтобы передать таблицу значений в качестве параметра запроса, все её колонки должны быть строго типизированы. Если исходная функция вернула таблицу с нетипизированными колонками (тип Произвольный), этот метод может вызвать ошибку. В таком случае колонки необходимо сначала типизировать.
Рассмотрим пример реализации соединения на уровне запроса:
// Создаем запрос с менеджером временных таблиц
ЗапросОбъединения = Новый Запрос;
ЗапросОбъединения.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Передаем исходную таблицу как параметр
ЗапросОбъединения.УстановитьПараметр("ТаблицаТовары", ТаблицаТовары);
ЗапросОбъединения.УстановитьПараметр("ЗаказКлиента", ЗаказКлиента);
// Формируем текст запроса с левым соединением
ЗапросОбъединения.Текст =
"ВЫБРАТЬ
| ИсходнаяТаблица.Номенклатура КАК Номенклатура,
| ИсходнаяТаблица.Характеристика КАК Характеристика,
| ИсходнаяТаблица.Количество КАК Количество,
| ИсходнаяТаблица.Цена КАК Цена,
| ИсходнаяТаблица.Сумма КАК Сумма
|ПОМЕСТИТЬ ВТ_ИсходныеТовары
|ИЗ
| &ТаблицаТовары КАК ИсходнаяТаблица
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Характеристика КАК Характеристика,
| ВТ.Количество КАК Количество,
| ВТ.Цена КАК Цена,
| ВТ.Сумма КАК Сумма,
| // Добавляем новые колонки из данных Заказа клиента
| ЕСТЬNULL(ДопСведения.FSC, ЗНАЧЕНИЕ(Справочник.КлассификаторFSC.ПустаяСсылка)) КАК FSC,
| ЕСТЬNULL(ДопСведения.КубМетровВПачке, 0) КАК КубМетровВПачке,
| ЕСТЬNULL(ДопСведения.КубМетровВсего, 0) КАК КубМетровВсего
|ИЗ
| ВТ_ИсходныеТовары КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента.Товары КАК ДопСведения
| ПО ВТ.Номенклатура = ДопСведения.Номенклатура
| И ВТ.Характеристика = ДопСведения.Характеристика
| И ДопСведения.Ссылка = &ЗаказКлиента";
// Выполняем запрос и выгружаем результат обратно в переменную
ТаблицаТовары = ЗапросОбъединения.Выполнить().Выгрузить();
// Теперь таблица полностью готова для загрузки в табличную часть документа
Товары.Загрузить(ТаблицаТовары);
Мы рассмотрели три эффективных метода решения задачи по добавлению колонок с данными из запроса в существующую таблицу значений. Для простых сценариев и небольшого объема данных отлично подойдет Способ 1 (Индексированная ТЗ). Если вам критически важна скорость работы алгоритмов в оперативной памяти сервера приложений, используйте Способ 2 (Соответствие).
Для разработки крупных корпоративных систем и тяжелых документов наиболее правильным выбором будет Способ 3 (Соединение в запросе), так как он минимизирует нагрузку на сервер приложений, не создает лишних циклов в коде и использует всю мощь оптимизатора СУБД.