Оглавление
Теория графов и 1С
Данной статьей мы открываем цикл, посвященный работе с графовыми структурами. Почему мы решили поговорить об этом? Оказывается не все так просто, как может показаться на первый взгляд.
Мы попробуем ответить на вопросы, где платформа заботится о нас и строит иерархию за нас, поэтому мы не нуждаемся в программировании и понимании дискретной математики, где мы должны строить иерархию самостоятельно и насколько далеко распространяется наша самостоятельность.
Только ли флаг иерархии — это та самая иерархия или бывают иные случаи? Что такое иерархия с точки зрения устройства таблиц СУБД? Как платформа пытается заботиться о производительности, когда работает с иерархическими структурами?
Поговорим, конечно, и о приемах визуализации.
Первая статья цикла будет посвящена деревьям в 1С.
Дерево (теория графов) — это связный ациклический граф. Связность означает наличие маршрута между любой парой вершин, ацикличность — отсутствие циклов.
Иерархия 1С и как она устроена изнутри
Рассмотрим, как устроены различные механизмы, которые предоставляет платформа 1С для построения иерархии. Это может быть классическая иерархия в справочнике, связь с планом видов характеристик, или же вовсе иерархический вывод данных в отчете.
Справочники с иерархией элементов/групп и элементов
В самом простом случае, если мы хотим настроить порядок подчинения элементов между собой, в платформе 1С существует возможность указания иерархии для справочника. Вид иерархии может быть «Иерархия элементов» и «Иерархия групп и элементов».
Для иерархии элементов у справочника появляется стандартный реквизит «Родитель», который содержит ссылку на элемент верхнего уровня иерархии по отношению к текущему. Для элементов верхнего уровня родитель будет не заполнен. Таким образом и определяется подчиненность элементов.
Заглянем в описание таблицы на уровне системы управления базами данных. Чтобы найти имя соответствующей таблицы в базе, воспользуемся обработкой «Структура хранения метаданных»:
Далее, если мы используем в качестве СУБД MSSQL Server воспользуемся программой Management Studio для просмотра описания таблиц базы данных. Находим описание и видим добавленную колонку «ParentIDRef»:
Если говорить о СУБД PostgreSQL, то здесь можно либо воспользоваться программой pgAdmin с графическим интерфейсом:
Или же консольной утилитой psql, входящей в состав дистрибутива PostgreSQL. Для этого сначала необходимо подключиться к базе данных по ее имени при помощи опции -d:
Затем обратиться к описанию таблицы по имени при помощи специальной команды \d:
Когда выстраиваем иерархию, задача быстрой работы списков никуда не уходит. Давайте посмотрим на индексы по полю родителя:
- Индекс, содержащий колонку родителя, а также код, если его длина не равна 0:
- Индекс, содержащий колонку родителя, а также наименование, если его длина не равна 0. Аналогично, для MSSQL и PostgreSQL:
- Индекс, содержащий колонку родителя, а также значение реквизита, для которого включено индексирование. Например, в справочнике «Циклы планирования», используемом в данном примере, проиндексируем поле «Общее количество дней»:
После этого, в системе появится дополнительный индекс с полями «ParentIDRef», которое соответствует стандартному реквизиту «Родитель» и «_Fld34748», которое соответствует проиндексированному реквизиту «Общее количество дней»:
В случае иерархии групп и элементов дополнительно вводится разделение элементов при помощи реквизита «Это группа», причем родителем в таком случае может быть только группа.
В описание таблицы на уровне СУБД добавляется колонка «Folder»:
Также, с включенным в свойствах справочника флагом «Размещать группы сверху», который указывает на порядок расположения групп и элементов на одном уровне иерархии, в вышеописанные индексы будет добавлено поле «Folder» сразу после поля «ParentIDRef». Например для индекса по коду это будет выглядеть следующим образом:
Стоит отметить, что в MSSQL и PostgreSQL индексы строятся аналогично, пример описания аналогичного индекса в PgAdmin:
Для чего же система добавляет все перечисленные индексы? Для ускорения построения иерархии и доступа к свойствам родительских элементов справочника. В этом можно убедиться рассмотрев запросы, которые используются платформой при работе с иерархией, что будет сделано далее.
Также следует задуматься нужна ли вам иерархия групп, а не только элементов. Ведь за это удобство приходится расплачиваться дополнительным полем в таблице и в индексе.
Еще одно обобщение, которое можно сделать, изучая данный раздел, что за каждое действие, которое вы производите на уровне конфигурирования, вам приходится «расплачиваться» чем-то с точки зрения устройства СУБД. Поэтому хорошей рекомендацией будет изучать раздел знаний, называемый «экспертиза производительности», чтобы всегда иметь возможность глубокого понимания последствий ваших действий.
Справочники с владельцами
В том случае, когда необходимо подчинить элементы одного справочника элементам другого справочника, выстроив иерархию, в платформе 1С есть возможность указания владельцев справочника.
При такой настройке элементы подчиненного справочника всегда должны иметь владельца. Для его указания используется соответствующий стандартный реквизит:
В описание таблицы на уровне СУБД добавляется колонка «OwnerIDRRef». А для организации иерархии применяется ряд дополнительных индексов, т. к. конкретный элемент подчиненного справочника не используется без указания его владельца. По аналогии с иерархическими справочниками, это индексы содержащие владельца и поля наименование/код, если их длина не равна 0 или любой проиндексированный в конфигураторе реквизит:
Если же справочник одновременно является и подчиненным и иерархическим, то для всех индексов, содержащих поле «Родитель» в начало также добавляется и поле «Владелец»:
Из вышеописанного можно сделать вывод, что иерархические и подчиненные справочники содержат большое количество дополнительных индексов и это следует учитывать при проектировании системы. И прежде чем сделать подчиненный справочник иерархическим да еще с подчинением групп и элементов, подумайте, действительно ли вы в этом нуждаетесь.
Иерархия в запросах 1С
Рассмотрим, как система может использовать созданные индексы в иерархических справочниках при выполнении запросов в различных стандартных ситуациях.
В языке запросов 1С существует возможность вывести элементы справочника с итогами по иерархии. Пусть, у нас есть справочник со следующей иерархией групп и элементов:
Настроим сбор логов технологического журнала событий SDBL и DBMSSQL для того, чтобы отследить, какие запросы выполняются в системе:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
<log history="2" location="f:\chesdm\tjlogs">
<property name="all"/>
<event>
<eq property="name" value="SDBL"/>
<eq property="p:processname" value="chesdm_supo"/>
</event>
<event>
<eq property="name" value="DBMSSQL"/>
<eq property="p:processname" value="chesdm_supo"/>
</event>
</log>
</config>
Выполним запрос, который строит итоги по иерархии вышеописанного справочника без отбора:
ВЫБРАТЬ
УОП_ЦиклыПланирования.Ссылка КАК Ссылка
ИЗ
Справочник.УОП_ЦиклыПланирования КАК УОП_ЦиклыПланирования
ИТОГИ ПО
Ссылка ИЕРАРХИЯ
По событию DBMSSQL в логах технологического журнала видим, что на уровне СУБД система точно также, как и в языке запросов 1С, выбрала все записи из справочника (за исключением отбора по полю разделителя Fld929), но в выбранных полях к ссылке добавила еще и поле родителя для построения иерархии:
SELECT
T1._IDRRef,
T1._ParentIDRRef
FROM dbo._Reference34768 T1
WHERE (T1._Fld929 = ?)
Попробуем воспользоваться еще одной стандартной функциональностью — запросом с отбором по иерархии:
ВЫБРАТЬ
УОП_ЦиклыПланирования.Ссылка КАК Ссылка
ИЗ
Справочник.УОП_ЦиклыПланирования КАК УОП_ЦиклыПланирования
ГДЕ
УОП_ЦиклыПланирования.Ссылка В ИЕРАРХИИ(&Ссылка)
Здесь уже имеем гораздо большее количество запросов, т. к. система пытается вычислить массив ссылок, по которым необходимо выполнить отбор в таблице. Сначала создается временная таблица, содержащая колонку со ссылкой, а также кластерный индекс к ней:
CREATE TABLE #tt2 (_REFFIELDRRef BINARY(16))
CREATE UNIQUE CLUSTERED INDEX idx2 ON #tt2 (_REFFIELDRRef)
Затем в эту временную таблицу добавляются все потомки переданного в качестве параметра элемента справочника (первая часть запроса), а также сам переданный элемент справочника (вторая часть запроса):
INSERT INTO #tt2 WITH(TABLOCK) (_REFFIELDRRef) SELECT
T1._IDRRef
FROM
dbo._Reference34678 T1
WHERE
((T1._Fld929 = ?)) AND ((T1._ParentIDRRef IN (?)))
UNION SELECT
T2._IDRRef
FROM
dbo._Reference34678 T2
WHERE
((T2._Fld929 = ?)) AND ((T2._IDRRef IN (?)))
p_0: 0N
p_1: 0x810A002590A7FB1511ECA50B450BD2CE
p_2: 0N
p_3: 0x810A002590A7FB1511ECA50B450BD2CE
Далее к полученной на предыдущем шаге временной таблице добавляются элементы, родителями которых являются элементы, уже присутствующие во временной таблице:
INSERT INTO #tt2 WITH(TABLOCK) (_REFFIELDRRef) SELECT
T1._IDRRef
FROM
dbo._Reference34678 T1
INNER JOIN #tt2 T2 WITH(NOLOCK)
ON T1._ParentIDRRef = T2._REFFIELDRRef
LEFT OUTER JOIN #tt2 T3 WITH(NOLOCK)
ON T1._IDRRef = T3._REFFIELDRRef
WHERE
((T1._Fld929 = ?)) AND (T3._REFFIELDRRef IS NULL)
p_0: 0N
Такой запрос будет выполнен столько раз, сколько подчиненных уровней иерархии еще осталось присоединить, после чего во временной таблице #tt2 получим ссылки на все подчиненные группы и элементы для того элемента, который был передан в качестве параметра в исходном запросе 1С.
Далее система выбирает все ссылки из временной таблицы и следующим запросом уже обращается к таблице справочника с отбором по найденным ссылкам:
SELECT
T1._REFFIELDRRef
FROM #tt2 T1 WITH(NOLOCK)
SELECT
T1._IDRRef
FROM dbo._Reference34678 T1
WHERE ((T1._Fld929 = ?)) AND (((T1._IDRRef IN (?, ?, ?, ?, ?, ?))))
p_0: 0N
p_1: 0x810A002590A7FB1511ECA50B450BD2CE
p_2: 0x810A002590A7FB1511ECA50B4F5183F8
p_3: 0x810A002590A7FB1511ECA52A0F5F731D
p_4: 0x810A002590A7FB1511ECAA75D1DEAB1C
p_5: 0x810A002590A7FB1511ECAA75D93F4697
p_6: 0x810A002590A7FB1511ECAA75DFAF00AF
Группировка полей в схеме компоновки данных
Следующий случай построения иерархии платформой — это группировка полей в схеме компоновки данных. К примеру, если в качестве источника данных укажем вышеописанный иерархический справочник, а в настройках полей группировок укажем необходимость построения иерархии:
То система на уровне СУБД выполнит дополнительный запрос, в котором добавится поле родителя:
SELECT
T1._IDRRef,
T1._IDRRef,
T1._Description,
T1._ParentIDRRef,
T1._Description
FROM dbo._Reference34678 T1
WHERE
((T1._Fld929 = ?)) AND ((T1._IDRRef IN (?, ?, ?, ?, ?, ?, ?, ?, ?)))
Отображение динамического списка в виде дерева
Еще один случай построения иерархии — это непосредственное отображение динамического списка в форме списка справочника при условии, что режим отображения такого списка установлен в виде дерева так, как это было показано на рисунке выше.
Настройка такого динамического списка будет выглядеть следующим образом:
Сначала выбираются элементы верхнего уровня (у которых ссылка на родителя пустая), причем не все, а только первые N (в нашем случае первые 45), причем количество выбираемых строк вычисляется платформой автоматически:
SELECT TOP 45
T1._IDRRef,
T1._Marked,
T1._ParentIDRRef,
CASE WHEN (T1._Folder = 0x00) THEN 0x01 ELSE 0x00 END,
T1._Code,
T1._Description,
CASE WHEN T1._PredefinedID > 0x00000000000000000000000000000000 THEN 0x01 ELSE 0x00 END
FROM dbo._Reference34678 T1
WHERE
((T1._Fld929 = ?)) AND ((T1._ParentIDRRef = 0x00000000000000000000000000000000))
ORDER BY
CASE WHEN ((T1._Folder = 0x00)) THEN 0x01 ELSE 0x00 END DESC, (T1._Description), (T1._IDRRef)
Затем, при условии, что мы указали необходимость разворачивания дерева динамического списка в его свойствах, для каждого элемента верхнего уровня, который является группой, выбираются его потомки второго уровня (причем отдельным запросом для каждого из элементов верхнего уровня):
SELECT TOP 45
T1._IDRRef,
T1._Marked,
T1._ParentIDRRef,
CASE WHEN (T1._Folder = 0x00) THEN 0x01 ELSE 0x00 END,
T1._Code,
T1._Description,
CASE WHEN T1._PredefinedID > 0x00000000000000000000000000000000 THEN 0x01 ELSE 0x00 END
FROM dbo._Reference34678 T1
WHERE ((T1._Fld929 = ?)) AND ((T1._ParentIDRRef = ?))
ORDER BY CASE WHEN ((T1._Folder = 0x00)) THEN 0x01 ELSE 0x00 END DESC, (T1._Description), (T1._IDRRef)
Далее, к предыдущему запросу добавляется парный запрос, который строит иерархию множественным соединением таблицы справочника с самой собой по полю родителя из первой таблицы равному полю ссылки во второй (получение потомков):
SELECT
T1._IDRRef,
T1._ParentIDRRef,
T2._IDRRef,
T2._ParentIDRRef,
T3._IDRRef,
T3._ParentIDRRef,
T4._IDRRef,
T4._ParentIDRRef,
T5._IDRRef,
T5._ParentIDRRef,
T6._IDRRef,
T6._ParentIDRRef,
T7._IDRRef,
T7._ParentIDRRef,
T8._IDRRef,
T8._ParentIDRRef,
T9._IDRRef,
T9._ParentIDRRef,
T10._IDRRef
FROM dbo._Reference34678 T1
LEFT OUTER JOIN dbo._Reference34678 T2
ON (T1._ParentIDRRef = T2._IDRRef) AND (T2._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T3
ON (T2._ParentIDRRef = T3._IDRRef) AND (T3._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T4
ON (T3._ParentIDRRef = T4._IDRRef) AND (T4._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T5
ON (T4._ParentIDRRef = T5._IDRRef) AND (T5._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T6
ON (T5._ParentIDRRef = T6._IDRRef) AND (T6._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T7
ON (T6._ParentIDRRef = T7._IDRRef) AND (T7._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T8
ON (T7._ParentIDRRef = T8._IDRRef) AND (T8._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T9
ON (T8._ParentIDRRef = T9._IDRRef) AND (T9._Fld929 = ?)
LEFT OUTER JOIN dbo._Reference34678 T10
ON (T9._ParentIDRRef = T10._IDRRef) AND (T10._Fld929 = ?)
WHERE ((T1._Fld929 = ?)) AND ((T1._IDRRef IN (?)))
Вышеописанные два запроса повторяются для каждого последующего уровня иерархии для всех элементов, которые являются группами. При этом, если начать скроллинг списка, то данные будут подгружаться порциями по N штук, как и при его открытии, повторяя все вышеописанные запросы.
Можем сделать вывод, что развертка динамического списка в виде дерева требует большого количества запросов для построения иерархии этого самого дерева. Поэтому для справочников, содержащих большое количество данных рекомендуется использовать режим отображения «Иерархический список» или по крайней мере не устанавливать свойство «Начальное отображение дерева» в значение «Раскрывать все уровни».
Прикладные задачи использования иерархии в 1С
Как вы могли заметить технологическая платформа «1С:Предприятие 8» предоставляет различные возможности построения иерархии. Однако всегда ли их хватает и всегда ли результат ожидаемый?
Для того, чтобы ответить на данный вопрос, давайте рассмотрим пару кейсов, которые произошли у нас на проектах за последнее время.
Иерархии нет, а если найду?
Одной из особенностей платформы 1С:Предприятие 8 является отключение отображения иерархии при отборах, кроме необходимых ей для построения самой иерархии.
Можно предположить, что так было сделано по причине неясности ожиданий пользователя. Например, нужен ли ему просто быстрый поиск значения или же он ожидает увидеть и иерархию недоступную для выбора. Что делать если задача отобразить эту иерархию все-таки есть или нужно сформировать общую иерархию двух таблиц?
Возможно, эта задача покажется сложной и весьма не очевидной. Однако, для таких задач можно использовать механизм схемы компоновки данных, а точнее его возможности связи наборов данных.
Давайте рассмотрим использование этого механизма на примере. В библиотеке стандартных подсистем существует механизм дополнительных реквизитов и сведений. Данный механизм позволяет расширять структуру объектов метаданных из режима «1С:Предприятие». Благодаря этому типовые решения могут подходить самым разным клиентам, не углубляясь в тонкости отрасли.
Подготовим новый дополнительный реквизит «Инвентарь» для справочника «Сотрудники» и заполним его у текущих сотрудников.
Далее перед нами появляется задача отобразить все значения данного реквизита, а также их владельцев. Для применения механизма схемы компоновки данных необходимо подготовить таблицу с матрицей связей между элементами. В данной ситуации она будет состоять из объединения таблиц инвентаря и сотрудников. В таблице инвентаря матрица связи является хранимой и описывается реквизитами «Ссылка» и «Родитель», где «Ссылка» является источником, а «Родитель» — приемником.
При этом у таблицы сотрудников источником будет выступать аналогично реквизит «Ссылка», а вот приемником — значение дополнительного реквизита.
ВЫБРАТЬ
СправочникЗначений.Ссылка КАК Источник,
СправочникЗначений.Родитель КАК Приемник
ИЗ
Справочник.ЗначенияСвойствОбъектов КАК СправочникЗначений
ГДЕ
СправочникЗначений.Владелец = &ДополнительныйРеквизит
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ТЧДополнительныеРеквизиты.Ссылка,
ТЧДополнительныеРеквизиты.Значение
ИЗ
Справочник.Сотрудники.ДополнительныеРеквизиты КАК ТЧДополнительныеРеквизиты
ГДЕ
ТЧДополнительныеРеквизиты.Свойство = &ДополнительныйРеквизит
Затем подготовим макет схемы компоновки данных, где создадим набор данных с двумя полями «Источник» и «Приемник». В данном примере на входе набора данных мы используем внешний набор данных, что позволяет нам максимально гибко подготовить исходные данные для формирования иерархии. Однако это не является ограничением использования данного подхода и при желании можно использовать и другие источники наборов данных.
Далее перейдем на вкладку связи наборов данных и добавим правило связи, где укажем поля источника и приемника, а также начальное значение связи.
На вкладке настройки добавим пустую группировку и заполним выбранные поля соответствующими значениями.
Подготовив данный макет останется последовательно инициализировать все элементы компоновки данных и заполнить реквизит с типом дерево на форме.
// Получение макета СКД.
СхемаКомпоновкиДанных = Справочники.ЗначенияСвойствОбъектов.ПолучитьМакет("УОП_ИерархияЗначений");
// Инициализация компоновщиков.
КомпоновщикНастроек = Новый КомпоновщикНастроекКомпоновкиДанных;
КомпоновщикНастроек.Инициализировать(Новый ИсточникДоступныхНастроекКомпоновкиДанных(СхемаКомпоновкиДанных));
КомпоновщикНастроек.ЗагрузитьНастройки(СхемаКомпоновкиДанных.НастройкиПоУмолчанию);
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных,
КомпоновщикНастроек.Настройки,,,
Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
ВнешнийНаборДанных = Новый Структура("ТаблицаЭлементов", ТаблицаСвязей);
ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки, ВнешнийНаборДанных);
// Подготовка и заполнение дерева иерархии.
ДеревоИерархии = Новый ДеревоЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(ДеревоИерархии);
ПроцессорВывода.Вывести(ПроцессорКомпон
В результате можно получить вот такое дерево элементов, где к основной иерархии элементов инвентаря был добавлен дополнительный уровень с сотрудниками.
Тут важно понимать, что в данном случае мы занимаемся так называемой декларативной частью разработки. То есть описываем, какие именно поля связываются, чтобы образовать матрицу связности, а ее построением и рекурсивным обходом занимается «черный ящик» СКД, за что ему большое спасибо!
Таким образом, для решения задачи формирования иерархии разнотипных сущностей и формировании сложных деревьев можно использовать механизм схемы компоновки данных платформы «1С:Предприятие 8». Данный подход является универсальным и подходит для решения большого количества задач связанных с формированием иерархии.
Иерархия в отчете приготовления блюд
В предыдущем примере мы рассмотрели базовый вариант, когда иерархия одной из таблиц уже являлась хранимой. Однако, на практике так бывает далеко не всегда. Следующая история произошла при разработке «Отчета по плановой себестоимости» конфигурации «1С:Предприятие 8. Общепит, редакция 3.0».
Данный отчет отображает плановую себестоимость приготовления блюд по ингредиентам с учетом разложения их полуфабрикатов. При этом для оценки каждого блюда или полуфабриката используются только актуальные рецептуры на конкретную дату. Соответственно состав входящих полуфабрикатов от времени может меняться, т. е. не является хранимым.
Пример построенного отчета.
Если мы посмотрим на пример сформированного отчета, то можно увидеть все уровни приготовления блюда «Пюрешка с котлеткой и огурчиком» согласно его рецептуры.
Что же из себя представляет эта рецептура? Рецептура приготовления является документом, в котором в шапке указывается блюдо и количество, которое получается на выходе, а в табличной части указан состав ингредиентов, необходимый для его приготовления. При этом ингредиентом может выступать и полуфабрикат, который по сути является таким же блюдом со своей рецептурой приготовления.
Получается, что на уровне объекта метаданных связь между блюдами и ингредиентами определяется лишь объектно, а не двумя реквизитами, но иерархию построить нужно. Для этого мы используем тот же самый принцип со схемой компоновки данных, а именно, сначала готовим таблицу производства, где описывается связь между блюдами и ингредиентами, затем она передается схеме компоновки данных и результат выводится в отчет.
Построение таблицы производства осуществляется в несколько этапов:
- Получение составов рецептур приготовляемых блюд и их полуфабрикатов с учетом актуальных рецептур.
- Получение стоимости приготовления каждого блюда/фабриката по ценам ингредиентов.
- Получение итоговой общей таблицы.
Для сохранения связей между блюдами и ингредиентами при построении таблицы производства заполняются служебные колонки «Ключ источника» и «Ключ приемника». Ключ источника идентифицирует конкретную строку приготовления. Он может быть представлен уникальным идентификатором или же просто числом, которое постепенно увеличивается. Ключом приемника является ключ источника блюда/полуфабриката для приготовления которого и нужен этот ингредиент.
В результате получим следующую таблицу производства.
Блюдо | Ингредиент | Ключ источника | Ключ приемника | Уровень | Количество блюда | Количество ингредиента | Сумма |
---|---|---|---|---|---|---|---|
Пюрешка с котлеткой и огурчиком | 1 | 0 | 0 | 1 | 1 | 882,5 | |
Пюрешка с котлеткой и огурчиком | Пюрешка с котлеткой | 2 | 1 | 1 | 1 | 1 | 882,5 |
Пюрешка с котлеткой и огурчиком | Огурчик свежий | 3 | 1 | 1 | 1 | 0,5 | 0 |
Пюрешка с котлеткой | Пюрешка | 4 | 2 | 2 | 1 | 1 | 240 |
Пюрешка с котлеткой | Котлетка | 5 | 2 | 2 | 1 | 0,5 | 642,5 |
Пюрешка | Картошка | 6 | 4 | 3 | 1 | 4 | 200 |
Котлетка | Мука | 7 | 5 | 3 | 0,5 | 0,25 | 2,5 |
Пюрешка | Молоко | 8 | 4 | 3 | 1 | 1 | 40 |
Котлетка | Фарш | 9 | 5 | 3 | 0,5 | 2,5 | 625 |
Котлетка | Хлеб | 10 | 5 | 3 | 0,5 | 0,5 | 15 |
Полученную таблицу помещаем в качестве набора данных.
На вкладке связи наборов указываем поле «Ключ источника» в качестве выражения источника и «Ключ приемника» в качестве выражения приемника.
Для группировки колонок «Блюдо» и «Ингредиент», а также «Количество блюда» и «Количество ингредиента» подготовим соответствующие вычисляемые поля «Номенклатура» и «Количество», которые будем заполнять в зависимости от типа строки.
На вкладке настройки укажем выбранные поля.
После компоновки данных получается отчет с иерархией по каждому уровню производства.
По итогу, вследствие использования схемы компоновки данных для формирования иерархии, результат можно представить как в виде дерева элементов, так и в табличном документе.
Если нам нужен иерархический вид, а его нет
Давайте представим себе ситуацию, что к вам приходит клиент и говорит, что у вас справочник не иерархический, а нам нужен именно иерархический и все доводы ни к чему не приходят.
Схожая ситуация у нас произошла при разработке «1С:УНФ 8. Управление предприятием общепита», в котором справочник «Дисконтные карты» являлся линейным, но с подчинением справочнику «Виды карт». Однако подчинение другому справочнику не решало все возможные ситуации. Потому мы решили это исправить.
Для этого мы сначала включили свойство справочника иерархический и выбрали вид иерархии.
Однако если запустить такой справочник и попробовать создать группу, то режим отображения останется список и переключение на другие варианты будет недоступно. Даже если вы заполните у элементов реквизит «Родитель».
Данное поведение связано с тем, что данный справочник является подчиненным и для построения иерархии платформе требуется отбор по полю владельца.
Стоит отметить, что если попробовать просто выполнить группировку по полю владельца, то платформа все равно не отобразит иерархию.
Диаграмма структуры организации за 5 минут
К вам приходит начальник и просит показать структуру организации в графическом виде, а до совещания осталось пару минут — не беда.
Для построения диаграмм на платформе «1С:Предприятие 8» можно воспользоваться табличным документом, графической схемой или полем HTML. Однако, при использовании табличного документа или графической схемы разработчику потребуется генерировать объекты, связи между ними и смещать относительно друг от друга.
Пока речь идет о прямой линейной структуре это звучит не так страшно. Хотя и за короткое время решить не получится. Однако если структура дерева начинает ветвится или имеет петли? В такие моменты приходит на помощь поле HTML. В частности для реализации данной задачи можно воспользоваться библиотекой динамической визуализации Vis.js. Она проста в использовании и предоставляет возможность обработки большого объема данных.
Для начала подготовим HTML-разметку, которая будет помещаться в реквизит поля. В разделе <head> разместим подключение библиотеки Vis.js, а в <body> разместим область куда будет выведено дерево и место для скрипта инициализации дерева.
<html>
<head>
<!– Подключим библиотеку Vis.js-->
<script type="text/javascript" src="https://unpkg.com/vis-network/dist/vis-network.min.js"></script>
</head>
<body>
<div id="canvasConvainer" style="width: 100%; height: 100%;">
<!-- Здесь будет располагаться дерево после инициализации -->
</div>
<script type="text/javascript">
// Здесь будет располагаться скрипт инициализации дерева.
</script>
</body>
</html>
Для инициализации дерева с помощью библиотеки необходимо подготовить список узлов и таблицу ребер. В качестве примера представим следующую структуру организации:
- Офис Сладкоежка (Код 001):
- Офис Плюшки (Код 002);
- Офис Пампушки (Код 003);
- Офис Сытый волк (Код 004).
Представим структуру данной организации в виде соответствующих коллекций узлов и связей между ними.
Коллекция узлов:
Код узла | Наименование узла |
---|---|
001 | Офис Сладкоежка |
002 | Офис Плюшки |
003 | Офис Пампушки |
004 | Офис Сытый волк |
Коллекция связей между узлами (Таблица ребер):
Источник | Приемник |
---|---|
002 | 001 |
003 | 001 |
Преобразуем данные коллекции в строки скрипта инициализации дерева.
// Подготовим список узлов.
var nodes = new vis.DataSet([
{ id: "001", label: "Офис Сладкоежка" },
{ id: "002", label: "Офис Плюшки" },
{ id: "003", label: "Офис Пампушки" },
{ id: "004", label: "Офис Сытый волк" },
]);
// Подготовим таблицу ребер.
var edges = new vis.DataSet([
{ from: "002", to: "001" },
{ from: "003", to: "001" }
]);
Добавим определение места размещения дерева и передадим необходимые параметры для инициализации.
// Объект HTML документа помещаем в переменную
var container = document.getElementById("canvasConvainer");
// Коллекция данных для визуализации
var data = {nodes: nodes, edges: edges,};
// Коллекция параметров визуализации
var options = {};
// Инициализация объекта приложения
var network = new vis.Network(container, data, options);
Разместим текст итогового скрипта в исходную HTML-разметку. В результате запуска получим следующую картинку:
Если же написать заполнение этих коллекций на основании результата запроса по нужным нам таблицам, то в качестве узлов будут выступать уникальные идентификаторы ссылок, а связь между узлами может быть описана идентификаторами реквизитов «Ссылка» и «Родитель».
Таким образом, благодаря использованию библиотеки Vis.js можно формировать сложные диаграммы связи между объектами за считанные минуты.
Оптимизация «Структурных единиц»
Случай произошел при внедрении типового продукта «1С:УНФ 8. Управление предприятием общепита» у крупного промышленного предприятия. Пользователи системы открывали форму выбора структурных единиц и время ее открытия составляло от 2 до 5 минут, что, конечно, не считалось приемлемым. При этом в качестве системы управления базами данных использовалась Microsoft SQL Server 2017, а технологическая платформа была версии 8.3.18.1289.
Для того, чтобы разобраться с этой проблемой давайте посмотрим на саму форму выбора. Она состоит из динамического списка, полей отборов и вывода контактной информации.
Если ее запустить с замером производительности, то в результате в лидерах можно увидеть: обход всех строк динамического списка методом ПриПолученииДанныхНаСервере.
Данные строки кода действительно создают задержку при отображении списка. Однако их продолжительность не занимает все время и лидирующую позицию занимает событие открытия формы. Из чего можно предположить, что данная форма так долго открывается из-за чего то другого. Например, влияния условного оформления или набора картинок значений в динамическом списке или вообще чего-то другого.
Для того, чтобы исключить все возможные гипотезы с неоптимальным кодом и модификациями форм, создадим простую внешнюю обработку, которая будет содержать только динамический список.
Пробуем ее запустить вместе с замером производительности. И тут начинается самое интересное: вроде кода нет, но почему тогда форма снова открывается так долго. Давайте разбираться.
Для этого настроим сбор технологического журнала по событиям CALL и SDBL:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
<log history="24"
location="e:\Логи\ТестСтруктурныхЕдиниц\">
<property name="all"/>
<event>
<eq property="name" value="CALL"/>
<eq property="p:processName" value="grimut_WhatYouWant3"/>
</event>
<event>
<eq property="name" value="SDBL"/>
<eq property="p:processName" value="grimut_WhatYouWant3"/>
</event>
</log>
</config>
Повторим открытие формы и остановим сбор. После открытия файла логов можно увидеть вот такие запросы и их количество совпадает с количеством элементов — 615.
Таким образом, технологическая платформа для получении дерева элементов выполняет запросы уточнения всей иерархии на вложенность в справочнике и сумма выполнения данных запросов занимает все указанное время.
Стоит отметить, что справочник «Структурные единицы» является иерархическим, с иерархией элементов.
Данная технология не является новой и применяется в других типовых решениях. Например, в «малоизвестном» продукте «1С:ERP Управление предприятием 2» справочник «Партнеры» также имеет иерархию элементов и при этом работает очень быстро. Если мы заглянем на его форму списка, то увидим лишь одно явное отличие: свойство отображение таблицы в справочнике «Партнеры» стоит «Иерархический список», а в «Структурные единицы» — Дерево.
Делаем предположение, что причиной является этот переключатель. Устанавливаем его в значение в «Иерархический список» и повторяем эксперимент. В результате получаем открытие формы за 3 секунды. А установив значение «Список» форма открылась меньше чем за одну секунду. Повторив сбор технологического журнала мы пришли к выводу, что для отображении иерархии платформа формирует запросы для каждого отображаемого элемента. При этом если элемент вложенный и ветка не раскрыта, то данные по нему получены не будут, что и влияет на скорость отображения.
Далее перед нами стал выбор: отказаться от использования дерева и перейти к иерархическому списку или реализовать данное дерево заполняя его самостоятельно.
Выбирать подразделения или склады не осознавая структуры предприятия очень тяжело. Особенно, если название элементов совпадает. Потому мы приняли решение сохранить отображение деревом. Для этого мы добавили честный реквизит с типом «Дерево» и повторили отображаемые колонки.
Для заполнения этого дерева мы сначала выполняем запрос повторяющий текст запроса динамического списка, а затем пользуемся механизмом схемы компоновки данных. Данный механизм позволяет сократить время на формирование иерархии, т. к. решает задачу иерархии не с помощью запросов к базе данных, а с помощью встроенного механизма компоновки данных.
Повторив эксперимент после модернизации получили время выполнения меньше одной секунды. Таким образом, использование динамических списков иерархических объектов и отображение в виде дерева может привести к большому количеству служебных запросов к базе данных и задержке при открытии, перемещении или поиске по таблице.
Для того, чтобы этого избежать рекомендуется либо отказаться от режима отображения деревом и перейти к одноуровневому списку, либо формировать иерархию деревьев самостоятельно. Например, с помощью механизма схемы компоновки данных.
Матрица смежности в древовидных структурах 1С необходима
Что ж, подведем первые итоги.
Прежде всего заметим, что математику никто не отменял (да и вряд ли когда-то отменит) и если речь идет о решении задачи построения графа, а в данном случае частного случая графа — дереве, то обойтись без матрицы смежности нам не удастся. 1С строит матрицу смежности, именно поэтому неизбежно возникает поле родителя или поле владельца в структуре СУБД.
Поскольку для построения дерева по матрице смежности системе нужно строить соединение по этому полю, то оно индексируется и включается в состав индексов других индексированных полей.
Кроме того, мы и сами можем построить деревья. Для этого мы можем воспользоваться возможностями СКД или сторонних компонент, но так или иначе от матрицы смежности нам никуда не деться.
Что ожидает нас в последующей серии публикации? Мы поговорим о:
- сетевых маршрутах;
- компонентах сильной связности;
- циклах;
- графах с миллионами ребер;
- реальных задачах, где возникают названные пункты и о подходах к их решению.
Вторая статья про графовые структуры в 1С: «Графовые структуры в 1С — сетевые маршруты для анализа финансового результата».
От экспертов «1С-Рарус»