При разработке складских, торговых или производственных систем автоматизации на платформе «1С:Предприятие 8» программисты регулярно сталкиваются с задачей пакетного добавления товаров в табличную часть документа. Типичный сценарий: у нас есть входящий массив позиций (например, из формы подбора, внешнего файла или результата запроса), и нам необходимо перенести эти данные в документ. При этом если товар уже присутствует в документе, нужно просто увеличить его количество, а если отсутствует — создать новую строку.
Ситуация значительно усложняется, когда уникальность строки определяется не одной только номенклатурой, а целым набором аналитик — составным ключом, состоящим, например, из номенклатуры, характеристики и серии товара. В этой статье мы подробно разберем три основных метода решения данной задачи, проанализируем их производительность и рассмотрим важнейшие архитектурные нюансы, такие как сохранение ключей связи в современных типовых конфигурациях.
Прежде чем перейти к программным алгоритмам поиска, необходимо затронуть важнейшую деталь проектирования современных конфигураций (таких как ERP, Комплексная автоматизация 2 или Управление торговлей 11). В этих системах строки табличной части Товары практически никогда не существуют изолированно. Они связаны со строками других табличных частей (например, ЭтапыГрафикаОплаты, ВидыЗапасов, СкидкиНаценки) с помощью специального реквизита — КлючСвязи (или КлючСтроки).
Если мы решим пойти по «простому» пути — выгрузить текущую табличную часть в таблицу значений, свернуть ее, добавить новые строки из внешнего источника и загрузить обратно методом Загрузить, — то все уникальные идентификаторы связей будут безвозвратно утеряны. Это гарантированно приведет к нарушению логики проведения документа и системным ошибкам. Поэтому наиболее безопасным подходом является изменение строк «по месту» (in-place) — то есть точечный поиск существующей строки и модификация ее полей без удаления.
---
Самый очевидный способ поиска строк по составному ключу в таблице значений или табличной части — использование метода НайтиСтроки(). В качестве параметра этот метод принимает структуру отбора. Рассмотрим пример реализации этого метода на практике:
// Предположим, что ТЗ_НовыеТовары - это таблица значений с добавляемым товаром,
// а ДокументОбъект.Товары - целевая табличная часть документа.
// Шаг 1. Для ускорения поиска обязательно индексируем табличную часть по ключевым полям
ДокументОбъект.Товары.Индексы.Добавить("Номенклатура, Характеристика, Серия");
// Шаг 2. Обходим входящую таблицу новых товаров
Для Каждого СтрокаНовая Из ТЗ_НовыеТовары Цикл
// Формируем структуру отбора по составному ключу
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", СтрокаНовая.Номенклатура);
KeepХарактеристика = ?(ЗначениеЗаполнено(СтрокаНовая.Характеристика), СтрокаНовая.Характеристика, Справочники.ХарактеристикиНоменклатуры.ПустаяСсылка());
Отбор.Вставить("Характеристика", KeepХарактеристика);
KeepСерия = ?(ЗначениеЗаполнено(СтрокаНовая.Серия), СтрокаНовая.Серия, Справочники.СерииНоменклатуры.ПустаяСсылка());
Отбор.Вставить("Серия", KeepСерия);
// Выполняем поиск в ТЧ документа
НайденныеСтроки = ДокументОбъект.Товары.НайтиСтроки(Отбор);
Если НайденныеСтроки.Количество() > 0 Тогда
// Строка найдена. Берем первую совпавшую и увеличиваем количество
СтрокаТЧ = НайденныеСтроки[0];
СтрокаТЧ.Количество = СтрокаТЧ.Количество + СтрокаНовая.Количество;
// При необходимости пересчитываем сумму и НДС
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
Иначе
// Строка отсутствует. Добавляем новую запись
СтрокаТЧ = ДокументОбъект.Товары.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаТЧ, СтрокаНовая);
КонецЕсли;
КонецЦикла;
Важное правило оптимизации 1С: Индексирование коллекции дает прирост скорости только в том случае, если структура отбора в методе НайтиСтроки() содержит точно такой же набор полей, который был передан при создании индекса. Если вы проиндексировали три поля, а поиск ведете только по двум — платформа проигнорирует созданный индекс и выполнит медленный линейный обход всей таблицы.
---
Когда количество строк в документе превышает 500-1000 элементов, даже индексированный поиск через НайтиСтроки может демонстрировать падение производительности. Наиболее эффективным объектным методом в экосистеме «1С:Предприятие» является использование хэш-таблицы, роль которой выполняет объект Соответствие. Поиск элемента в нем выполняется за константное время $O(1)$.
Поскольку Соответствие не умеет автоматически сравнивать сложные составные структуры (при передаче структуры в качестве ключа платформа будет сравнивать ее по ссылке на объект в памяти, а не по значениям внутри нее), мы должны самостоятельно сконструировать уникальный строковый ключ из идентификаторов наших аналитик.
Разберем этот элегантный и быстрый алгоритм по шагам:
// Создаем соответствие для кэширования строк ТЧ
КэшСтрок = Новый Соответствие;
ТабличнаяЧастьТовары = ДокументОбъект.Товары;
// Шаг 1. Заполняем кэш существующими строками документа
Для Каждого СтрокаТЧ Из ТабличнаяЧастьТовары Цикл
// Конструируем уникальный строковый ключ на основе УИД ссылочных типов
НоменклатураУИД = Строка(СтрокаТЧ.Номенклатура.УникальныйИдентификатор());
ХарактУИД = ?(ЗначениеЗаполнено(СтрокаТЧ.Характеристика), Строка(СтрокаТЧ.Характеристика.УникальныйИдентификатор()), "0");
СерияУИД = ?(ЗначениеЗаполнено(СтрокаТЧ.Серия), Строка(СтрокаТЧ.Серия.УникальныйИдентификатор()), "0");
УникальныйКлюч = НоменклатураУИД + "_" + ХарактУИД + "_" + СерияУИД;
// Записываем в кэш ссылку на саму строку табличной части
КэшСтрок.Вставить(УникальныйКлюч, СтрокаТЧ);
КонецЦикла;
// Шаг 2. Обходим добавляемый товар и производим мгновенный поиск
Для Каждого СтрокаНовая Из ТЗ_НовыеТовары Цикл
НоменклатураУИД = Строка(СтрокаНовая.Номенклатура.УникальныйИдентификатор());
ХарактУИД = ?(ЗначениеЗаполнено(СтрокаНовая.Характеристика), Строка(СтрокаНовая.Характеристика.УникальныйИдентификатор()), "0");
СерияУИД = ?(ЗначениеЗаполнено(СтрокаНовая.Серия), Строка(СтрокаНовая.Серия.УникальныйИдентификатор()), "0");
КлючПоиска = НоменклатураУИД + "_" + ХарактУИД + "_" + СерияУИД;
СтрокаТЧ = КэшСтрок.Получить(КлючПоиска);
Если СтрокаТЧ <> Неопределено Тогда
// Товар найден в кэше, обновляем количество
СтрокаТЧ.Количество = СтрокаТЧ.Количество + СтрокаНовая.Количество;
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
Иначе
// Товара нет, создаем новую строку ТЧ
СтрокаТЧ = ТабличнаяЧастьТовары.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаТЧ, СтрокаНовая);
// КРИТИЧЕСКИ ВАЖНО: Добавляем новую строку в кэш!
// Это предотвратит создание дублей, если один и тот же товар встретится в ТЗ_НовыеТовары несколько раз
КэшСтрок.Вставить(КлючПоиска, СтрокаТЧ);
КонецЕсли;
КонецЦикла;
Этот метод работает невероятно быстро даже на таблицах с десятками тысяч строк, поскольку исключает накладные расходы платформы на постоянное сканирование индексов и переборы массивов строк.
---
Если перед вами стоит задача обработать колоссальный объем данных (например, загрузка чеков из внешней системы за целый месяц или групповое проведение сотен документов корректировок), объектные методы в цикле могут нагрузить процессор. В этом случае правильным решением будет переложить вычислительную работу на СУБД с помощью механизма временных таблиц.
Проанализируем логику этого подхода. Мы выгружаем текущую табличную часть документа во временную таблицу, туда же загружаем таблицу новых товаров, объединяем их через оператор ОБЪЕДИНИТЬ ВСЕ, выполняем группировку с суммированием количества и выгружаем обратно. Нам нужно быть аккуратными, чтобы не затереть другие важные поля. Посмотрим, как это выглядит в программном коде:
// Шаг 1. Выгружаем текущую ТЧ и новые товары в таблицы значений
ТЗ_ТекущиеТовары = ДокументОбъект.Товары.Выгрузить();
// Шаг 2. Инициализируем менеджер временных таблиц и запрос
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Описываем схему и передаем данные текущих товаров в ВТ
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_ТекущиеТовары ИЗ &ТЗ_ТекущиеТовары КАК ТЗ_ТекущиеТовары";
Запрос.УстановитьПараметр("ТЗ_ТекущиеТовары", ТЗ_ТекущиеТовары);
Запрос.Выполнить();
// Передаем данные новых товаров в ВТ
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_НовыеТовары ИЗ &ТЗ_НовыеТовары КАК ТЗ_НовыеТовары";
Запрос.УстановитьПараметр("ТЗ_НовыеТовары", ТЗ_НовыеТовары);
Запрос.Выполнить();
// Шаг 3. Объединяем и группируем данные в запросе
Запрос.Текст =
"ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Характеристика КАК Характеристика,
| ВТ.Серия КАК Серия,
| СУММА(ВТ.Количество) КАК Количество,
| МАКСИМУМ(ВТ.Цена) КАК Цена
|ПОМЕСТИТЬ ВТ_ИтоговыеТовары
|ИЗ
| (ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Характеристика КАК Характеристика,
| Т.Серия КАК Серия,
| Т.Количество КАК Количество,
| Т.Цена КАК Цена
| ИЗ
| ВТ_ТекущиеТовары КАК Т
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ИЗ
| ВТ_НовыеТовары КАК Н) КАК ВТ
|
|ГРУППИРОВАТЬ ПО
| ВТ.Номенклатура,
| ВТ.Характеристика,
| ВТ.Серия;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ * ИЗ ВТ_ИтоговыеТовары КАК ИтоговыеТовары";
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// Шаг 4. Безопасно обновляем ТЧ документа
// Чтобы сохранить ключи связей для неизменных товаров, мы можем применить комбинированный подход:
// Очищаем ТЧ и заполняем ее заново только в том случае, если документ "простой".
// Если документ сложный (с ключами связи), то используем РезультатЗапроса для обновления "по месту" через кэш из Метода 2.
ДокументОбъект.Товары.Загрузить(РезультатЗапроса);
// Пересчитываем суммы для загруженных строк
Для Каждого СтрокаТЧ Из ДокументОбъект.Товары Цикл
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецЦикла;
При использовании запросов всегда помните о правильном расчете зависимых полей. Поля Цена и СтавкаНДС ни в коем случае нельзя суммировать, к ним применяются агрегатные функции МАКСИМУМ. А такие расчетные реквизиты, как Сумма и СуммаНДС, рекомендуется вычислять заново после проведения группировки по формуле Количество * Цена, чтобы избежать копеечных погрешностей округления.
---
Для того чтобы сделать правильный выбор архитектурного решения в вашей конкретной задаче, воспользуйтесь следующими рекомендациями: