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