Часто при разработке в 1С мы сталкиваемся с классической задачей: необходимо получить список документов, удовлетворяющих определенным условиям. Но проблема заключается в том, что эти условия применяются не к реквизитам шапки документа, а к строкам его табличной части. Разберем типичную ситуацию: в документе есть 5 строк, которые подходят под наше условие. В результате выполнения прямого запроса к табличной части мы получим 5 строк выборки. Однако для дальнейшей логики программы (например, проведения, отправки уведомлений или выгрузки) нам нужно обработать этот документ всего один раз, а не пять.
Проанализируем эту ситуацию подробнее и рассмотрим несколько способов элегантного и архитектурно правильного решения данной проблемы, избегая распространенных ошибок.
Для начала давайте посмотрим, как делать не нужно. Очень часто начинающие разработчики пытаются решить эту проблему "в лоб" на уровне кода перебора выборки, сохраняя ссылку на предыдущий обработанный документ в переменную. Выглядит это примерно так:
НомерПредыдущегоДокумента = Неопределено;
Если Не Рез.Пустой() Тогда
Выборка = Рез.Выбрать();
Пока Выборка.Следующий() Цикл
Если НомерПредыдущегоДокумента = Неопределено ИЛИ Выборка.Номер <> НомерПредыдущегоДокумента Тогда
Сообщить("Отправляется " + Выборка.Номер);
НомерПредыдущегоДокумента = Выборка.Номер;
КонецЕсли;
КонецЦикла;
КонецЕсли;
Почему этот подход считается признаком плохой архитектуры? Проблема кроется в избыточности получаемых данных. Запрос вытаскивает из базы данных, передает по сети на сервер 1С и помещает в оперативную память все дублирующиеся строки. Если документов тысячи, а строк в них десятки тысяч, такая выборка создаст огромную и совершенно бессмысленную нагрузку на систему. Все фильтрации, отсечения дублей и сортировки всегда должны выполняться на стороне СУБД средствами языка запросов.
Самый простой способ избавиться от дублей прямо в запросе — использовать ключевое слово РАЗЛИЧНЫЕ (аналог DISTINCT в SQL). Этот метод идеально подходит, если в результате запроса вам нужны только реквизиты шапки документа (Ссылка, Номер, Дата и т.д.), и вам не нужны данные самих строк табличной части, по которым производился отбор.
Рассмотрим пример запроса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ТЧ_ОтметкиСклада.Ссылка КАК Ссылка,
ТЧ_ОтметкиСклада.Ссылка.Номер КАК Номер,
ТЧ_ОтметкиСклада.Ссылка.Дата КАК Дата
ИЗ
Документ.ПредварительныйЗаказПоставщику.ОтметкиСклада КАК ТЧ_ОтметкиСклада
ГДЕ
ТЧ_ОтметкиСклада.Сертификат = ЛОЖЬ
И ТЧ_ОтметкиСклада.ПаспортИзделия = ЛОЖЬ
При использовании РАЗЛИЧНЫЕ система проанализирует все полученные строки и "схлопнет" полностью идентичные записи в одну. В итоге, даже если в документе 10 строк без сертификата, в выборку попадет только одна запись с данным документом.
С точки зрения оптимизации платформы 1С и чистоты кода, наиболее правильным подходом является разделение сущностей. Основной запрос должен обращаться к таблице документов, а фильтрация по табличной части выносится во вложенный запрос с использованием оператора В.
Посмотрим на пример такого запроса:
ВЫБРАТЬ
ДокументШапка.Ссылка КАК Ссылка,
ДокументШапка.Номер КАК Номер
ИЗ
Документ.ПредварительныйЗаказПоставщику КАК ДокументШапка
ГДЕ
ДокументШапка.Ссылка В (
ВЫБРАТЬ
ТЧ.Ссылка
ИЗ
Документ.ПредварительныйЗаказПоставщику.ОтметкиСклада КАК ТЧ
ГДЕ
ТЧ.Сертификат = ЛОЖЬ
И ТЧ.ПаспортИзделия = ЛОЖЬ
)
Разберем по шагам, как это работает. База данных сначала выполняет вложенный запрос, собирая список ссылок на документы, где в табличной части есть нужные нам строки. Затем основной запрос выбирает документы из главной таблицы, проверяя, входит ли их ссылка в полученный список. Этот метод гарантированно вернет каждый подходящий документ ровно один раз, исключая появление дублей по определению, так как мы выбираем из таблицы шапок, где ссылки уникальны.
Иногда возникает усложненная задача: нам нужно обойти каждый подходящий документ один раз (например, для формирования письма), но внутри этого обхода нам необходимо получить доступ к конкретным строкам табличной части, которые подошли под условие (чтобы перечислить проблемные товары в этом письме).
В этом случае методы с РАЗЛИЧНЫЕ или вложенным запросом не подойдут, так как они отсекают данные строк. Здесь нам на помощь приходит механизм итогов и специальный обход результата запроса.
В конец исходного запроса к табличной части необходимо добавить конструкцию ИТОГИ ПО:
ВЫБРАТЬ
ТЧ.Ссылка КАК Ссылка,
ТЧ.ДатаПоступления КАК ДатаПоступления,
ТЧ.Сертификат КАК Сертификат
ИЗ
Документ.ПредварительныйЗаказПоставщику.ОтметкиСклада КАК ТЧ
ГДЕ
ТЧ.Сертификат = ЛОЖЬ
ИТОГИ ПО
Ссылка
Теперь мы изменим код обхода результата запроса. Мы будем использовать двойной цикл и метод ОбходРезультатаЗапроса.ПоГруппировкам. Проанализируем этот код:
Рез = Запрос.Выполнить();
Если Не Рез.Пустой() Тогда
// Получаем выборку верхнего уровня (уникальные документы)
ВыборкаДокумент = Рез.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокумент.Следующий() Цикл
// Этот блок кода выполнится ровно ОДИН РАЗ для каждого документа
Сообщить("Обрабатываем документ: " + ВыборкаДокумент.Ссылка);
// Получаем детальные записи для текущего документа
ВыборкаДетали = ВыборкаДокумент.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// Этот блок выполнится для каждой подходящей строки ТЧ документа
Сообщить(" -- Строка с датой поступления: " + ВыборкаДетали.ДатаПоступления);
КонецЦикла;
КонецЦикла;
КонецЕсли;
Этот элегантный паттерн позволяет убить сразу двух зайцев: вы получаете возможность обработать документ единожды на уровне группировки и сохраняете доступ ко всем детальным записям табличной части во вложенном цикле. Выбор метода зависит исключительно от ваших конечных целей: нужны только документы — используйте подзапрос с условием В; нужны и документы, и детализированные строки — используйте ИТОГИ ПО.