Всем привет! Меня зовут Хусрав, я бэкенд разработчик в компании Bimeister. В этой статье я бы хотел бы поговорить о способе поиска родительских и дочерних элементов структуры посредством PostgreSQL Materialized Path. Упорядочить собственное понимание темы и на простых практических примерах продемонстрировать работу Materialized Path. Эти примеры могут оказаться полезными для работы с иерархическими данными посредством реляционной СУБД. Статья является вводной и рассчитана на людей, незнакомых с темой. Сразу оговорюсь, что существует несколько способов хранения деревьев в реляционной базе. Для ознакомления со всеми ними рекомендую к прочтению статью моего коллеги на эту тему. Ниже я рассмотрю лишь один из них, и упор будет сделан, прежде всего, на лёгкость воспроизведения примеров. До настоящего времени моим основным стеком была связка C# и MSSQL, и мне не приходилось сталкиваться с деревьями. Я привык думать, что когда речь заходит о сложных структурных данных, то решение однозначно упирается в графовую БД, однако здесь я был ограничен реляционной БД. В этих ограничениях единственно логичным для работы с деревьями решением было использование возможности хранения дерева в PostgreSQL посредством Materialized Path. Этим подходом я и спешу поделиться с вами. Представим, что у нас есть некое дерево классификации транспортных средств, и наша задача — эффективно получить дочерние и родительские элементы относительно заданного элемента. В таком случае мы можем организовать дерево посредством Materialized Path...  Начнем с установки базы данных PostgreSQL в docker-контейнер.  Устанавливаем docker desktop. Открываем любимый терминал, в моем случае, это git bash. Скачиваем образ postgres c docker hub. docker pull postgres Создаем docker-контейнер с нашей базой данных. docker run --name myPostgresDb -p 5455:5432 -e POSTGRES_USER=postgresUser -e POSTGRES_PASSWORD=postgresPW -e POSTGRES_DB=postgres -d postgres  Описание: myPostgresDb — имя контейнера 5455:5432 — порт для внешнего подключения к контейнеру postgresUser — пользователь postgresPW — пароль postgres — имя базы данных Устанавливаем любимый редактор для работы с базой данных PostgreSQL. В моем случае это PgAdmin. Настраиваем подключение в редакторе: Name: MyPostresDB. Вкладка Connection: Host: localhost Port: 5455 Maintenance Database: postgres UserName: postgresUser Password: postgresPW Дополнительная информация по установке PostgreSQL в docker.  Создаем таблицу и индекс GIST: create table transport (name text, path ltree); create index path_gist_idx on transport using GIST (path);  Индекс GIST дает возможность выполнять такие выражения (более подробное описание):  <, <=, =, >=, >, @>, <@, @, ~, ?  Примечание: если у вас при создании таблицы выдает ошибку, что нет такого типа, как ltree, то выполните следующую команду: create extension ltree  Наполняем тестовыми данными: Получаем необходимые данные: Получение дочерних элементов 'Transport.Car':  Получение родительских элементов 'Transport.AirCraft.AirBus': Как видно из примера, получить все необходимые элементы достаточно просто, выполнив несложные запросы к базе данных. Об остальных возможностях Ltree Materialized Path можно прочитать в официальной документации.  Давайте теперь перейдем к созданию нашего веб-приложения, в котором мы будем работать с деревом посредством API и Entity Framework Core.  Создаем проект с типом ASP.NET Core Web API и назовем наш проект MaterializedPathAPI.  Конфигурируем наш проект в соответствии со следующим изображением:    Устанавливаем nuget пакеты: Npgsql.EntityFrameworkCore.PostgreSQL. Microsoft.EntityFrameworkCore.Design. Microsoft.EntityFrameworkCore.Tools. Далее нам нужно создать определенную структуру проекта, основными элементами которой будут: TreeController – контроллер для взаимодействия извне с нашим API; TreeService – сервис с основной логикой; TreeContext – контекст базы данных, использующий EntityFrame Work Core; TreeRepository – репозиторий для работы с контекст базы данных.   Создаем папку DB и добавляем папку Models, а в нее основные сущности для дерева: Tree.cs. TreeItem.cs. Класс для создания таблицы TreeItems для хранения элементов дерева.   Создаем контекст базы данных TreeContext.cs в папке DB. В этом классе в методе OnModelCreating() добавляем индекс «gist» к таблице TreeItem для более удобной работы с LTree. Мы сможем пользоваться методами и свойствами LTree, аналогами которых на PostgreSQL являются ниже указанные операторы: <, <=, =, >=, >, @>, <@, @, ~, ? (более подробно в оф. документации) При создании нового элемента, первым делом мы должны получить значение свойства MaterializedPath его родителя. Далее мы должны конкатенировать свойство MaterializedPath со значение нового элемента (EntityValue) через точку, чтобы получить полный путь от вышестоящих по иерархии родительских элементов.  В итоге если у родительского элемента значение равно “first” и значения нового дочернего элемента “second”, в итоге в свойстве MaterialledPath мы должны поместить значение “fist.second”  Для получения дочерних и родительских элементов мы можем воспользоваться методами класса Ltree: Метод IsAncestorOf() соответствует в postgreSql оператору @> и возвращает все родительские элементы.  Метод IsDescendantOf() соответствует в postgreSql оператору <@ и возвращает все дочерние элементы. Примечание: при работе со свойством класса TreeItem MaterializedPath (тип Ltree) необходимо учитывать, что его методы и свойства доступны до материализации запроса с базы данных.   Итак, продолжим. Создаем TreeRepository.cs для работы с контекстом базы данных в папке DB. Для создания дерева нам потребуются: метод для создания корня дерева (стр. 16); метод для создания дочерних элементов в таблице TreeItem (стр. 32); метод для получения информации о дереве (стр. 59); методы для получения дочерних и родительских элементов определенного элемента (стр. 116 и стр.139). Создаем TreeService.cs для работы с основной логикой приложения в корневой папке. Данный сервис будет для нас промежуточным звеном, с которым будет непосредственно работать контроллер. Добавляем настройки строки подключения к нашей базе данных в appsettings.json: Конфигурируем класс Program. Чтобы не заморачиваться с авторизацией и созданием дополнительных интерфейсов для  dependency injection, удалим app.UseAuthorization(), добавим конфигурацию для подключения к нашей базе данных из appsettings.json и добавим в сервисы TreeRepository и TreeService: Конфигурируем launchsettings.json: Нам осталось только определить модели для работы с нашим приложением извне. Создаем папку Models в корне проекта, создаем папку Info и заносим в нее сущности:  CreateTreeInfo.cs – модель для создания нового дерева.  GetTreeItemsByTreeInfo.cs – модель для получения иерархии элементов. FromIndex – индекс элемента, с которого мы получаем дерево. ToIndex -максимальное количество элементов для получения. ExpandedTreeItemIds – список id элементов, если мы хотим получить раскрытие элемента и получить его дочерние элементы на первом уровне. TreeKey – id дерева из таблицы Trees. TreeItemInfo.cs – модель для создания нового элемента в таблице TreeItems.  K папке Models также создаем папку View (это модели для ответа на запрос через контроллер) и создаем класс TreeItemView. EntityId — id элемента в дереве; ParentEntityId — id родительского элемента; EntityValue — значение элемента; Path — путь к элементу с начала иерархии. Как говорилось в примере выше, тут будет хранится значение “first.second”; Level — уровень, на котором находится элемент. 1 уровень — это корневой элемент. Чем больше значение уровня, тем выше уровень вложенности элемента в иерархии. И, наконец, создаем простой контроллер TreeController.cs для работы с нашим веб-приложением извне в папке Controllers в корне проекта. Далее нам нужно создать наши таблицы в базе данных. Для этого открываем Package manager Console и создаем Code-First базу данных: Теперь у нас есть все необходимые сущности в нашей базе данных, а также готовое API. Осталось наполнить данными наши таблицы посредством API и получить корректные результаты. Обращение к нашему API я оставлю вам в виде готовых cUrl запросов на создание и поиск сущностей.  Можно воспользоваться инструкцией, как их импортировать в PostMan для более удобного взаимодействия с ними. Создание дерева: Создание корневых элементов: Я не буду расписывать все запросы для создание разветвлённого дерева. Думаю, это будет отличной тренировкой для вас самих. Добавим побольше дочерних элементов для Car. И теперь к Truck добавим дочерние элементы: И еще для наглядности добавим элемент MicroTruck для MiniTruck: Теперь у нас есть иерархия элементов по элементу Car. Давайте попробуем получить данные. Получение родителей элемента MiniTruck: Результат: Как видно из ответа, у нас есть уровень вложенности элемента в поле level. 1 уровень — это уровень корневых элементов и, соответственно, чем больше уровень, тем ниже по дереву находится элемент. Теперь получим дочерние элементы от Car: Результат: Также я дополнительно оставил метод в контроллере GetTreeItemsFlatTree, который возвращает корневые элементы дерева, и также вы можете получить элементы нижнего уровня этих элементов, если укажите в свойстве ExpandedTreeItemIds идентификатор раскрываемого объекта. Вот пример, где раскроем дополнительно элемент Car из списка: Результат: В этой статье мы коснулись темы работы с деревом, используя PostgreSQL Materialized Path, подготовили демонстрационный стенд с деревом и API для работы с ним, а также сделали несколько тестовых запросов. Прошу прощения, если в некоторых местах я слишком скрупулёзно описывал элементарные вещи. Однако, я уверен, на базе этого проекта, вы сможете исследовать глубже как текущий, так и другие способы работы с деревьями, и, возможно, улучшить представленное решение. Мне не хотелось сильно уходить в теорию, главной моей задачей было показать рабочий пример и позволить разработчику прочувствовать механизмы работы Materialized Path, по шагам выполняя практические упражнения. Данная статья в какой-то мере адресована мне самому из прошлого. Мне она позволила структурировать моё понимание этого механизма работы с деревьями. Надеюсь, она и вам была полезна или, как минимум, интересна! P. S. Хочу отметить, что выбор способа работы с деревьями очень варьируется в зависимости от требований. Если тема вам интересна, очень рекомендую ознакомиться со следующими материалами: Обзор паттернов хранения деревьев в реляционных БД / Хабр (habr.com) PostgreSQL : Документация: 9.6: F.21. ltree : Компания Postgres Professional Struct LTree | Npgsql Documentation PostgreSQL : Документация: 9.6: F.21. ltree : Компания Postgres Professional Репозиторий с кодом, который рассматривали выше: KhusravBim/MaterializedPathTreeApi (github.com).