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