Как заполнить или обновить табличную часть документа результатом запроса в 1С

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В процессе разработки в среде 1С:Предприятие одной из наиболее частых задач является заполнение или обновление данных табличной части документа на основе информации из регистров сведений, накоплений или других таблиц базы данных. Начинающие разработчики часто сталкиваются с ситуацией, когда написанный ими запрос отлично отрабатывает в «Консоли запросов», но при попытке перенести код в модуль документа табличная часть либо остается пустой, либо полностью очищается, теряя ранее введенные пользователем данные.

В рамках этой статьи мы подробно разберем, почему возникают такие ошибки, проанализируем физический смысл популярных методов работы с коллекциями строк и разработаем надежный, быстрый и безопасный алгоритм обновления колонок табличной части.

Анализ классических ошибок на примере исходного кода

Рассмотрим ситуацию, с которой столкнулся автор темы. Ему необходимо заполнить колонку МестоХраненияНаСкладе в табличной части Товары документа «Инвентаризация» данными из непериодического регистра сведений МестаХраненияНоменклатуры. Автор предложил следующий вариант кода:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    МестаХраненияНоменклатуры.Номенклатура,
    |    МестаХраненияНоменклатуры.МестоХранения
    |ИЗ
    |    РегистрСведений.МестаХраненияНоменклатуры КАК МестаХраненияНоменклатуры
    |ГДЕ
    |    МестаХраненияНоменклатуры.Номенклатура.Ссылка = &Ссылка";

Запрос.УстановитьПараметр("Ссылка", Ссылка);

РезультатЗапроса = Запрос.Выполнить();

ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    Товары.Загрузить(РезультатЗапроса.Выгрузить());
КонецЦикла;

Проанализируем этот фрагмент и выявим сразу несколько критических ошибок, которые не позволяют коду работать корректно.

Ошибка №1: Логическое несоответствие типов в параметрах запроса
В условии запроса мы видим: МестаХраненияНоменклатуры.Номенклатура.Ссылка = &Ссылка. Если этот код выполняется в контексте модуля документа или его формы, переменная Ссылка (или свойство ЭтотОбъект.Ссылка) содержит ссылку на сам документ «Инвентаризация». В то же время поле Номенклатура в регистре сведений хранит ссылку на элемент справочника «Номенклатура». Платформа пытается сравнить ссылку на документ со ссылкой на товар, что логически невозможно. В результате запрос всегда будет возвращать пустой результат.

Ошибка №2: Разыменование полей (обращение «через точку»)
Использование конструкции Номенклатура.Ссылка в условиях запроса является избыточным. Поле Номенклатура само по себе уже содержит ссылочный тип данных. Обращение к его свойству Ссылка заставляет СУБД выполнять неявное левое соединение (LEFT JOIN) с таблицей справочника «Номенклатура». Это снижает производительность системы, особенно на крупных базах данных. Правильно писать напрямую: МестаХраненияНоменклатуры.Номенклатура = &Номенклатура.

Ошибка №3: Неверное использование цикла обхода и метода Загрузить()
В цикле Пока ВыборкаДетальныеЗаписи.Следующий() разработчик пытается на каждой итерации вызвать метод Товары.Загрузить(). Метод Загрузить() предназначен для полной перезаписи всей табличной части данными из таблицы значений. Вызывать его внутри цикла по выборке — грубейшая ошибка, которая приводит к многократному затиранию и перезаписи данных одной и той же таблицей.

Ошибка №4: Полная потеря данных при использовании метода Загрузить()
Это наиболее коварная архитектурная проблема. Метод Загрузить() полностью очищает табличную часть перед заполнением. Если исходная табличная часть документа содержала заполненные колонки (например, Количество, Цена, Сумма, ЕдиницаИзмерения), а в результате запроса мы выбрали только два поля (Номенклатура и МестоХранения), то после вызова метода Загрузить() все остальные колонки документа будут безвозвратно стерты или сброшены в дефолтные значения! Нам необходимо научиться обновлять только целевую колонку, сохраняя остальные данные пользователя.

Способ 1: Полная перезапись табличной части (когда ТЧ изначально пуста)

Если перед нами стоит задача заполнить табличную часть с нуля (например, при первоначальном автоматическом заполнении документа по остаткам на складе), мы действительно можем использовать выгрузку результата запроса напрямую. Однако делать это нужно безопасно, обязательно проверяя результат запроса на наличие записей.

Рассмотрим правильный пример реализации такой перезаписи:


// Создаем запрос для получения данных
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    МестаХраненияНоменклатуры.Номенклатура КАК Номенклатура,
    |    МестаХраненияНоменклатуры.МестоХранения КАК МестоХраненияНаСкладе
    |ИЗ
    |    РегистрСведений.МестаХраненияНоменклатуры КАК МестаХраненияНоменклатуры";

РезультатЗапроса = Запрос.Выполнить();

// Проверяем, вернул ли запрос строки, чтобы не очистить ТЧ впустую
Если Не РезультатЗапроса.Пустой() Тогда
    // Имена колонок в запросе (псевдонимы) должны строго совпадать с реквизитами ТЧ
    Товары.Загрузить(РезультатЗапроса.Выгрузить());
Иначе
    ВызватьИсключение "Не найдены данные для заполнения табличной части!";
КонецЕсли;

Обратите внимание на использование псевдонима КАК МестоХраненияНаСкладе. Платформа 1С сопоставляет колонки таблицы значений и табличной части по их именам. Если имя колонки в результате запроса не совпадет с именем реквизита табличной части, данные в эту колонку загружены не будут.

Способ 2: Безопасное обновление конкретной колонки табличной части (Рекомендуемый способ)

Если в документе уже заполнены товары, количества и цены, и нам требуется обновить только сведения о местах хранения, использовать метод Загрузить() нельзя. В этом случае мы должны применить алгоритм точечного обновления строк. Рассмотрим этот процесс по шагам.

Шаг 1. Получение списка уникальных товаров из табличной части
Чтобы не считывать из регистра сведений информацию по всей номенклатуре предприятия (коих могут быть сотни тысяч позиций), мы передадим в запрос только те товары, которые физически присутствуют в документе. Для этого выгрузим колонку номенклатуры в массив и свернем его для удаления дубликатов.

Шаг 2. Выполнение оптимального запроса с отбором
Передадим полученный массив в качестве параметра запроса, используя оператор В (&МассивНоменклатуры).

Шаг 3. Индексация временной таблицы значений
Для обеспечения максимального быстродействия при поиске соответствий в цикле, мы добавим индекс по ключевому полю Номенклатура для полученной таблицы значений.

Шаг 4. Обход табличной части в цикле и обновление реквизита
Обходя строки табличной части документа, мы будем выполнять быстрый поиск по индексу во временной таблице значений и точечно заполнять только нужную нам колонку.

Посмотрим на практическую реализацию этого алгоритма:


// Шаг 1: Собираем массив уникальных ссылок номенклатуры из табличной части
МассивНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");

// Сворачиваем массив для удаления дублирующихся позиций
// Примечание: Если в конфигурации доступна БСП, можно использовать:
// ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивНоменклатуры);
// Ниже представлен универсальный способ сворачивания через соответствие:
СоответствиеГраниц = Новый Соответствие;
Для Каждого ЭлементМассива Из МассивНоменклатуры Цикл
    СоответствиеГраниц.Вставить(ЭлементМассива, Истина);
КонецЦикла;

УникальныйМассивНоменклатуры = Новый Массив;
Для Каждого КлючИЗначение Из СоответствиеГраниц Цикл
    УникальныйМассивНоменклатуры.Добавить(КлючИЗначение.Ключ);
КонецЦикла;

// Если табличная часть пуста, дальнейшие действия не имеют смысла
Если УникальныйМассивНоменклатуры.Количество() = 0 Тогда
    Возврат;
КонецЕсли;

// Шаг 2: Настраиваем и выполняем запрос с отбором по массиву
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    МестаХраненияНоменклатуры.Номенклатура КАК Номенклатура,
    |    МестаХраненияНоменклатуры.МестоХранения КАК МестоХраненияНаСкладе
    |ИЗ
    |    РегистрСведений.МестаХраненияНоменклатуры КАК МестаХраненияНоменклатуры
    |ГДЕ
    |    МестаХраненияНоменклатуры.Номенклатура В (&МассивНоменклатуры)";

Запрос.УстановитьПараметр("МассивНоменклатуры", УникальныйМассивНоменклатуры);

ТаблицаМестХранения = Запрос.Выполнить().Выгрузить();

// Шаг 3: Индексируем таблицу значений для оптимизации поиска в цикле
ТаблицаМестХранения.Индексы.Добавить("Номенклатура");

// Шаг 4: Обходим табличную часть документа и заполняем целевой реквизит
Для Каждого СтрокаТЧ Из Товары Цикл
    // Быстрый поиск строки по индексированному полю
    НайденнаяСтрока = ТаблицаМестХранения.Найти(СтрокаТЧ.Номенклатура, "Номенклатура");
    
    Если НайденнаяСтрока <> Неопределено Тогда
        СтрокаТЧ.МестоХраненияНаСкладе = НайденнаяСтрока.МестоХраненияНаСкладе;
    Иначе
        // Если место хранения не задано в регистре, можно очистить или задать дефолтное
        СтрокаТЧ.МестоХраненияНаСкладе = Справочники.МестаХранения.ПустаяСсылка();
    EndЕсли;
КонецЦикла;

Этот подход гарантирует высокую скорость выполнения алгоритма даже на документах, содержащих тысячи строк. Индексация таблицы значений превращает поиск из линейного обхода в мгновенную операцию с использованием хэш-таблицы.

Способ 3: Особенности работы в управляемых формах (Клиент-Сервер)

В современной платформе 1С:Предприятие 8.3 при работе с управляемыми формами необходимо строго разграничивать клиентский и серверный контексты выполнения кода. Наш программный код содержит создание объекта Новый Запрос и работу с базой данных, что категорически запрещено выполнять на стороне клиента (в процедурах с директивой &НаКлиенте).

Весь алгоритм работы с запросом должен быть вынесен на сторону сервера. Рассмотрим, как правильно организовать вызов серверного контекста.

Вариант А: Контекстный вызов на сервере (&НаСервере)
Этот вариант наиболее прост в реализации, так как мы имеем прямой доступ к реквизиту формы Объект и его табличным частям.


&НаКлиенте
Процедура ЗаполнитьМестаХраненияНаКлиенте(Команда)
    // Вызываем контекстную серверную процедуру
    ЗаполнитьМестаХраненияНаСервере();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьМестаХраненияНаСервере()
    
    // Получаем массив уникальной номенклатуры напрямую из реквизита формы "Объект"
    МассивНоменклатуры = Объект.Товары.Выгрузить().ВыгрузитьКолонку("Номенклатура");
    
    // Сворачиваем массив для удаления дубликатов
    СоответствиеНоменклатуры = Новый Соответствие;
    Для Каждого Элемент Из МассивНоменклатуры Цикл
        СоответствиеНоменклатуры.Вставить(Элемент, Истина);
    КонецЦикла;
    
    УникальныйМассив = Новый Массив;
    Для Каждого КлючИЗначение Из СоответствиеНоменклатуры Цикл
        УникальныйМассив.Добавить(КлючИЗначение.Ключ);
    КонецЦикла;
    
    Если УникальныйМассив.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    МестаХраненияНоменклатуры.Номенклатура КАК Номенклатура,
        |    МестаХраненияНоменклатуры.МестоХранения КАК МестоХраненияНаСкладе
        |ИЗ
        |    РегистрСведений.МестаХраненияНоменклатуры КАК МестаХраненияНоменклатуры
        |ГДЕ
        |    МестаХраненияНоменклатуры.Номенклатура В (&МассивНоменклатуры)";
        
    Запрос.УстановитьПараметр("МассивНоменклатуры", УникальныйМассив);
    
    ТаблицаМест = Запрос.Выполнить().Выгрузить();
    ТаблицаМест.Индексы.Добавить("Номенклатура");
    
    // Модифицируем данные формы напрямую через "Объект.Товары"
    Для Каждого СтрокаТЧ Из Объект.Товары Цикл
        НайденнаяСтрока = ТаблицаМест.Найти(СтрокаТЧ.Номенклатура, "Номенклатура");
        Если НайденнаяСтрока <> Неопределено Тогда
            СтрокаТЧ.МестоХраненияНаСкладе = НайденнаяСтрока.МестоХраненияНаСкладе;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Вариант Б: Оптимизированный неконтекстный вызов (&НаСервереБезКонтекста)
Для крупных высоконагруженных систем передача всего контекста формы (всех реквизитов, элементов управления и скрытых данных формы) между клиентом и сервером может приводить к задержкам. В таких случаях рекомендуется использовать неконтекстные вызовы. Мы передаем на сервер только минимально необходимый объем данных — массив номенклатуры, получаем на сервере соответствие мест хранения и возвращаем его обратно на клиент для локального заполнения формы.

Проанализируем этот высокопроизводительный шаблон проектирования:


&НаКлиенте
Процедура ЗаполнитьМестаХраненияОптимально(Команда)
    
    // 1. Собираем массив уникальных ссылок прямо на клиенте
    МассивНоменклатуры = Новый Массив;
    Для Каждого СтрокаТЧ Из Объект.Товары Цикл
        Если ЗначениеЗаполнено(СтрокаТЧ.Номенклатура) 
           И МассивНоменклатуры.Найти(СтрокаТЧ.Номенклатура) = Неопределено Тогда
            МассивНоменклатуры.Добавить(СтрокаТЧ.Номенклатура);
        КонецЕсли;
    КонецЦикла;
    
    Если МассивНоменклатуры.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // 2. Получаем данные с сервера БЕЗ передачи контекста всей формы
    ТаблицаРезультата = ПолучитьМестаХраненияНаСервереБезКонтекста(МассивНоменклатуры);
    
    // 3. Заполняем форму на клиенте
    Для Каждого СтрокаТЧ Из Объект.Товары Цикл
        ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаТЧ.Номенклатура);
        НайденныеСтроки = ТаблицаРезультата.НайтиСтроки(ПараметрыОтбора);
        Если НайденныеСтроки.Количество() > 0 Тогда
            СтрокаТЧ.МестоХраненияНаСкладе = НайденныеСтроки[0].МестоХраненияНаСкладе;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьМестаХраненияНаСервереБезКонтекста(МассивНоменклатуры)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    МестаХраненияНоменклатуры.Номенклатура КАК Номенклатура,
        |    МестаХраненияНоменклатуры.МестоХранения КАК МестоХраненияНаСкладе
        |ИЗ
        |    РегистрСведений.МестаХраненияНоменклатуры КАК МестаХраненияНоменклатуры
        |ГДЕ
        |    МестаХраненияНоменклатуры.Номенклатура В (&МассивНоменклатуры)";
        
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    
    Возврат Запрос.Выполнить().Выгрузить();
    
КонецФункции

Использование неконтекстного вызова минимизирует трафик между клиентом и сервером и значительно повышает отзывчивость интерфейса при работе пользователей через веб-клиент или при низкой скорости интернет-соединения.

Резюме

Для качественного заполнения табличных частей документов результатами запросов всегда придерживайтесь следующих правил:

  1. Никогда не сравнивайте разнородные типы данных в параметрах запроса (например, ссылки на документы со ссылками на номенклатуру).
  2. Избегайте разыменования полей ссылочного типа в условиях запроса через точку, используйте прямое сопоставление ссылок.
  3. Не применяйте метод Загрузить() для табличных частей, которые уже частично заполнены пользователем, чтобы не потерять другие важные реквизиты строк.
  4. Используйте индексацию временных таблиц значений перед выполнением поиска в циклах обхода табличных частей.
  5. Проектируйте вызовы с учетом клиент-серверной архитектуры платформы 1С:Предприятие 8.3, отдавая предпочтение оптимизированным неконтекстным методам.
← На главную