В различных приложениях часто возникает потребность в реализации иерархического представления объектов. Как правило, это используется для их классификации путем задания групп. Эти группы образуют дерево динамической глубины, которая в дальнейшем используется для навигации, агрегирования данных, задания параметров.
В этой статье я покажу, каким образом эту логику можно реализовать в открытой и бесплатной платформе lsFusion.
В качестве примера возьмем простую логику, в которой нужно реализовать логику товаров, объединенных в определенные группы, которые образуют иерархию динамической глубины. При этом товар может быть привязан и к промежуточному узлу дерева.
Для начала по стандартной схеме объявим сущность Группа товаров в виде простого плоского класса с формами редактирования и списка:
CLASS Group 'Группа'; |
Теперь сделаем из групп иерархию. Для этого введем свойство, которое будет содержать ссылку на родительскую группу:
parent = DATA Group (Group); |
Дальше сделаем свойство, которое будет рекурсивно определять связь между двумя группами:
level 'Уровень' (Group child, Group parent) = |
По какому именно принципу работает оператор RECURSION, в данной статье я описывать не буду, но свойство level будет возвращать 2 в степени “длина пути между child и parent в соответствующем направленном дереве”. MATERIALIZED указывает, что платформа должна хранить его в отдельной таблице, где для каждой пары связанных узлов будет отдельная запись со значением level в соответствующей колонке. При любом изменении структуры дерева эта таблица будет автоматически пересчитываться.
Например, для вот такого дерева:
Таблица будет выглядеть вот так:
В ней key0 — это код потомка, а key1 — код родителя. Количество записей в этой таблице будет приблизительно равно количеству групп, умноженное на среднюю глубину дерева. Такая схема хранения будет полезна тем, что при необходимости считать всех потомков группы не придется прибегать к CTE запросам, а можно будет воспользоваться обычным JOIN к этой таблице.
Дальше на основании построенного свойства можно высчитать каноническое имя группы:
canonicalName 'Каноническое имя' (Group group) = |
Например, для группы Молоко на приведенной выше картинке каноническое имя будет равно Все / Продовольственные товары / Молочные продукты / Молоко. CHARWIDTH указывается для того, чтобы сказать платформе какую под это свойство отводить ширину (в символах) при построении интерфейса.
Теперь расширим форму просмотра и редактирования групп вновь созданными свойствами:
EXTEND FORM group |
Форма со списком групп в плоском виде будет выглядеть следующим образом:
После того, как логика групп завершена, добавляем сущность Товар:
CLASS Product 'Товар'; |
Создаем ссылку товара на группу товара, к которой он относится:
group 'Группа' = DATA Group (Product); |
Наконец, сделаем форму по вводу товаров, в которой будет два элемента: дерево групп и список товаров. Для выбранной группы дерева в списке будут отображаться только товары, которые принадлежат любому потомку выбранного узла. Сначала объявим форму и добавим на нее дерево со списком групп:
FORM products 'Товары' |
При помощи команды TREE создается дерево из объектов класса Group, иерархия которых определяется по ранее созданному свойству parent.
Добавляем форму в навигатор:
NAVIGATOR { |
В данном примере, ввод и редактирование товаров будут осуществляться не через отдельные диалоги, а непосредственно в самой форме. Для этого создадим действие по созданию товара с привязкой к выбранной группе:
newProduct 'Добавить' (Group g) { |
Теперь, на созданную ранее форму, добавляем список товаров с редактируемыми колонками:
EXTEND FORM products |
Закидываем на форму кнопки по добавлению и удалению товаров:
EXTEND FORM products |
Так как действие newProduct определено для группы товаров, то в явную нужно указать, что оно должно быть добавлено в тулбар со списком товаров (p).
Осталось настроить дизайн, чтобы дерево отображалось слева, а список товаров — справа, а между ними был разделитель, при помощи которого можно изменять размеры объектов:
DESIGN products { |
Итоговая форма будет выглядеть следующим образом:
После того, как иерархия товаров и групп создана, часто возникает потребность в задании некоторого параметра на любом из уровней. При этом, чем на более низком уровне иерархии он задан, тем его значение приоритетнее. Например, если для группы Молочная продукция задано значение 30, а для группы Молоко — 20, то выбраться должно последнее из них.
Предположим нужно таким образом определить параметр Надбавка. Для этого нужно сначала создать соответствующее свойство для группы:
markup 'Надбавка, %' = DATA NUMERIC[10,2] (Group); |
Для того, чтобы найти нужное значение, достаточно просто воспользоваться группировкой с выбором последнего значения:
parentMarkup 'Надбавка (от верхней группы), %' (Group child) = |
Если переводить на обычный язык, то это выражение находит (GROUP) последнюю (LAST) надбавку (markup) по верхней группе (Group parent), в порядке убывания расстояния до нее (ORDER DESC level(child, parent)), для которой эта надбавка задана (WHERE markup(parent)). Здесь хочется отметить, насколько язык lsFusion соответствует естественному языку.
Добавим созданные выше свойства на форму с товарами в дерево групп:
EXTEND FORM products |
Предположим, что существует потребность задавать надбавку непосредственно для товара, и чтобы она была приоритетнее надбавки для группы. Для этого сначала создаем первичное свойство для товара:
dataMarkup 'Надбавка по товару, %' = DATA NUMERIC[10,2] (Product); |
Затем объявляем свойство, которое будет возвращать надбавку от товара, если она задана, или надбавку от группы:
markup 'Надбавка, %' (Product p) = OVERRIDE dataMarkup(p), parentMarkup(group(p)); |
После этого добавляем оба свойства на форму:
EXTEND FORM products |
Механизм задания надбавок для групп и товаров будет выглядеть следующим образом:
Заключение
В приведенной статье мы смогли создать логику товаров, объединить их в группы с иерархией динамической глубины, а также предоставить пользователю возможность задавать надбавки на любом из уровней. Все это заняло около 70 значимых строчек кода. Попробовать, как это работает в онлайне, а также внести свои изменения в код, можно в соответствующем разделе сайта (вкладка Платформа). Вот весь исходный код, который нужно вставить в соответствующее поле:
Исходный код
CLASS Group 'Группа'; |
Описанный выше шаблон может различным образом модифицироваться и использоваться путем добавления дополнительных параметров в свойства. Например, в одной из реализаций ERP-системы надбавки для групп и товаров задаются подобным образом не глобально, а для каждого вида цены отдельно. При этом реализация по сложности ничем не отличается от описанного выше примера.