В процессе разработки и сопровождения систем на платформе 1С:Предприятие 8 часто возникает задача поиска определенных данных по всей базе. Типичный пример — необходимость найти все документы, в табличных частях которых фигурируют конкретная Номенклатура и связанная с ней СерияНоменклатуры. Трудность заключается в том, что структура метаданных может быть сложной, а список документов, использующих эти реквизиты, заранее не известен или может измениться при обновлении конфигурации.
В этой статье мы подробно разберем, как решить эту задачу несколькими способами: от динамического конструирования запроса по метаданным до использования встроенных оптимизированных механизмов платформы. Мы проанализируем код, предложенный на форуме, исправим его критические ошибки и рассмотрим альтернативные, более производительные архитектурные подходы.
Если нам действительно неизвестно, в каких именно документах и табличных частях содержатся реквизиты Номенклатура и СерияНоменклатуры, мы можем обойти дерево метаданных программно, найти нужные объекты и динамически сконструировать текст запроса.
Давайте подробно разберем и оптимизируем алгоритм, который пытался реализовать автор темы. В исходном коде автора содержалось несколько критических недочетов, из-за которых запрос работал крайне неэффективно или возвращал неполные данные. Рассмотрим эти ошибки:
СерияНоменклатуры, но забыл добавить условие фильтрации в текст запроса, из-за чего отбор происходил только по номенклатуре.ОБЪЕДИНИТЬ (UNION) заставляет СУБД проверять строки на уникальность и удалять дубликаты, что сильно замедляет работу. Поскольку ссылки на документы уникальны по определению, необходимо использовать ОБЪЕДИНИТЬ ВСЕ (UNION ALL).ВременнаяТаблица.Номенклатура.Ссылка вместо прямой проверки значения ВременнаяТаблица.Номенклатура создает лишнее неявное соединение таблиц на уровне СУБД.Ниже представлен исправленный, полностью готовый и оптимизированный код для динамического поиска данных по всем документам:
Процедура НайтиДокументыПоНоменклатуреИСерии(Кнопка)
// Определяем искомые значения
СсылкаНоменклатура = Справочники.Номенклатура.НайтиПоКоду("Н0021225");
СсылкаСерия = Справочники.СерииНоменклатуры.НайтиПоКоду("000030631", Истина);
Если СсылкаНоменклатура.Пустая() Или СсылкаСерия.Пустая() Тогда
Предупреждение("Не найдены элементы для поиска! Проверьте коды.");
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("СсылкаНоменклатура", СсылкаНоменклатура);
Запрос.УстановитьПараметр("СерияНоменклатуры", СсылкаСерия);
МассивЗапросов = Новый Массив;
// Обходим метаданные документов
Для Каждого Документ Из Метаданные.Документы Цикл
Для Каждого ТабличнаяЧасть Из Документ.ТабличныеЧасти Цикл
ЕстьНоменклатура = Ложь;
ЕстьСерия = Ложь;
// Проверяем наличие нужных реквизитов в табличной части
Для Каждого Реквизит Из ТабличнаяЧасть.Реквизиты Цикл
Если Реквизит.Имя = "СерияНоменклатуры" Тогда
ЕстьСерия = Истина;
ИначеЕсли Реквизит.Имя = "Номенклатура" Тогда
ЕстьНоменклатура = Истина;
КонецЕсли;
КонецЦикла;
// Если оба реквизита присутствуют, формируем часть запроса
Если ЕстьНоменклатура И ЕстьСерия Тогда
ШаблонЗапроса = "
|ВЫБРАТЬ
| ТЧ.Ссылка КАК Ссылка,
| ТЧ.Номенклатура КАК Номенклатура,
| ТЧ.СерияНоменклатуры КАК СерияНоменклатуры
|ИЗ
| Документ.[ИмяДокумента].[ИмяТЧ] КАК ТЧ
|ГДЕ
| ТЧ.Номенклатура = &СсылкаНоменклатура
| И ТЧ.СерияНоменклатуры = &СерияНоменклатуры";
ТекстПодзапроса = СтрЗаменить(ШаблонЗапроса, "[ИмяДокумента]", Документ.Имя);
ТекстПодзапроса = СтрЗаменить(ТекстПодзапроса, "[ИмяТЧ]", ТабличнаяЧасть.Имя);
МассивЗапросов.Добавить(ТекстПодзапроса);
КонецЕсли;
КонецЦикла;
КонецЦикла;
// Если подходящих табличных частей не найдено, выходим
Если МассивЗапросов.Количество() = 0 Тогда
Сообщить("В конфигурации не найдены документы с подходящей структурой табличных частей.");
Возврат;
КонецЕсли;
// Объединяем все запросы через ОБЪЕДИНИТЬ ВСЕ для максимальной производительности
Разделитель = "
|ОБЪЕДИНИТЬ ВСЕ
|";
Запрос.Текст = СтрСоединить(МассивЗапросов, Разделитель) + "
|УПОРЯДОЧИТЬ ПО
| Ссылка";
// Выполняем запрос
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
// Выводим результат пользователю
Для Каждого Строка Из ТаблицаРезультатов Цикл
Сообщить(ШаблонСообщения("Найден документ: %1, Номенклатура: %2, Серия: %3",
Строка.Ссылка, Строка.Номенклатура, Строка.СерияНоменклатуры));
КонецЦикла;
Сообщить("Всего найдено строк: " + ТаблицаРезультатов.Количество());
КонецПроцедуры
В этом варианте фильтрация происходит на самом раннем этапе — на уровне СУБД при сканировании каждой конкретной таблицы. База данных использует индексы табличных частей (если они созданы для реквизитов Номенклатура и СерияНоменклатуры), что обеспечивает высокую скорость выполнения даже на крупных информационных базах.
Во время обсуждения на форуме у автора возник вопрос: почему при установке точки останова на строке выполнения запроса он не видит данные временной таблицы. Разберем этот важный нюанс отладки.
Когда мы используем конструкцию ПОМЕСТИТЬ ВременнаяТаблица, запрос не возвращает результат в привычном виде. Данные сохраняются во временном хранилище СУБД, привязанном к менеджеру временных таблиц МВТ. Чтобы посмотреть содержимое временной таблицы в режиме отладки (начиная с платформы 8.3.15), выполните следующие действия:
ТаблицаЗначений = Запрос.Выполнить().Выгрузить();.Запрос.МенеджерВременныхТаблиц.Хотя динамический запрос по метаданным решает задачу "в лоб", в реальной практике разработки в среде 1С обращение напрямую к табличным частям всех документов считается плохим тоном (антипаттерном) и используется в крайних случаях (например, при написании универсальных обработок поиска и замены дубликатов).
В типовых конфигурациях (таких как ERP, Комплексная автоматизация, Управление торговлей или УНФ) документы никогда не хранят данные изолированно. При проведении они всегда записывают информацию в регистры накопления или сведения (например, ТоварыНаСкладах, ДвиженияСерийНоменклатуры, ВыручкаИПродажи).
Вместо обхода десятков тяжелых таблиц документов гораздо эффективнее сделать простой и быстрый запрос к виртуальной таблице оборотов или остатков соответствующего регистра. Измерение Регистратор в результате такого запроса и будет являться искомым документом. Рассмотрим пример такого запроса к регистру накопления ТоварыНаСкладах:
Запрос = Новый Запрос;
Запрос.Текст = "
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| ТоварыНаСкладах.Регистратор КАК Документ,
| ТоварыНаСкладах.Номенклатура КАК Номенклатура,
| ТоварыНаСкладах.Серия КАК Серия
|ИЗ
| РегистрНакопления.ТоварыНаСкладах КАК ТоварыНаСкладах
|ГДЕ
| ТоварыНаСкладах.Номенклатура = &СсылкаНоменклатура
| И ТоварыНаСкладах.Серия = &СерияНоменклатуры
|УПОРЯДОЧИТЬ ПО
| Документ";
Запрос.УстановитьПараметр("СсылкаНоменклатура", НужнаяНоменклатура);
Запрос.УстановитьПараметр("СерияНоменклатуры", НужнаяСерия);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Преимущества этого подхода:
Если вам нужно найти абсолютно все упоминания номенклатуры или серии во всей базе данных (не только в табличных частях документов, но и в реквизитах шапки, задачах, бизнес-процессах или справочниках), платформа предоставляет мощный оптимизированный механизм — метод НайтиПоСсылкам().
Проанализируем пример использования этого метода для решения нашей задачи:
// Поиск мест использования номенклатуры
МассивПоиска = Новый Массив;
МассивПоиска.Добавить(СсылкаНоменклатура);
ТаблицаМестИспользования = НайтиПоСсылкам(МассивПоиска);
// ТаблицаМестИспользования содержит колонки:
// Ссылка - объект, в котором найдена ссылка
// Данные - объект метаданных (или реквизит), где именно найдена ссылка
Для Каждого Строка Поиска Из ТаблицаМестИспользования Цикл
СсылкаНаОбъект = СтрокаПоиска.Ссылка;
МетаданныеОбъекта = СсылкаНаОбъект.Метаданные();
// Проверяем, является ли найденный объект документом
Если Метаданные.Документы.Содержит(МетаданныеОбъекта) Тогда
// Теперь мы можем программно проверить, есть ли в этом документе нужная нам серия
// (например, прочитав табличную часть найденного документа)
КонецЕсли;
КонецЦикла;
Если в конфигурацию встроена Библиотека стандартных подсистем (БСП), рекомендуется использовать аналогичную, но более функциональную процедуру из общего модуля БСП: ОбщегоНазначения.МестаИспользования(). Данный метод отрабатывает значительно быстрее ручного сканирования таблиц метаданных, поскольку СУБД использует готовые внутренние таблицы связей платформы.
В платформе 1С:Предприятие 8 существует специализированный объект метаданных — Критерии отбора. Он предназначен именно для быстрого поиска документов по определенным значениям реквизитов (например, критерий отбора СвязанныеДокументы или ДокументыПоНоменклатуре).
Если в вашей конфигурации настроен критерий отбора, связанный с номенклатурой, вы можете получить список всех документов одной строчкой запроса, без обращения к метаданным и циклам:
Запрос = Новый Запрос;
Запрос.Текст = "
|ВЫБРАТЬ
| Критерий.Ссылка КАК Документ
|ИЗ
| КритерийОтбора.СвязанныеДокументы(&Номенклатура) КАК Критерий";
Запрос.УстановитьПараметр("Номенклатура", НужнаяНоменклатура);
ТаблицаДокументов = Запрос.Выполнить().Выгрузить();
Этот способ является наиболее производительным на уровне СУБД, так как платформа автоматически создает и поддерживает индексы для всех реквизитов, входящих в состав критерия отбора.