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