В процессе разработки в системе 1С предприятия часто возникает необходимость отобразить в форме списка не только стандартные реквизиты шапки объекта (справочника или документа), но и данные из его табличной части. Подобная задача нередко ставит в тупик начинающих разработчиков, поскольку стандартный интерфейс платформы по умолчанию выводит только реквизиты верхнего уровня.
В рамках этой статьи мы подробно разберем, как решить эту задачу правильно, проанализируем типичные ошибки новичков, рассмотрим классическое решение через произвольный запрос динамического списка, а также изучим альтернативные и более производительные методы, отвечающие современным стандартам разработки фирмы 1С.
Для начала проанализируем попытку решения, которую предпринял автор темы в своем первом сообщении. Он попытался программно обойти все элементы справочника и извлечь данные из табличной части следующим образом:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Номенклатура.Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Ссылка = Справочники.Номенклатура.НайтиПоКоду(Выборка.Код);
Элемент = Ссылка.Товары.ВыгрузитьКолонку("Количество")[0];
// Попытка как-то внедрить данные в список...
КонецЦикла;
КонецПроцедуры
Этот код содержит сразу несколько критических ошибок, которые категорически недопустимы в реальной практике разработки:
Справочники.Номенклатура.НайтиПоКоду(Выборка.Код) выполняется на каждой итерации цикла. Это заставляет систему делать дополнительное обращение к базе данных для каждой строки. Вместо этого в самом запросе необходимо сразу выбирать поле Ссылка.Ссылка.Товары.ВыгрузитьКолонку() вызывает неявное получение всего объекта из базы данных в память сервера. Если в списке будет тысяча элементов, платформа выполнит тысячу тяжелых запросов к СУБД, что приведет к полному зависанию интерфейса.Выяснив, как делать не нужно, перейдем к правильным методам реализации этой задачи.
Самый простой и быстрый способ вывести данные табличной части в список — перевести основной реквизит формы списка (динамический список) в режим использования произвольного запроса.
Рассмотрим по шагам, как это сделать:
Номенклатура) в конфигураторе.Список (тип ДинамическийСписок) установим флаг Произвольный запрос.Нам необходимо соединить основную таблицу справочника с его табличной частью. Поскольку в исходной задаче оговорено, что нас интересует значение из первой строки табличной части, мы используем левое соединение с условием отбора по номеру строки.
Запишем текст запроса:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
Номенклатура.Код КАК Код,
Номенклатура.Наименование КАК Наименование,
ЕСТЬNULL(НоменклатураТовары.Количество, 0) КАК Количество,
Номенклатура.Склад КАК Склад
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура.Товары КАК НоменклатураТовары
ПО Номенклатура.Ссылка = НоменклатураТовары.Ссылка
И НоменклатураТовары.НомерСтроки = 1
Проанализируем структуру этого запроса подробнее:
Номенклатура, даже те, у которых табличная часть вообще не заполнена.ПО Номенклатура.Ссылка = НоменклатураТовары.Ссылка связывает строки табличной части с конкретным элементом справочника.И НоменклатураТовары.НомерСтроки = 1 отсекает все остальные строки табличной части, кроме первой. Без этого условия, если у какого-то элемента окажется несколько строк в табличной части, этот элемент в списке продублируется столько раз, сколько строк у него записано, что исказит представление данных.ЕСТЬNULL(..., 0) заменяет пустые значения (для элементов без табличной части) на красивый и понятный ноль.После сохранения запроса новое поле Количество появится в списке доступных полей динамического списка. Перетащим его мышкой в дерево элементов формы, чтобы колонка начала отображаться у пользователя.
Если в процессе развития системы окажется, что строк в табличной части может быть несколько, и вам нужно вывести, к примеру, общую сумму количества по всем строкам, предыдущий запрос выдаст неверный результат. В этом случае структуру запроса нужно изменить, применив группировку.
Рассмотрим пример запроса с вложенной выборкой и агрегацией данных:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
Номенклатура.Код КАК Код,
Номенклатура.Наименование КАК Наименование,
ЕСТЬNULL(ТоварыИтоги.ОбщееКоличество, 0) КАК Количество
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
Товары.Ссылка КАК Ссылка,
СУММА(Товары.Количество) КАК ОбщееКоличество
ИЗ
Справочник.Номенклатура.Товары КАК Товары
ГРУППИРОВАТЬ ПО
Товары.Ссылка
) КАК ТоварыИтоги
ПО Номенклатура.Ссылка = ТоварыИтоги.Ссылка
В этом варианте мы сначала во вложенном запросе группируем всю таблицу товаров по ссылкам и рассчитываем сумму количества для каждого элемента, а уже затем полученную свернутую таблицу соединяем с основным справочником. Это решает проблему дублирования строк в списке.
Описанные выше способы отлично работают на небольших базах данных. Однако при росте объема информации (когда количество элементов справочника и строк табличных частей превысит десятки тысяч) левые соединения начнут сильно замедлять работу интерфейса.
СУБД при каждом открытии или прокрутке списка будет вынуждена "на лету" соединять две огромные физические таблицы. Индексы по ссылке помогают, но при сложных отборах или сортировках производительность все равно будет деградировать. Рассмотрим два профессиональных архитектурных паттерна, которые позволяют избежать этой проблемы.
Начиная с версии платформы 1С 8.3.10, у динамического списка появилось замечательное событие — ПриПолученииДанныхНаСервере. Оно позволяет динамически подгружать дополнительные данные только для тех строк списка, которые в данный момент реально отображаются на экране пользователя (обычно это всего 30–40 строк).
Разберем, как реализовать этот подход по шагам:
КоличествоТЧ), но поле ПутьКДанным оставляем пустым.ПриПолученииДанныхНаСервере.В модуле формы пишем следующий программный код:
&НаСервере
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
// Шаг 1. Собираем ссылки только отображаемых на экране элементов
МассивСсылок = Новый Массив;
Для Каждого СтрокаСписок Из Строки Цикл
МассивСсылок.Добавить(СтрокаСписок.Ключ);
КонецЦикла;
Если МассивСсылок.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Шаг 2. Запрашиваем данные из табличной части только по этим ссылкам
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Ссылка КАК Ссылка,
| Товары.Количество КАК Количество
|ИЗ
| Справочник.Номенклатура.Товары КАК Товары
|ГДЕ
| Товары.Ссылка В (&МассивСсылок)
| И Товары.НомерСтроки = 1";
Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
ТаблицаРезультата = Запрос.Выполнить().Выгрузить();
// Шаг 3. Заполняем виртуальную колонку на форме полученными значениями
Для Каждого СтрокаРезультата Из ТаблицаРезультата Цикл
СтрокаОформления = Строки[СтрокаРезультата.Ссылка];
Если СтрокаОформления <> Неопределено Тогда
СтрокаОформления.Данные["КоличествоТЧ"] = СтрокаРезультата.Количество;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот метод работает молниеносно. Вместо сканирования всей базы данных система делает очень легкий запрос по конкретному массиву идентификаторов, который отрабатывает за доли миллисекунды.
Если вам требуется не только видеть количество в списке, но и иметь возможность быстро сортировать по нему список или строить отборы, использование динамических вычислений или соединений в реальном времени не подойдет. В таких случаях применяется классический паттерн проектирования БД — денормализация.
Если в табличной части действительно всегда хранится одно ключевое значение (или сумма), правильнее всего продублировать его в реквизитах шапки.
Выполним следующие шаги:
Номенклатура обычный реквизит КоличествоДляСписка (тип Число).
Процедура ПередЗаписью(Отказ)
// Защита от авто-регистрации обмена, если данные не поменялись
Если Товары.Количество() > 0 Тогда
НовоеКоличество = Товары[0].Количество;
Else
НовоеКоличество = 0;
КонецЕсли;
Если КоличествоДляСписка <> НовоеКоличество Тогда
КоличествоДляСписка = НовоеКоличество;
КонецЕсли;
КонецПроцедуры
При таком подходе данные рассчитываются ровно один раз — в момент нажатия кнопки "Записать". В форме списка вам останется лишь вывести обычное поле КоличествоДляСписка. Это решение обеспечивает максимальное быстродействие, легкую настройку любых отборов и не требует написания сложного программного кода в интерфейсных формах.