В процессе разработки конфигураций на платформе 1С:Предприятие мы часто сталкиваемся с необходимостью связать несколько сущностей. Классический пример — справочники Типы и Категории. Как быть, если одна категория может относиться к нескольким типам, а тип, в свою очередь, может содержать множество категорий (или вообще не иметь их)? При этом к связке типа и категории должен привязываться определенный коэффициент для расчета KPI, который может меняться со временем.
Проанализируем эту ситуацию подробно, разберем типичные архитектурные ошибки проектирования связей и найдем оптимальное решение, которое обеспечит высокую производительность, ссылочную целостность данных и простоту построения отчетов.
На первый взгляд, подчинение одного справочника другому кажется самым простым решением. Если сделать справочник Категории подчиненным справочнику Типы, мы автоматически получим удобный выбор на форме: при выборе типа в документе список категорий будет автоматически отфильтрован по владельцу. Однако у подчиненных справочников есть жесткое ограничение: у одного элемента подчиненного справочника может быть только один владелец.
Если одна и та же категория (например, «Эконом») должна относиться и к типу «Аренда», и к типу «Продажа», нам придется создать два абсолютно одинаковых элемента в справочнике категорий — по одному для каждого владельца. Рассмотрим, к каким проблемам это приводит:
Таким образом, использование подчиненного справочника при связи «многие ко многим» — это архитектурная ошибка, порождающая огромный технический долг.
Еще один частый вариант, который предлагают разработчики — добавить в справочник Типы табличную часть Категории, в которую будут заноситься элементы из независимого справочника категорий. На первый взгляд, это решает проблему дублирования. Но давайте проанализируем это решение глубже:
Единственным методологически верным решением в экосистеме 1С для связи «многие ко многим» является использование двух независимых справочников и связующего Регистра сведений. Рассмотрим по шагам, как спроектировать эту структуру:
Типы.Категории (без указания владельца).СвязиТиповИКатегорий.Настроим структуру нашего регистра сведений:
В пределах дня (это позволит нам хранить историю изменения коэффициентов во времени).Независимый.Тип (Тип: СправочникСсылка.Типы, свойство Индексировать = Истина).Категория (Тип: СправочникСсылка.Категории, свойство Индексировать = Истина).Коэффициент (Тип: Число, длина 10, точность 2).Благодаря такой структуре мы получаем гибкую систему, в которой один тип может быть связан со многими категориями, категории не дублируются, а коэффициенты легко меняются во времени.
Теперь решим проблему, которая беспокоит авторов подобных тем: как сделать так, чтобы на форме документа при выборе типа в поле выбора категории отображались только те категории, которые связаны с этим типом в регистре сведений?
Для этого мы откажемся от стандартной связи параметров выбора и настроим фильтрацию программно с помощью обработки событий формы. Рассмотрим два эффективных способа.
Этот способ прост в реализации и отлично подходит, когда выборка категорий невелика. Мы перехватываем открытие формы выбора и передаем туда готовый список разрешенных элементов.
Разберем программный код для модуля формы документа:
&НаКлиенте
Процедура КатегорияНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
// Проверяем, заполнил ли пользователь Тип
Если Не ЗначениеЗаполнено(Объект.Тип) Тогда
ПоказатьПредупреждение(, "Для выбора категории сначала необходимо заполнить поле ""Тип""!");
СтандартнаяОбработка = Ложь;
Возврат;
КонецЕсли;
// Блокируем стандартное открытие формы выбора
СтандартнаяОбработка = Ложь;
// Получаем список разрешенных категорий с сервера
СписокРазрешенныхКатегорий = ПолучитьСписокКатегорийДляТипа(Объект.Тип);
// Формируем параметры открытия формы выбора
ПараметрыВыбора = Новый Структура;
ПараметрыВыбора.Вставить("ЗакрыватьПриВыборе", Истина);
ПараметрыВыбора.Вставить("МножественныйВыбор", Ложь;
// Накладываем жесткий отбор по списку разрешенных ссылок
Отбор = Новый Структура("Ссылка", СписокРазрешенныхКатегорий);
ПараметрыВыбора.Вставить("Отбор", Отбор);
// Открываем форму выбора справочника Категории с нашим отбором
ОткрытьФорму("Справочник.Категории.ФормаВыбора", ПараметрыВыбора, Элемент);
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьСписокКатегорийДляТипа(ВыбранныйТип)
Список = Новый СписокЗначений;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Связи.Категория КАК Категория
|ИЗ
| РегистрСведений.СвязиТиповИКатегорий.СрезПоследних(&Период, Тип = &Тип) КАК Связи";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("Тип", ВыбранныйТип);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Список.Добавить(Выборка.Категория);
КонецЦикла;
Возврат Список;
КонецФункции
Если пользователи часто вводят наименование категории с клавиатуры (используют автоподбор), предыдущий способ не сработает в полной мере. Лучше переопределить получение данных выбора непосредственно в модуле менеджера справочника Категории.
Для этого в свойствах реквизита Категория в документе настроим Связи параметров выбора: укажем, что параметр Отбор.ТипСвязи очищается при изменении и зависит от реквизита Тип.
Затем в модуле менеджера справочника Категории напишем следующий код:
Процедура ОбработкаПолученияДанныхВыбора(ДанныеВыбора, Параметры, СтандартнаяОбработка)
// Проверяем, передан ли наш специфический параметр отбора
Если Параметры.Свойство("Отбор") И Параметры.Отбор.Свойство("ТипСвязи") Тогда
ВыбранныйТип = Параметры.Отбор.ТипСвязи;
Если ЗначениеЗаполнено(ВыбранныйТип) Тогда
// Отключаем стандартный поиск платформы
СтандартнаяОбработка = Ложь;
Запрос = Новый Запрос;
// Выбираем только те категории, которые связаны с типом и соответствуют строке автоподбора
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Связи.Категория КАК Ссылка,
| Связи.Категория.Наименование КАК Представление
|ИЗ
| РегистрСведений.СвязиТиповИКатегорий.СрезПоследних(&Период, Тип = &Тип) КАК Связи
|ГДЕ
| Связи.Категория.Наименование ПОДОБНО &СтрокаПоиска";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("Тип", ВыбранныйТип);
Запрос.УстановитьПараметр("СтрокаПоиска", "%" + Параметры.СтрокаПоиска + "%");
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
СписокВыбора = Новый СписокЗначений;
Для Каждого Строка Из ТаблицаРезультатов Цикл
СписокВыбора.Добавить(Строка.Ссылка, Строка.Представление);
КонецЦикла;
ДанныеВыбора = СписокВыбора;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
При использовании регистра сведений расчет KPI становится прозрачным и легко поддается оптимизации. Рассмотрим, как составить запрос в Системе компоновки данных (СКД), чтобы соединить данные документов продаж с актуальными коэффициентами на дату проведения документа.
Мы используем Левое соединение к виртуальной таблице СрезПоследних регистра сведений, передавая дату документа в качестве периода среза. Проанализируем пример такого запроса:
ВЫБРАТЬ
Продажи.Ссылка КАК ДокументПродажи,
Продажи.Дата КАК ДатаДокумента,
Продажи.Тип КАК Тип,
Продажи.Категория КАК Категория,
Продажи.Сумма КАК СуммаПродаж,
ЕСТЬNULL(Коэффициенты.Коэффициент, 1) КАК КоэффициентKPI,
Продажи.Сумма * ЕСТЬNULL(Коэффициенты.Коэффициент, 1) КАК СуммаСКоэффициентом
ИЗ
Документ.Продажи КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СвязиТиповИКатегорий.СрезПоследних(
&Период,
(Тип, Категория) В (
ВЫБРАТЬ ИЗХ.Тип, ИЗХ.Категория ИЗ Документ.Продажи КАК ИЗХ
)
) КАК Коэффициенты
ПО Продажи.Тип = Коэффициенты.Тип
И Продажи.Категория = Коэффициенты.Категория
В СКД для корректного среза последних на каждую дату документа рекомендуется использовать соединение по реальной таблице регистра сведений с условием на период, либо использовать возможности механизма виртуальных таблиц СКД (автоматическую связь наборов данных по периоду). Использование конструкции ЕСТЬNULL гарантирует, что если для типа не задана категория (или нет коэффициента в регистре), система подставит дефолтное значение (например, 1), и расчет не прервется из-за пустых значений (NULL).
Проанализировав все возможные архитектурные решения для связи «многие ко многим», мы пришли к однозначному выводу. Если вы столкнулись с необходимостью связать сущности, имеющие перекрестные зависимости: