Как заполнить реквизит одного документа значением из связанного документа в 1С 8.3 без зависаний базы данных?

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

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

В этой статье мы подробно разберем, как правильно связать документы, проанализируем синтаксические и архитектурные ошибки начинающих разработчиков, рассмотрим причины зависания обработки на больших базах данных (объемом 40 ГБ и более) и напишем оптимальный, безопасный код с использованием транзакций и блокировок.

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

Часть 1. Разбор типичных ошибок начинающих разработчиков

В начале обсуждения на форуме автор попытался решить задачу «в лоб», используя метод выборки документов:


Пока ДокументыПоступления.Следующий Цикл 
// ...

Ошибка 1. Отсутствие скобок при вызове метода. Как справедливо заметили коллеги на форуме, Следующий — это метод (функция) объекта выборки. Обращение к нему без скобок приводит к ошибке Поле объекта не обнаружено (Следующий). Правильный синтаксис выглядит так: Пока ДокументыПоступления.Следующий() Цикл.

Ошибка 2. Отсутствие связи между документами. В первом варианте кода автор пытался получить все заказы поставщикам и все поступления товаров без какой-либо логической связи между ними. Если бы такой код выполнился, система записала бы во все документы ПТУ случайные проекты из первого попавшегося заказа.

Выясним, как связаны эти документы. Если документ ПТУ создается на основании заказа, связь может находиться либо в шапке (реквизит ЗаказПоставщику), либо непосредственно в строках табличной части Товары (реквизит ЗаказПоставщику или ДокументОснование). В нашем случае реквизит связи находится в табличной части товаров, при этом на один заказ оформляется одно поступление.

Часть 2. Почему простое решение из сообщения 16 «вешает» базу данных?

На форуме было предложено следующее решение для обхода документов и заполнения реквизита:


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

Этот код рабочий в теории, но на реальной базе объемом 40 ГБ он гарантированно приведет к зависанию системы, перегрузке процессора и дисковой подсистемы. Проанализируем критические архитектурные недостатки данного алгоритма:

  1. Дублирование строк и многократная перезапись одного документа. Запрос обращается к табличной части Товары. Если в одном документе ПТУ содержится 50 строк, то этот документ попадет в результат запроса 50 раз. В цикле программа выполнит ресурсоемкую операцию ПолучитьОбъект() и метод Записать() 50 раз для одного и того же документа! Это порождает колоссальную и бесполезную нагрузку на СУБД.
  2. Неоптимальное условие связи через «точки» в запросе. Конструкция ПоступлениеТоваровУслугТовары.Ссылка.Проект.Ссылка ЕСТЬ NULL заставляет СУБД выполнять неявные левые соединения (LEFT JOIN) таблиц. Кроме того, в 1С незаполненный реквизит ссылочного типа содержит пустую ссылку, а не значение NULL. Проверять незаполненное значение ссылки нужно сравнением с пустой ссылкой конкретного типа.
  3. Перепроведение документов при записи. Метод Док.Записать() без параметров по умолчанию выполняет запись в том режиме, в котором документ уже находится в базе. Если документ поступления был проведен, система начнет его заново проводить. На большой базе это запустит тяжелые алгоритмы контроля остатков, расчета себестоимости и формирования проводок, что окончательно заблокирует работу пользователей.

Часть 3. Оптимальное и безопасное решение

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

Рассмотрим по шагам оптимальный программный код:


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

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

Пока Выборка.Next() Цикл
	
	НачатьТранзакцию();
	Попытка
		// Устанавливаем управляемую блокировку на документ перед изменением
		Блокировка = Новый БлокировкаДанных;
		ЭлементБлокировки = Блокировка.Добавить("Документ.ПоступлениеТоваровУслуг");
		ЭлементБлокировки.УстановитьЗначение("Ссылка", Выборка.Ссылка);
		Блокировка.Заблокировать();
		
		// Получаем объект для изменения
		ДокОбъект = Выборка.Ссылка.ПолучитьОбъект();
		ДокОбъект.Проект = Выборка.Проект;
		
		// Записываем документ БЕЗ ПЕРЕПРОВЕДЕНИЯ (только запись)
		ДокОбъект.Записать(РежимЗаписиДокумента.Запись); 
		
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		// Регистрируем ошибку в журнале регистрации и продолжаем обработку остальных документов
		ЗаписьЖурналаРегистрации("ЗаполнениеПроектов", УровеньЖурналаРегистрации.Ошибка,,, 
			"Ошибка изменения документа " + Выборка.Ссылка + ": " + ОписаниеОшибки());
	КонецПопытки;
	
КонецЦикла;

Часть 4. Разбор ошибки компиляции «глОбщиеЗначения» в Управляемом приложении

В сообщениях 17–23 упоминается ошибка инициализации модуля: Переменная не определена (глОбщиеЗначения).

Выясним причину этой ошибки:
Данная проблема возникает в старых конфигурациях (например, Управление производственным предприятием 1.3 или Управление торговлей 10.3), когда базу запускают в режиме Управляемого приложения (или код обработки выполняется в контексте серверного вызова), в то время как конфигурация рассчитана на работу в Обычном приложении. Глобальная переменная глОбщиеЗначения объявлена в модуле обычного приложения, который не компилируется при запуске тонкого или управляемого клиента. Серверные общие модули при попытке обратиться к этой переменной вызывают ошибку компиляции всей системы.

Способы решения:

  1. Запустите базу данных 1С:Предприятие в режиме «Толстый клиент (обычное приложение)». В этом режиме инициализация глобальных переменных пройдет корректно, и ошибка исчезнет.
  2. Если требуется работа именно в управляемом режиме, необходимо доработать общий модуль РаботаСОбщимиПеременными в Конфигураторе, ограничив обращение к переменной директивами препроцессора:
    
    #Если Клиент ИЛИ ВнешнееСоединение Тогда
        Кэш = глОбщиеЗначения;
    #Иначе
        Кэш = ПараметрыСеанса.ОбщиеЗначения.Получить();
    #КонецЕсли
    

Часть 5. Альтернативное решение без изменения конфигурации и программирования

Если ваша конфигурация построена на базе Библиотеки стандартных подсистем (БСП), вам совсем не обязательно писать обработку в конфигураторе. Мы можем использовать стандартный и очень мощный инструмент — Групповое изменение реквизитов.

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

  1. Перейдем в раздел Администрирование — Обслуживание — Корректировка данных — Групповое изменение реквизитов.
  2. В поле «Изменять» выберем объект Поступление товаров и услуг.
  3. Добавим отбор по условию: Проект Не заполнен.
  4. Нажмем кнопку Дополнительные параметры и установим флаг Выполнять произвольный алгоритм (данная опция доступна только пользователям с полными правами Администратора).
  5. В появившемся поле для ввода кода напишем следующий компактный и безопасный алгоритм:
    
    Для Каждого СтрокаТЧ Из Объект.Товары Цикл
        Если ЗначениеЗаполнено(СтрокаТЧ.ЗаказПоставщику) И ЗначениеЗаполнено(СтрокаТЧ.ЗаказПоставщику.Проект) Тогда
            Объект.Проект = СтрокаТЧ.ЗаказПоставщику.Проект;
            Прервать; // Проект найден, выходим из цикла по строкам товаров
        КонецЕсли;
    КонецЦикла;
    
  6. Нажмем кнопку Изменить реквизиты.

Использование встроенной обработки БСП гарантирует, что система сама корректно обработает транзакции, запишет документы без перепроведения (если это настроено в параметрах) и зафиксирует все возможные ошибки блокировок в журнале регистрации.

← На главную