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

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

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

1. Проверка на пустую ссылку в запросах 1С

Начнем с базового вопроса: как в тексте запроса определить, что ссылочное поле (например, ДокументОснование или Контрагент) не заполнено? Использование привычного метода Пустая(), который доступен для объектов ссылок во встроенном языке, непосредственно в запросе невозможно, так как СУБД не знает о существовании такого метода.

Для решения этой задачи в языке запросов 1С предусмотрены два основных подхода.

Способ А. Использование системной функции ЗНАЧЕНИЕ()

Если тип проверяемого реквизита однозначно определен (одиночный тип), мы можем использовать предопределенную функцию ЗНАЧЕНИЕ(), которая позволяет получить пустую ссылку конкретного типа прямо в теле запроса без передачи дополнительных параметров.

Рассмотрим пример запроса:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка,
    Заявка.Номер КАК Номер
ИЗ
    Документ.ЗаявкаПокупателя КАК Заявка
ГДЕ
    Заявка.ДоговорКонтрагента = ЗНАЧЕНИЕ(Справочник.ДоговорыКонтрагентов.ПустаяСсылка)

Этот способ максимально лаконичен, не требует подготовки параметров перед выполнением запроса и легко читается в коде.

Способ Б. Передача пустой ссылки через параметр запроса

В некоторых случаях тип данных реквизита может определяться динамически или разработчику удобнее передать пустое значение из внешнего кода. В этом случае мы объявляем параметр в запросе, а перед его запуском устанавливаем значение с помощью метода УстановитьПараметр().

Посмотрим на пример реализации:


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

Запрос.УстановитьПараметр("ПустойДоговор", Справочники.ДоговорыКонтрагентов.ПустаяСсылка());
РезультатЗапроса = Запрос.Выполнить();

2. Особенности проверки для составных типов данных

Ситуация существенно усложняется, если проверяемый реквизит имеет составной тип данных (например, реквизит Основание может принимать значения типов ДокументСсылка.ЗаказПокупателя, ДокументСсылка.СчетНаОплату или вообще быть не выбранным).

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

Важное предупреждение о производительности: ни в коем случае не используйте обращение через точку к реквизитам составного типа для проверки заполненности (например, Заявка.Основание.Ссылка ЕСТЬ NULL). При обращении через точку к составному полю платформа 1С генерирует сложнейший SQL-запрос с левыми соединениями (LEFT JOIN) ко всем физическим таблицам, которые входят в этот составной тип. Если в типе разрешено множество документов, запрос начнет критически тормозить даже на мощных серверах.

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


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка
ИЗ
    Документ.ЗаявкаПокупателя КАК Заявка
ГДЕ
    Заявка.Основание = НЕОПРЕДЕЛЕНО
    ИЛИ Заявка.Основание В (&МассивПустыхСсылок)

В параметр &МассивПустыхСсылок из встроенного языка мы передаем массив, содержащий пустые ссылки всех возможных типов, входящих в данное составное поле.

3. Как выявить «битые» ссылки в запросе

Иногда возникает специфическая проблема: в поле записана так называемая «битая» ссылка. Это происходит, когда объект был физически удален из базы данных (например, непосредственным удалением без контроля ссылочной целостности), но ссылка на его уникальный идентификатор (GUID) осталась в реквизитах других документов. На форме такое значение отображается текстом <Объект не найден>....

Проверка Заявка.Основание = &ПустаяСсылка для такого поля вернет Ложь, так как GUID внутри ссылки физически заполнен и не равен нулевому значению. Метод Пустая() во встроенном языке также вернет Ложь.

Выявить битые ссылки в запросе мы можем только путем левого соединения с таблицей этого объекта и проверкой ключевого поля на NULL. Проанализируем пример:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка,
    Заявка.Основание КАК БитаяСсылка
ИЗ
    Документ.ЗаявкаПокупателя КАК Заявка
    // Соединяемся с таблицей документа-основания
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказПокупателя КАК ЗаказПокупателя
        ПО Заявка.Основание = ЗаказПокупателя.Ссылка
ГДЕ
    // Если в таблице-источнике записи нет, а поле не пустое — ссылка битая
    ЗаказПокупателя.Ссылка ЕСТЬ NULL
    И НЕ Заявка.Основание = ЗНАЧЕНИЕ(Документ.ЗаказПокупателя.ПустаяСсылка)
    И НЕ Заявка.Основание = НЕОПРЕДЕЛЕНО

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

4. Эффективные методы копирования документов

Перейдем ко второму вопросу, затронутому в обсуждении: как красиво и правильно скопировать данные одного документа в другой при вводе на основании? Часто разработчики начинают вручную присваивать каждый реквизит нового документа, что приводит к написанию громоздкого кода, который тяжело поддерживать при изменении структуры метаданных.

Выясним, какие инструменты предоставляет платформа для автоматизации этого процесса.

Использование функции ЗаполнитьЗначенияСвойств()

Когда мы работаем в модуле создаваемого документа внутри стандартного обработчика события ОбработкаЗаполнения(ДанныеЗаполнения, ТекстЗаполнения, СтандартнаяОбработка), наилучшим решением для копирования шапки документа является функция ЗаполнитьЗначенияСвойств().

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

Рассмотрим правильный шаблон заполнения:


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

Метод Скопировать() для документов одного типа

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


// Пример создания копии документа из внешней обработки
ДокументОбъект = СсылкаНаИсходныйДокумент.ПолучитьОбъект();
КопияДокумента = ДокументОбъект.Скопировать();
// При необходимости очищаем или корректируем реквизиты
КопияДокумента.Дата = ТекущаяДата();
КопияДокумента.Записать(РежимЗаписиДокумента.Запись);

5. Копирование табличных частей документов

Перенос строк табличных частей также можно выполнить несколькими способами. Разберем их преимущества и недостатки.

Вариант 1. Построчный обход в цикле

Этот классический способ применяется, если структура табличных частей источника и приемника различается или требуется дополнительная фильтрация/обработка строк в процессе копирования.


Для Каждого СтрокаИсточника Из ДанныеЗаполнения.Товары Цикл
    НоваяСтрока = ЭтотОбъект.Товары.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
    // Здесь можно пересчитать цены или заполнить специфичные для нового документа колонки
КонецЦикла;

Вариант 2. Быстрая загрузка через Выгрузить/Загрузить

Если структуры табличных частей полностью идентичны (одинаковые имена и типы колонок), платформа позволяет перенести все строки одной строчкой кода, минуя медленные циклы встроенного языка:

ЭтотОбъект.Товары.Загрузить(ДанныеЗаполнения.Товары.Выгрузить());

Обратите внимание: данный метод работает чрезвычайно быстро, так как операции выполняются на уровне платформенного кода C++. Однако помните, что метод Загрузить() полностью заменяет текущее содержимое табличной части приемника и при этом не вызывает интерактивные обработчики событий строк (такие как ПриИзменении), если они были привязаны к элементам формы.

6. Динамическое копирование всех табличных частей по метаданным

Что делать, если у документа десятки табличных частей и мы хотим написать универсальный код заполнения, который не придется переписывать при добавлении новых табличных частей в конфигураторе? Мы можем прибегнуть к объектной модели метаданных.

Давайте оптимизируем код, предложенный в сообщении 19 форума, сделав его более лаконичным и быстродействующим за счет комбинации обхода метаданных и платформенного метода Загрузить():


// Получаем метаданные текущего документа
МетаданныеДокумента = ЭтотОбъект.Метаданные();

// Обходим все табличные части, зарегистрированные для данного типа документа
Для Каждого МетаТЧ Из МетаданныеДокумента.ТабличныеЧасти Цикл
    ИмяТЧ = МетаТЧ.Имя;
    
    // Проверяем, существует ли табличная часть с таким же именем у документа-основания
    Если ДанныеЗаполнения.Метаданные().ТабличныеЧасти.Найти(ИмяТЧ) <> Неопределено Тогда
        // Выполняем быстрое копирование данных без циклов по строкам
        ЭтотОбъект[ИмяТЧ].Загрузить(ДанныеЗаполнения[ИмяТЧ].Выгрузить());
    КонецЕсли;
КонецЦикла;

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

7. Анализ метаданных при сторнировании движений

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

При написании универсальных механизмов сторнирования важно понимать принципиальные различия между типами регистров в системе «1С:Предприятие 8»:

  1. Регистры сведений: Записи в этих регистрах отражают срезы состояний или статические данные. Они не имеют понятия "расход" или "приход". Сторнирование таких записей обычно заключается в их физическом удалении (очистке набора записей с отбором по регистратору) или внесении нейтрализующей записи, но не классическим "минусованием". Поэтому в алгоритмах автоматического сторно регистры сведений, как правило, обходят стороной.
  2. Регистры накопления оборотов: Сторнируются путем создания аналогичных записей, где все числовые ресурсы (суммы, количества) умножаются на -1.
  3. Регистры накопления остатков: Здесь важно учитывать вид движения (Приход или Расход). В зависимости от методологии учета, принятой в конфигурации, сторнирование выполняется либо изменением вида движения на противоположный при сохранении положительных знаков ресурсов, либо сохранением вида движения, но с умножением ресурсов на -1 (классическое "красное сторно").
  4. Регистры бухгалтерии: Сторнируются формированием идентичных проводок по дебету и кредиту со знаком "минус" в полях суммового и количественного учета.

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


// Получаем список регистров, по которым исходный документ делал движения
СписокРегистровОснования = ДанныеЗаполнения.Метаданные().Движения;

Для Каждого МетаРегистр Из СписокРегистровОснования Цикл
    
    // Исключаем регистры сведений из автоматического сторнирования показателей
    Если Метаданные.РегистрыСведений.Содержит(МетаРегистр) Тогда
        Продолжить;
    КонецЕсли;
    
    ИмяРегистра = МетаРегистр.Имя;
    
    // Получаем и считываем движения исходного документа
    ДвиженияОснования = ДанныеЗаполнения.Движения[ИмяРегистра];
    ДвиженияОснования.Прочитать();
    
    Если ДвиженияОснования.Количество() > 0 Тогда
        
        // Подготавливаем коллекцию движений текущего документа-сторнирования
        ДвиженияТекущие = ЭтотОбъект.Движения[ИмяРегистра];
        
        Для Каждого ЗаписьОснования Из ДвиженияОснования Цикл
            НоваяЗапись = ДвиженияТекущие.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяЗапись, ЗаписьОснования);
            
            // Устанавливаем текущий документ в качестве регистратора
            НоваяЗапись.Регистратор = ЭтотОбъект.Ссылка;
            НоваяЗапись.Период = ЭтотОбъект.Дата;
            
            // Логика инвертирования ресурсов для Регистров Накопления
            Если Метаданные.РегистрыНакопления.Содержит(МетаРегистр) Тогда
                // Проверяем тип регистра (Остатки или Обороты)
                Если МетаРегистр.ВидРегистра = Метаданные.СвойстваОбъектов.ВидРегистраНакопления.Остатки Тогда
                    // Для остатков инвертируем вид движения (Приход -> Расход, Расход -> Приход)
                    Если ЗаписьОснования.ВидДвижения = ВидДвиженияНакопления.Приход Тогда
                        НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Расход;
                    Иначе
                        НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Приход;
                    КонецЕсли;
                Иначе
                    // Для оборотных регистров умножаем ресурсы на -1
                    Для Каждого Ресурс Из МетаРегистр.Ресурсы Цикл
                        НоваяЗапись[Ресурс.Имя] = -ЗаписьОснования[Ресурс.Имя];
                    КонецЦикла;
                EndIf;
            КонецЕсли;
        КонецЦикла;
        
    КонецЕсли;
    
КонецЦикла;

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

← На главную