При разработке отчетов в Системе компоновки данных (СКД) программисты часто сталкиваются с ситуацией, когда левое соединение двух наборов данных начинает вести себя как внутреннее. Типичный пример: у нас есть первый набор данных (например, плановые показатели из регистра сведений по всей номенклатуре) и второй набор данных (фактические продажи или закупки из регистра накопления). Мы связываем их в СКД по номенклатуре и характеристике. Без отборов все работает корректно — выводятся все плановые показатели, а там, где были закупки, выводятся еще и фактические суммы.
Однако стоит пользователю установить глобальный отбор по какому-либо полю второго (подчиненного) набора данных — например, по Контрагенту или Складу — как из отчета бесследно исчезают строки первого набора, для которых не было движений по закупкам. Происходит искажение плановых данных, хотя период и другие параметры главного регистра не менялись. Выясним причину такого поведения платформы 1С и разберем по шагам несколько эффективных способов решения этой проблемы.
Для того чтобы понять логику работы СКД, обратимся к официальной технической документации. По умолчанию все связи между наборами данных в схеме компоновки считаются левыми внешними соединениями. Это означает, что записи родительского (первого) набора должны выводиться в любом случае, даже если для них не найдены соответствующие записи в подчиненном (втором) наборе данных.
Однако при формировании финального макета компоновщик выполняет оптимизацию запросов. Если на поле подчиненного набора данных накладывается глобальный отбор (отбор на уровне всего отчета), компоновщик макета СКД автоматически меняет тип связи между наборами на Внутреннюю. С точки зрения оптимизации СУБД это логично: зачем выбирать строки родительского набора, если для них все равно нет данных в подчиненном наборе, удовлетворяющих условию фильтрации? Но с точки зрения бизнес-логики отчета это приводит к критической ошибке: строки родительского набора, у которых в подчиненных полях должен быть NULL, просто отсекаются.
Рассмотрим подробнее пять проверенных способов, которые помогут обойти это ограничение платформы и сохранить целостность данных.
{ГДЕ ...} в запросе подчиненного набораЭто наиболее красивый и производительный способ решения проблемы непосредственно в тексте запроса. Если подчиненный набор данных выбирает информацию из виртуальной таблицы (например, оборотов или остатков), мы можем передать отбор пользователя напрямую в параметры этой виртуальной таблицы еще до того, как наборы соединятся.
Разберем ситуацию на примере. Пусть во втором наборе у нас выбираются данные из виртуальной таблицы Закупки.Обороты. Нам нужно дать пользователю возможность фильтровать отчет по контрагенту, не ломая левое соединение. Для этого используем фигурные скобки — специальное расширение языка запросов СКД.
В тексте запроса подчиненного набора пропишем конструкцию следующего вида:
ВЫБРАТЬ
ЗакупкиОбороты.Номенклатура КАК Номенклатура,
ЗакупкиОбороты.Характеристика КАК Характеристика,
ЗакупкиОбороты.Контрагент КАК Контрагент,
ЗакупкиОбороты.СтоимостьОборот КАК СтоимостьЗакуплено
ИЗ
РегистрНакопления.Закупки.Обороты(&ДатаНачала, &ДатаКонца, , {(Контрагент).* КАК КонтрагентДляОтбора}) КАК ЗакупкиОбороты
После добавления этого кода в СКД на вкладке "Настройки" появится доступное поле КонтрагентДляОтбора. Проанализируем, что произойдет при формировании отчета:
Если структура отчета сложная или мы не можем изменить запросы, мы можем принудительно переопределить поведение компоновщика макета перед выводом отчета. Для этого нам потребуется написать код в модуле объекта отчета в предопределенной процедуре ПриКомпоновкеРезультата.
Посмотрим на пример реализации этого метода:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
// Получаем настройки пользователя
НастройкиОтчета = КомпоновщикНастроек.ПолучитьНастройки();
// Создаем компоновщик макета
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, НастройкиОтчета, ДанныеРасшифровки);
// Обходим связи наборов данных в сгенерированном макете и принудительно меняем тип связи
Для Каждого Связь Из МакетКомпоновки.СвязиНаборовДанных Цикл
// Нам нужно принудительно установить внешнюю (левую) связь
Связь.ТипСвязи = ТипСвязиНаборовДанныхКомпоновкиДанных.Внешняя;
КонецЦикла;
// Инициализируем процессор компоновки
ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
// Выводим результат в табличный документ
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);
КонецПроцедуры
В этом коде ключевым является обход коллекции СвязиНаборовДанных сгенерированного макета. По умолчанию после наложения отбора компоновщик присваивает свойству ТипСвязи значение Внутренняя, а мы программно возвращаем его в состояние Внешняя. Обратите внимание, что этот метод требует отключения стандартной обработки вывода отчета.
СКД изменяет тип связи на внутреннюю только тогда, когда отбор накладывается на поле, которое присутствует исключительно в зависимом наборе данных. Если мы сделаем так, чтобы это поле присутствовало и выбиралось также в главном наборе, СКД не будет принудительно менять тип связи.
Например, если в первом наборе данных (планы) у нас нет контрагента, но мы можем связать таблицу планов со справочником или другой управленческой таблицей так, чтобы поле Контрагент выбиралось и в первом наборе тоже. В этом случае при наложении глобального отбора по контрагенту СКД сначала отфильтрует строки первого набора, а затем выполнит полноценное левое соединение со вторым набором. Данные не исказятся.
Если вам нужно быстро решить проблему в режиме "1С:Предприятие" без изменения конфигурации и написания кода, можно воспользоваться особенностями наложения фильтров на разных уровнях настроек СКД.
По умолчанию пользователи накладывают отборы на самой верхней вкладке настроек — это глобальный отбор всего отчета, который и заставляет СКД менять тип связи. Но мы можем перенести отбор на уровень конкретной группировки:
В таком сценарии СКД сначала выполнит левое соединение наборов данных в памяти, а уже затем, на этапе вывода группировки, отфильтрует строки. Связь гарантированно останется левой. Однако учитывайте, что при работе с огромными объемами данных этот способ может снизить производительность отчета, так как СУБД сначала выберет все записи без предварительного отсечения на уровне запроса.
Иногда лучшим решением является полный отказ от использования механизма связей нескольких наборов данных СКД в пользу одного набора данных типа "Запрос". В одном запросе мы можем полностью контролировать поведение соединений таблиц с помощью классического ЛЕВОЕ СОЕДИНЕНИЕ.
Рассмотрим структуру такого запроса с использованием временных таблиц:
// Шаг 1: Получаем плановые данные во временную таблицу
ВЫБРАТЬ
СведенияОПланах.Номенклатура КАК Номенклатура,
СведенияОПланах.Характеристика КАК Характеристика,
ЕСТЬNULL(СведенияОПланах.Сумма, 0) КАК ПланСумма
ПОМЕСТИТЬ ВТ_План
ИЗ
РегистрСведений.СведенияОПланируемыхЗакупках КАК СведенияОПланах
ГДЕ
СведенияОПланах.Период МЕЖДУ &ДатаНачала И &ДатаКонца
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Характеристика
;
// Шаг 2: Получаем фактические данные
ВЫБРАТЬ
ЗакупкиОбороты.Номенклатура КАК Номенклатура,
ЗакупкиОбороты.Характеристика КАК Характеристика,
ЗакупкиОбороты.Контрагент КАК Контрагент,
ЗакупкиОбороты.СтоимостьОборот КАК ФактСумма
ПОМЕСТИТЬ ВТ_Факт
ИЗ
РегистрНакопления.Закупки.Обороты(&ДатаНачала, &ДатаКонца, , {Контрагент.*}) КАК ЗакупкиОбороты
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Характеристика
;
// Шаг 3: Соединяем данные левым соединением
ВЫБРАТЬ
ВТ_План.Номенклатура КАК Номенклатура,
ВТ_План.Характеристика КАК Характеристика,
ВТ_План.ПланСумма КАК ПланСумма,
ВТ_Факт.Контрагент КАК Контрагент,
ЕСТЬNULL(ВТ_Fact.ФактСумма, 0) КАК ФактСумма
ИЗ
ВТ_План КАК ВТ_План
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Факт КАК ВТ_Факт
ПО ВТ_План.Номенклатура = ВТ_Факт.Номенклатура
И ВТ_План.Характеристика = ВТ_Факт.Характеристика
В этом примере мы полностью управляем логикой компоновки. Необязательное условие СКД {Контрагент.*} во второй временной таблице позволяет пользователю накладывать отбор, который отфильтрует только фактические закупки, а итоговое левое соединение сохранит все плановые показатели в отчете.
Подводя итог, отметим, что выбор метода зависит от архитектуры вашего отчета. Для простых отчетов идеально подойдет Способ 1 с использованием параметров виртуальных таблиц. Если отчет базируется на сложных наборах объектов или внешних источниках данных, используйте программную корректировку макета из Способа 2.