Как реализовать значение реквизита, которое должно быть Истина только у одного элемента справочника?

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

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

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

Вариант 1. Программный контроль (интуитивный, но опасный подход)

Первая мысль, которая приходит в голову разработчику (и автору темы на форуме) — это создать в справочнике КонтактныеЛица реквизит ЭтоКонтактноеЛицоБухгалтерии (Тип: Булево). Логика реализации выглядит так: "Перед записью элемента, если пользователь поставил галочку, мы должны найти все остальные элементы этого владельца, у которых эта галочка тоже стоит, и снять её".

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


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

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

Почему этот подход считается плохой практикой?

  1. Транзакционные блокировки и Deadlocks. Запись объекта в 1С всегда происходит в транзакции. Если два пользователя одновременно редактируют разных контактных лиц одного контрагента и пытаются поставить им этот флаг, возникнет конфликт блокировок. Один пытается записать своего и изменить чужого, второй делает то же самое в обратном порядке. Это прямой путь к взаимоблокировке (deadlock).
  2. Рекурсия событий. Вызов метода Записать() внутри события ПередЗаписью (пусть и для другого объекта) вызывает цепочку подписок на события и проверок, что существенно замедляет работу системы.
  3. Сложность индексов. База данных будет содержать тысячи записей со значением Ложь и единицы со значением Истина. Индексировать такое поле неэффективно (низкая селективность), что приведет к замедлению поисков при больших объемах данных.

Вариант 2. Архитектурный подход: Реквизит владельца (Рекомендуемый)

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

Суть решения:

  1. В справочнике КонтактныеЛица мы не создаем никаких булевых реквизитов.
  2. В справочнике Контрагенты (Владелец) мы создаем реквизит КонтактноеЛицоБухгалтерии.
  3. Тип этого реквизита: СправочникСсылка.КонтактныеЛица.
  4. В свойствах реквизита настраиваем "Связи параметров выбора": Отбор.Владелец (Контрагент) = Ссылка. Это нужно, чтобы в поле можно было выбрать только контакты текущего контрагента.

Преимущества данного метода:

Вариант 3. Использование Регистра Сведений

Иногда задача бывает сложнее. Например, нам нужно хранить не просто "Контакт для бухгалтерии", а целый набор ролей: "Для счетов", "Для договоров", "Для доставки", причем один человек может совмещать роли, или роли могут меняться со временем.

В этом случае добавление множества реквизитов в справочник Контрагенты (Контакт1, Контакт2, Контакт3...) загромождает объект. Здесь на помощь приходят регистры сведений.

Реализация через периодический Регистр Сведений:

  1. Создаем справочник или перечисление ВидыКонтактныхЛиц (значения: Бухгалтерия, Директор, Менеджер).
  2. Создаем Регистр сведений ОтветственныеЛицаКонтрагентов.
  3. Измерения:
    • Контрагент (СправочникСсылка.Контрагенты) — Ведущее измерение.
    • ВидРоли (Перечисление.ВидыКонтактныхЛиц).
  4. Ресурс:
    • КонтактноеЛицо (СправочникСсылка.КонтактныеЛица).

Такой подход позволяет хранить историю (если сделать регистр периодическим) и неограниченное количество ролей для одного контрагента. Уникальность обеспечивается структурой регистра: уникальным ключом записи является набор измерений (Контрагент + ВидРоли). То есть для одной роли у одного контрагента может быть только одна запись в конкретный момент времени.

Вариант 4. Справочник ролей (если ролей очень много)

Если специфика учета такова, что само контактное лицо может выступать в разных ипостасях, можно использовать подход, предложенный в обсуждении: создать справочник РолиКонтактныхЛиц, подчиненный справочнику КонтактныеЛица (или регистр сведений, где измерением будет КонтактноеЛицо). Однако это скорее решение задачи "Кем является этот человек", а не задачи "Кто главный бухгалтер у контрагента". Для быстрого поиска ответственного этот вариант менее удобен, так как требует запроса к подчиненным таблицам.

Рекомендации по пользовательскому интерфейсу (UI/UX)

Если мы выбрали Вариант 2 (реквизит у владельца), возникает вопрос: как пользователю удобно назначать основное лицо? Заходить в карточку контрагента каждый раз неудобно.

Решение реализуется на форме списка или форме элемента Контрагента, где отображается список контактных лиц:

  1. Размещаем на форме динамический список контактных лиц.
  2. Добавляем команду (кнопку) над списком: "Установить как контакт бухгалтерии".
  3. При нажатии кнопки выполняем код на сервере, который берет текущую строку списка и записывает её ссылку в реквизит владельца.
  4. Используем Условное оформление динамического списка: сравниваем ссылку в строке со значением реквизита владельца. Если они совпадают — выделяем строку жирным шрифтом и добавляем картинку (например, зеленую галочку).

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

Заключение

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

← На главную