Как реализовать уникальность базовой единицы измерения в 1С и избежать критических ошибок

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

При проектировании структуры данных в среде 1С часто возникает задача обеспечения уникальности определенного свойства среди группы связанных объектов. Классический пример — справочник ЕдиницыИзмерения, подчиненный справочнику Номенклатура. У каждого товара может быть множество упаковок, коробок и штук, но только одна единица измерения должна являться базовой (иметь признак БазоваяЕдиница = Истина).

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

Анализ базового решения из практики программирования

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

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


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

КонецПроцедуры

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

Критическая ошибка №1: Отсутствие фильтра по Владельцу

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

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

  1. Для "Товара А" уже выбрана базовая единица "шт" (флаг БазоваяЕдиница равен Истина).
  2. Для "Товара Б" пользователь пытается установить базовую единицу "кг".
  3. Запрос в обработчике находит *любой* элемент в базе данных с флагом БазоваяЕдиница = Истина. Поскольку отбора по владельцу в запросе нет, система находит единицу "шт" "Товара А" и сбрасывает её флаг в Ложь.
  4. В итоге во всей информационной базе останется только один-единственный товар с установленной базовой единицей. У тысяч других товаров признак базовой единицы сотрется при первой же записи элементов!

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


Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЕдиницыИзмерения.Ссылка КАК Ссылка
    |ИЗ
    |	Справочник.ЕдиницыИзмерения КАК ЕдиницыИзмерения
    |ГДЕ
    |	ЕдиницыИзмерения.Владелец = &Владелец
    |	И ЕдиницыИзмерения.БазоваяЕдиница = ИСТИНА
    |	И ЕдиницыИзмерения.Ссылка <> &ТекущаяСсылка";

Запрос.УстановитьПараметр("Владелец", ЭтотОбъект.Владелец);
Запрос.УстановитьПараметр("ТекущаяСсылка", ЭтотОбъект.Ссылка);

Критическая ошибка №2: Игнорирование режима обмена данными

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

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

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

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


Если ЭтотОбъект.ОбменДанными.Загрузка Тогда
    Возврат;
EndЕсли;

Критическая ошибка №3: Опасность использования конструкции "ВЫБРАТЬ ПЕРВЫЕ 1"

На форумах часто советуют использовать ключевое слово ПЕРВЫЕ 1 в тексте запроса для оптимизации быстродействия. Разберем, почему в данном контексте это решение является ошибочным.

Представим ситуацию: из-за технического сбоя, некорректного импорта данных или работы внешней обработки у одного товара случайно оказалось две единицы измерения с флагом БазоваяЕдиница = Истина.

Если мы используем конструкцию ПЕРВЫЕ 1, запрос выберет только одну из этих некорректных единиц и сбросит её флаг в Ложь. Вторая ошибочная единица так и останется со значением ИСТИНА. Программа продолжит работать в неконсистентном состоянии.

Правильный подход — отказаться от ограничения ПЕРВЫЕ 1, выбрать запросом абсолютно все единицы владельца, у которых ошибочно установлен флаг базовой единицы, и в цикле сбросить его у всех найденных элементов. Это обеспечит автоматическое самоочищение и восстановление целостности базы данных при перезаписи.

Критическая ошибка №4: Транзакционные блокировки и взаимные блокировки (Deadlocks)

Когда мы вызываем метод Записать() для другого объекта внутри обработчика ПередЗаписью текущего объекта, обе записи выполняются в рамках одной неделимой транзакции базы данных.

В высоконагруженных многопользовательских системах это классический источник взаимных блокировок (Deadlocks). Если Пользователь №1 меняет Единицу А (что вызывает изменение Единицы Б), а Пользователь №2 в эту же миллисекунду меняет Единицу Б (что вызывает изменение Единицы А), СУБД заблокирует обе транзакции, и один из пользователей получит критическую ошибку сбоя блокировки.

Для предотвращения подобных ситуаций необходимо явно накладывать управляемую блокировку на данные, которые мы собираемся считывать и изменять в транзакции. Сделать это можно с помощью объекта БлокировкаДанных.

Идеальный безопасный код для обработчика "ПередЗаписью"

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


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

КонецПроцедуры

Лучший архитектурный подход (Best Practice в решениях фирмы 1С)

Хотя написанный нами выше код полностью безопасен и работоспособен, в профессиональной среде разработчиков 1С такое решение считается «плохим запахом кода» (code smell). Изменение связанных элементов внутри транзакции записи — это всегда дополнительная нагрузка на СУБД.

Рассмотрим, как данная задача спроектирована в типовых конфигурациях фирмы 1С (таких как 1С:ERP, 1С:Комплексная автоматизация, 1С:Управление торговлей или 1С:УНФ):

Проанализируем преимущества такого подхода:

  1. Абсолютная ссылочная целостность на уровне СУБД: У товара физически не может существовать две базовые единицы измерения, потому что реквизит в таблице номенклатуры ровно один.
  2. Максимальное быстродействие: Нет необходимости писать сложные транзакционные запросы, выполнять блокировки таблиц, перебирать выборки и программно дозаписывать другие объекты.
  3. Простота интерфейса: Пользователь выбирает базовую единицу непосредственно в карточке товара из выпадающего списка подчиненных единиц, что исключает любые логические ошибки ввода.

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

← На главную