При разработке печатных форм и отчетов в 1С часто возникает задача: вывести данные основного документа в одну строку, а все связанные значения из его табличной части (например, номера экземпляров, сертификатов или соисполнителей) объединить в строку и поместить в одну-единственную ячейку. При неправильном подходе к написанию запроса и циклов обхода разработчики сталкиваются с дублированием строк, медленной работой алгоритмов или «кашей» из данных, когда значения одного документа перетекают в другой. Рассмотрим, почему возникают эти ошибки и как реализовать задачу правильно, красиво и быстро.
Проанализируем типичные ошибки, которые часто совершают начинающие разработчики. Первая ошибка кроется в конструировании текста запроса. Использование конструкции выбора из двух несвязанных таблиц (например, ВТ и ВТ1) без явного указания условий связи в секции ПО приводит к так называемому перекрестному соединению (cross join / декартово произведение). В результате система пытается сопоставить каждую строку первой таблицы с каждой строкой второй. Это порождает лавинообразный рост дублирующихся строк.
Вторая распространенная ошибка — неверная область видимости переменной-накопителя в коде обхода выборки. Если объявить строковую переменную для сбора номеров (например, Текст3 = "") перед началом внешнего цикла по документам, а во внутреннем цикле выполнять конкатенацию, то значения предыдущих документов будут постоянно накапливаться и выводиться в последующих строках печатной формы.
Третья скрытая неэффективность — избыточная перезапись параметров области макета. Если заполнять параметры шапки документа (номера, даты, наименования) внутри вложенного цикла по табличной части, система будет перезаписывать одни и те же значения в оперативной памяти столько раз, сколько строк содержится в подчиненной таблице, хотя метод Вывести() вызывается один раз в самом конце. Разберем по шагам, как исправить эти недочеты.
Для правильного отбора данных нам необходимо использовать Левое соединение (Left Join) между таблицей документов и табличной частью их экземпляров. Чтобы система корректно сгруппировала данные, мы задействуем секцию ИТОГИ ПО. Это позволит нам построить иерархический обход результатов запроса.
Ниже представлен оптимизированный и исправленный код. Обратите внимание на то, что параметры шапки документа заполняются на уровне внешнего цикла, а во вложенном цикле происходит исключительно сборка текстовой строки.
Запрос6 = Новый Запрос;
Запрос6.Текст =
"ВЫБРАТЬ
| АрхивныйДокумент.Ссылка КАК Ссылка,
| АрхивныйДокумент.Дата КАК Дата,
| АрхивныйДокумент.ДецНомер КАК ДецНомер,
| АрхивныйДокумент.Наименование КАК Наименование,
| АрхивныйДокумент.КолвоПодл КАК КолвоПодл,
| АрхивныйДокумент.КолЛистов КАК КолЛистов,
| ЕСТЬNULL(АрхивныйДокументЭкземпляры.Номер, """") КАК НомерЭкземпляра
|ИЗ
| Документ.АрхивныйДокумент КАК АрхивныйДокумент
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.АрхивныйДокумент.Экземпляры КАК АрхивныйДокументЭкземпляры
| ПО АрхивныйДокументЭкземпляры.Ссылка = АрхивныйДокумент.Ссылка
|ГДЕ
| АрхивныйДокумент.ДецНомер В(&ТаблицаДецНомера)
|ИТОГИ ПО
| Ссылка";
Запрос6.УстановитьПараметр("ТаблицаДецНомера", ТаблицаДецНомера);
ВыборкаДокумента = Запрос6.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Ссылка");
к = 0;
Пока ВыборкаДокумента.Следующий() Цикл
к = к + 1;
// Заполняем общие параметры документа один раз (на уровне шапки)
ОбластьЗначенияТабл.Параметры.НомерПП = к;
ОбластьЗначенияТабл.Параметры.ДецНомер = ВыборкаДокумента.ДецНомер;
ОбластьЗначенияТабл.Параметры.ДатаРег = Формат(ВыборкаДокумента.Дата, "ДЛФ=Д");
ОбластьЗначенияТабл.Параметры.НаимДок = ВыборкаДокумента.Наименование;
ОбластьЗначенияТабл.Параметры.КолЭкз = ВыборкаДокумента.КолвоПодл;
ОбластьЗначенияТабл.Параметры.ЛистыЭкз = ВыборкаДокумента.КолЛистов;
ОбластьЗначенияТабл.Параметры.ВсегоЛистов = ЧислоПрописью((ВыборкаДокумента.КолЛистов * ВыборкаДокумента.КолвоПодл), "Л=ru_RU; НП=Ложь", ",,,,,,,,0");
// Инициализируем (очищаем) строку накопителя строго внутри внешнего цикла
ТекстНомера = "";
// Получаем детальные записи (номера экземпляров) для текущего документа
ВыборкаЭкземпляров = ВыборкаДокумента.Выбрать();
Пока ВыборкаЭкземпляров.Следующий() Цикл
Если ЗначениеЗаполнено(ВыборкаЭкземпляров.НомерЭкземпляра) Тогда
ТекстНомера = ТекстНомера + Строка(ВыборкаЭкземпляров.НомерЭкземпляра) + "; ";
КонецЕсли;
КонецЦикла;
// Удаляем лишний символ разделителя "; " на конце строки, если она не пустая
Если ЗначениеЗаполнено(ТекстНомера) Тогда
ТекстНомера = Лев(ТекстНомера, СтрДлина(ТекстНомера) - 2);
КонецЕсли;
ОбластьЗначенияТабл.Параметры.НомерЭкз = ТекстНомера;
// Выводим готовую и заполненную область макета
ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;
Для того чтобы сократить количество рутинного кода и сделать его чтение более приятным, воспользуемся системным методом ЗаполнитьЗначенияСвойств(). Чтобы этот прием сработал идеально, нам достаточно переименовать псевдонимы полей в запросе так, чтобы они полностью соответствовали именам параметров в макете табличного документа.
Например, если в макете параметр называется ДатаРег, то в запросе мы пишем: АрхивныйДокумент.Дата КАК ДатаРег. Посмотрим на пример лаконичного заполнения:
Пока ВыборкаДокумента.Следующий() Цикл
// Заполняем все совпадающие параметры автоматически одной строкой
ЗаполнитьЗначенияСвойств(ОбластьЗначенияТабл.Параметры, ВыборкаДокумента);
// Дозаполняем расчетные или специфические поля вручную
к = к + 1;
ОбластьЗначенияТабл.Параметры.НомерПП = к;
ОбластьЗначенияТабл.Параметры.ВсегоЛистов = ЧислоПрописью((ВыборкаДокумента.КолЛистов * ВыборкаДокумента.КолвоПодл), "Л=ru_RU; НП=Ложь", ",,,,,,,,0");
// Сборка номеров экземпляров
ТекстНомера = "";
ВыборкаЭкземпляров = ВыборкаДокумента.Выбрать();
Пока ВыборкаЭкземпляров.Следующий() Цикл
Если ЗначениеЗаполнено(ВыборкаЭкземпляров.НомерЭкземпляра) Тогда
ТекстНомера = ТекстНомера + Строка(ВыборкаЭкземпляров.НомерЭкземпляра) + "; ";
КонецЕсли;
КонецЦикла;
ОбластьЗначенияТабл.Параметры.НомерЭкз = СокрЛП(ТекстНомера);
ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;
Конкатенация строк через оператор "+" в циклах является неоптимальной операцией на больших объемах данных, так как в памяти каждый раз создается новый строковый объект. Начиная с версии платформы 8.3.6, в 1С доступен встроенный высокопроизводительный метод СтрСоединить(). Мы можем выгрузить подчиненные номера во временный массив или таблицу значений и объединить их одной строкой без написания вложенного цикла Пока.
Разберем пример реализации, где данные подчиненной таблицы предварительно помещаются в соответствие или обрабатываются методом поиска строк:
// Выполняем простой запрос по получению всех экземпляров отдельно в Таблицу Значений
ЗапросЭкземпляры = Новый Запрос;
ЗапросЭкземпляры.Текст =
"ВЫБРАТЬ
| Экземпляры.Ссылка КАК Ссылка,
| Экземпляры.Номер КАК Номер
|ИЗ
| Документ.АрхивныйДокумент.Экземпляры КАК Экземпляры
|ГДЕ
| Экземпляры.Ссылка.ДецНомер В(&ТаблицаДецНомера)";
ЗапросЭкземпляры.УстановитьПараметр("ТаблицаДецНомера", ТаблицаДецНомера);
ТЗЭкземпляры = ЗапросЭкземпляры.Выполнить().Выгрузить();
// Теперь при обходе основного списка документов собираем номера с помощью СтрСоединить
Пока ВыборкаДокумента.Следующий() Цикл
ЗаполнитьЗначенияСвойств(ОбластьЗначенияТабл.Параметры, ВыборкаДокумента);
// Получаем массив номеров для конкретного документа с помощью отбора в ТЗ
ПараметрыОтбора = Новый Структура("Ссылка", ВыборкаДокумента.Ссылка);
НайденныеСтроки = ТЗЭкземпляры.НайтиСтроки(ПараметрыОтбора);
МассивНомеров = Новый Массив;
Для Каждого СтрокаТЗ Из НайденныеСтроки Цикл
МассивНомеров.Добавить(Строка(СтрокаТЗ.Номер));
КонецЦикла;
// Объединяем массив в одну строку с разделителем за одно действие
ОбластьЗначенияТабл.Параметры.НомерЭкз = СтрСоединить(МассивНомеров, "; ");
ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;
Если ваша задача — вывести эти данные в отчет, то писать программный код обхода выборок и макетов вручную вообще не нужно. В Системе компоновки данных (СКД) есть мощная агрегатная функция СоединитьСтроки(), которая выполняет всю эту работу автоматически на уровне платформы.
Разберем по шагам, как настроить вывод значений табличной части в одну ячейку через СКД:
ЛЕВОЕ СОЕДИНЕНИЕ.Номер (номер экземпляра) и перенесите его в список ресурсов.СоединитьСтроки(ВычислитьВыражениеТаблицаЗначений("Номер"), "; ") или более простую формулу СоединитьСтроки(Номер, "; ").Номер автоматически выведется в итоговой строке группировки как одна строка с красивым разделителем.Использование СКД не только экономит время разработчика, но и гарантирует максимальное быстродействие отчета, так как механизмы агрегации строк оптимизированы на уровне платформенного C++ кода.