Обзор паттернов хранения деревьев в реляционных БД / Хабр
Всем привет! Меня зовут Пантелеев Александр и я бэкенд-разработчик в компании Bimeister.
Постараюсь описать исчерпывающе, кратко и понятно суть основных паттернов хранения деревьев в реляционных базах данных. Надеюсь, что статья будет полезна тем, кто до сего момента не сталкивался с такими паттернами, и станет отправной точкой в их понимании.
В этой статье не будет терминов реляционной алгебры или базы данных: таких как атрибут, домен и т. д. Также не будет привязки к какой-либо СУБД, какому-либо SQL или пользовательскому коду.
Всего существует 4 общепринятых паттерна хранения деревьев:
Adjacency List;
Nested Sets;
Closure Table;
Materialized Path.
Кратко рассмотрим каждый из них.
Adjacency List
Описание
Это самый простой и интуитивный вариант хранения. Каждому элементу сопоставляется его свойство — его родительский элемент. Если родительский элемент не задан, то он считается корневым элементом.
Когда связь сопоставления элемента и родительского элемента хранится отдельно от элемента, Adjacency List можно рассматривать как частный случай Closure Table со связями 1 уровня.
Преимущества
Лёгкость реализации, а также простота вставки, удаления и перемещения элементов в дереве.
Недостатки
Можно получить только непосредственные дочерние элементы. Чтобы получить все дочерние элементы, необходимо выполнить рекурсивный запрос либо производить множественные запросы.
Примеры
Рисунок 1.Элемент | Родительский элемент |
A | — |
B | A |
C | B |
D | C |
E | B |
F | B |
G | A |
H | G |
I | A |
Рассмотрим элемент «B»:
Чтобы получить все его дочерние элементы, нам необходимо выбрать элементы, удовлетворяющие условию:
Родительский элемент равен «B»
Nested Sets
Описание
Каждому элементу сопоставляются свойства: левый и правый индекс, на основе которых будет производиться выборка дочерних элементов. Также, но необязательно, элемент может дополняться свойством уровень для указания желаемого уровня вложенности выбираемого элемента относительно корня или родительского элемента.
Запрос получения дочерних элементов строится на том факте, что для любого дочернего элемента выполняются условия:
При создании и обновлении дерева левые и правые индексы элементов дерева, при его обходе в глубину, заполняются по определённым правилам.
Преимущества
Возможность получения дочерних элементов любых уровней вложенности с помощью простого одиночного запроса.
Недостатки
При использовании целочисленных типов для левого и правого индекса и уровня необходимо пересчитывать индексы всех связанных элементов в следующих случаях:
при вставке элементов;
при удалении элементов;
при изменении родительского элемента.
Пример
Рисунок 2.Элемент | Левый индекс | Правый индекс | Уровень |
A | 1 | 18 | 0 |
B | 2 | 11 | 1 |
C | 3 | 6 | 2 |
D | 4 | 5 | 3 |
E | 7 | 8 | 2 |
F | 9 | 10 | 2 |
G | 12 | 15 | 1 |
H | 13 | 14 | 2 |
I | 16 | 17 | 1 |
Рассмотрим элемент «B». Его значения свойств:
левый индекс = 2;
правый индекс = 11;
уровень = 1.
Чтобы получить все его дочерние элементы, нам необходимо выбрать элементы, удовлетворяющие условию:
левый индекс больше 2 И правый индекс меньше 11
Чтобы получить его непосредственные дочерние элементы, нам необходимо добавить к условию ограничение на уровень:
левый индекс больше 2 И правый индекс меньше 11 И уровень = 1
Чтобы получить дочерние элементы вместе с родительским элементом, нам необходимо ослабить условия индексов:
левый индекс больше или равен 2 И правый индекс меньше или равен 11
Closure Table
Описание
Суть этого паттерна заключается в том, что мы сопоставляем каждому элементу множество связей со всеми его дочерними элементами или сопоставляем каждому элементу множество связей со всеми его родительскими элементами. Также, но необязательно, связь может содержать свойство Уровень. Уровень задаёт расстояние между элементами в дереве.
Если в запросе получения дочерних или родительских элементов по элементу необходимо получать в результате сам элемент, то нужно добавлять связь элемента самого на себя — то есть со значением уровня связи 0.
Преимущества
Возможность получения дочерних элементов любых уровней вложенности с помощью простого одиночного запроса.
Возможность получения родительских элементов любых уровней с их иерархией относительно дочернего элемента с помощью простого одиночного запроса.
Недостатки
При вставке и удалении элементов из дерева, а также при перемещении элементов в дереве необходимо пересчитывать все связи, в которых этот элемент участвует.
Пример
Рисунок 3.Родительский элемент | Дочерний элемент | Уровень |
A | A | 0 |
A | B | 1 |
A | C | 2 |
A | E | 2 |
A | D | 3 |
B | B | 0 |
B | C | 1 |
B | E | 1 |
B | D | 2 |
C | C | 0 |
C | D | 1 |
E | E | 0 |
D | D | 0 |
Рассмотрим элемент «B»:
Чтобы получить все его дочерние элементы, нам необходимо выбрать элементы, удовлетворяющие условию:
родительский элемент равен «B»
Чтобы получить его непосредственные дочерние элементы, нам необходимо добавить к условию ограничение на уровень:
родительский элемент равен «B» И уровень = 1
Чтобы получить дочерние элементы вместе с родительскими, нам необходимо ослабить условия индексов:
родительский элемент равен «B» И уровень = 0
Чтобы получить все его родительские элементы, нам необходимо выбрать элементы, удовлетворяющие условию:
дочерний элемент равен «B»
Materialized Path
Описание
Каждому элементу сопоставляется свойство — его путь, который является последовательностью родительских элементов заданного элемента, отсортированных по уровням. В общем случае, чтобы формировать гибкие запросы, тип реализации свойства путь должен поддерживать сопоставление по шаблону в каком-либо виде. При денормализации пути в отдельную таблицу получается разновидность Closue Table.
Условия запросов на получение элементов заключается в применении предиката над свойством путь.
Преимущества
Возможность получения дочерних элементов любых уровней вложенности.
Возможность получения родительских элементов любых уровней с их иерархией относительно дочернего элемента.
Лёгкость вставки элемента.
Лёгкость удаления элемента.
Недостатки
Сложность изменения родителя для существующего элемента. Для всех дочерних элементов необходимо пересчитать новый путь.
Операции со свойством путь обычно происходят долго.
Пример
Рисунок 4.Элемент | Путь |
A |
|
B | A |
C | A B |
D | A B C |
E | A B |
Рассмотрим элемент «B»:
Чтобы получить все его дочерние элементы, нам необходимо выбрать элементы, удовлетворяющие условию:
путь содержит «B»
Чтобы получить его непосредственные дочерние элементы, нужно указать позицию, в которой содержится элемент. В примере путь отсортирован так, что последняя часть пути — это непосредственный родительский элемент:
последняя часть пути равна «B»
Заключение
Мы кратко рассмотрели основные паттерны хранения деревьев в реляционной базе данных. Их основные достоинства и недоставки, а также на примерах рассмотрели основные запросы к ним. В этой статье не были рассмотрены алгоритмы построения и заполнения метаданных деревьев, то есть операции добавления, обновления и удаления элементов.
Компоновщик
/ Паттерны проектирования / Структурные паттерны
Также известен как: Дерево, Composite
Суть паттернаКомпоновщик — это структурный паттерн проектирования, который позволяет сгруппировать множество объектов в древовидную структуру, а затем работать с ней так, как будто это единичный объект.
ПроблемаПаттерн Компоновщик имеет смысл только тогда, когда основная модель вашей программы может быть структурирована в виде дерева.
Например, есть два объекта: Продукт
и Коробка
. Коробка
может содержать несколько Продуктов
и других Коробок
поменьше. Те, в свою очередь, тоже содержат либо Продукты
, либо Коробки
Теперь предположим, ваши Продукты
и Коробки
могут быть частью заказов. Каждый заказ может содержать как простые Продукты
без упаковки, так и составные Коробки
. Ваша задача состоит в том, чтобы узнать цену всего заказа.
Заказ может состоять из различных продуктов, упакованных в собственные коробки.
Если решать задачу в лоб, то вам потребуется открыть все коробки заказа, перебрать все продукты и посчитать их суммарную стоимость. Но это слишком хлопотно, так как типы коробок и их содержимое могут быть вам неизвестны. Кроме того, наперёд неизвестно и количество уровней вложенности коробок, поэтому перебрать коробки простым циклом не выйдет.
РешениеКомпоновщик предлагает рассматривать Продукт
и Коробку
через единый интерфейс с общим методом получения стоимости.
Продукт
просто вернёт свою цену. Коробка
спросит цену каждого предмета внутри себя и вернёт сумму результатов. Если одним из внутренних предметов окажется коробка поменьше, она тоже будет перебирать своё содержимое, и так далее, пока не будут посчитаны все составные части.
Компоновщик рекурсивно запускает действие по всем элементам дерева — от корня к листьям.
Для вас, клиента, главное, что теперь не нужно ничего знать о структуре заказов. Вы вызываете метод получения цены, он возвращает цифру, а вы не тонете в горах картона и скотча.
Аналогия из жизниПример армейской структуры.
Армии большинства государств могут быть представлены в виде перевёрнутых деревьев. На нижнем уровне у вас есть солдаты, затем взводы, затем полки, а затем целые армии. Приказы отдаются сверху и спускаются вниз по структуре командования, пока не доходят до конкретного солдата.
Компонент определяет общий интерфейс для простых и составных компонентов дерева.
Лист — это простой компонент дерева, не имеющий ответвлений.
Из-за того, что им некому больше передавать выполнение, классы листьев будут содержать большую часть полезного кода.
Контейнер (или композит) — это составной компонент дерева. Он содержит набор дочерних компонентов, но ничего не знает об их типах. Это могут быть как простые компоненты-листья, так и другие компоненты-контейнеры. Но это не является проблемой, если все дочерние компоненты следуют единому интерфейсу.
Методы контейнера переадресуют основную работу своим дочерним компонентам, хотя и могут добавлять что-то своё к результату.
Клиент работает с деревом через общий интерфейс компонентов.
Благодаря этому, клиенту не важно, что перед ним находится — простой или составной компонент дерева.
В этом примере Компоновщик помогает реализовать вложенные геометрические фигуры.
Пример редактора геометрических фигур.
Класс CompoundGraphic
может содержать любое количество подфигур, включая такие же контейнеры, как он сам. Контейнер реализует те же методы, что и простые фигуры. Но, вместо непосредственного действия, он передаёт вызовы всем вложенным компонентам, используя рекурсию. Затем он как бы «суммирует» результаты всех вложенных фигур.
Клиентский код работает со всеми фигурами через общий интерфейс фигур и не знает, что перед ним — простая фигура или составная. Это позволяет клиентскому коду работать с деревьями объектов любой сложности, не привязываясь к конкретным классам объектов, формирующих дерево.
// Общий интерфейс компонентов.
interface Graphic is
method move(x, y)
method draw()
// Простой компонент.
class Dot implements Graphic is
field x, y
constructor Dot(x, y) { ... }
method move(x, y) is
this.x += x, this.y += y
method draw() is
// Нарисовать точку в координате X, Y.
// Компоненты могут расширять другие компоненты.
class Circle extends Dot is
field radius
constructor Circle(x, y, radius) { ... }
method draw() is
// Нарисовать окружность в координате X, Y и радиусом R.
// Контейнер содержит операции добавления/удаления дочерних
// компонентов. Все стандартные операции интерфейса компонентов
// он делегирует каждому из дочерних компонентов.
class CompoundGraphic implements Graphic is
field children: array of Graphic
method add(child: Graphic) is
// Добавить компонент в список дочерних.
method remove(child: Graphic) is
// Убрать компонент из списка дочерних.
method move(x, y) is
foreach (child in children) do
child. move(x, y)
method draw() is
// 1. Для каждого дочернего компонента:
// - Отрисовать компонент.
// - Определить координаты максимальной границы.
// 2. Нарисовать пунктирную границу вокруг всей области.
// Приложение работает единообразно как с единичными
// компонентами, так и с целыми группами компонентов.
class ImageEditor is
field all: CompoundGraphic
method load() is
all = new CompoundGraphic()
all.add(new Dot(1, 2))
all.add(new Circle(5, 3, 10))
// ...
// Группировка выбранных компонентов в один сложный
// компонент.
method groupSelected(components: array of Graphic) is
group = new CompoundGraphic()
foreach (component in components) do
group.add(component)
all.remove(component)
all.add(group)
// Все компоненты будут отрисованы.
all.draw()
Когда вам нужно представить древовидную структуру объектов.
Паттерн Компоновщик предлагает хранить в составных объектах ссылки на другие простые или составные объекты. Те, в свою очередь, тоже могут хранить свои вложенные объекты и так далее. В итоге вы можете строить сложную древовидную структуру данных, используя всего две основные разновидности объектов.
Когда клиенты должны единообразно трактовать простые и составные объекты.
Благодаря тому, что простые и составные объекты реализуют общий интерфейс, клиенту безразлично, с каким именно объектом ему предстоит работать.
Шаги реализацииУбедитесь, что вашу бизнес-логику можно представить как древовидную структуру. Попытайтесь разбить её на простые компоненты и контейнеры. Помните, что контейнеры могут содержать как простые компоненты, так и другие вложенные контейнеры.
Создайте общий интерфейс компонентов, который объединит операции контейнеров и простых компонентов дерева. Интерфейс будет удачным, если вы сможете использовать его, чтобы взаимозаменять простые и составные компоненты без потери смысла.
Создайте класс компонентов-листьев, не имеющих дальнейших ответвлений. Имейте в виду, что программа может содержать несколько таких классов.
Создайте класс компонентов-контейнеров и добавьте в него массив для хранения ссылок на вложенные компоненты. Этот массив должен быть способен содержать как простые, так и составные компоненты, поэтому убедитесь, что он объявлен с типом интерфейса компонентов.
Реализуйте в контейнере методы интерфейса компонентов, помня о том, что контейнеры должны делегировать основную работу своим дочерним компонентам.
Добавьте операции добавления и удаления дочерних компонентов в класс контейнеров.
Имейте в виду, что методы добавления/удаления дочерних компонентов можно поместить и в интерфейс компонентов. Да, это нарушит принцип разделения интерфейса, так как реализации методов будут пустыми в компонентах-листьях. Но зато все компоненты дерева станут действительно одинаковыми для клиента.
- Упрощает архитектуру клиента при работе со сложным деревом компонентов.
- Облегчает добавление новых видов компонентов.
- Создаёт слишком общий дизайн классов.
Строитель позволяет пошагово сооружать дерево Компоновщика.
Цепочку обязанностей часто используют вместе с Компоновщиком. В этом случае запрос передаётся от дочерних компонентов к их родителям.
Вы можете обходить дерево Компоновщика, используя Итератор.
Вы можете выполнить какое-то действие над всем деревом Компоновщика при помощи Посетителя.
Компоновщик часто совмещают с Легковесом, чтобы реализовать общие ветки дерева и сэкономить при этом память.
Компоновщик и Декоратор имеют похожие структуры классов из-за того, что оба построены на рекурсивной вложенности. Она позволяет связать в одну структуру бесконечное количество объектов.
Декоратор оборачивает только один объект, а узел Компоновщика может иметь много детей. Декоратор добавляет вложенному объекту новую функциональность, а Компоновщик не добавляет ничего нового, но «суммирует» результаты всех своих детей.
Но они могут и сотрудничать: Компоновщик может использовать Декоратор, чтобы переопределить функции отдельных частей дерева компонентов.
Архитектура, построенная на Компоновщиках и Декораторах, часто может быть улучшена за счёт внедрения Прототипа. Он позволяет клонировать сложные структуры объектов, а не собирать их заново.
Tree Patterns — Etsy Turkey
Etsy больше не поддерживает старые версии вашего веб-браузера, чтобы обеспечить безопасность пользовательских данных. Пожалуйста, обновите до последней версии.
Воспользуйтесь всеми преимуществами нашего сайта, включив JavaScript.
Найдите что-нибудь памятное, присоединяйтесь к сообществу, делающему добро.
( 1000+ релевантных результатов, с рекламой Продавцы, желающие расширить свой бизнес и привлечь больше заинтересованных покупателей, могут использовать рекламную платформу Etsy для продвижения своих товаров. Вы увидите результаты объявлений, основанные на таких факторах, как релевантность и сумма, которую продавцы платят за клик. Узнать больше. )
Построение с помощью шаблонов: древовидный шаблон
Многие из шаблонов проектирования схем, которые мы рассмотрели до сих пор, подчеркивали, что экономия времени на операциях JOIN является преимуществом. Данные, к которым обращаются вместе, должны храниться вместе, и некоторое дублирование данных допустимо. Хорошим примером является шаблон проектирования схемы, такой как расширенная ссылка. Однако что, если данные, которые нужно объединить, являются иерархическими? Например, вы хотите определить цепочку отчетности от сотрудника до генерального директора? MongoDB предоставляет оператор $graphLookup для навигации по данным в виде графиков, и это может быть одним из решений. Однако, если вам нужно выполнить много запросов к этой иерархической структуре данных, вы можете применить то же правило хранения вместе данных, к которым осуществляется доступ вместе. Здесь мы можем использовать шаблон дерева.
Шаблон дерева
Существует множество способов представить дерево в устаревшей табличной базе данных. Наиболее распространенные из них для узла в графе, чтобы перечислить своего родителя и для узла, чтобы перечислить своих дочерних элементов. Оба эти представления могут потребовать множественного доступа для построения цепочки узлов.
В качестве альтернативы мы можем сохранить полный путь от узла к вершине иерархии. В этом случае мы в основном будем хранить «родителей» для каждого узла. В табличной базе данных это, скорее всего, будет сделано путем кодирования списка родителей. Подход в MongoDB состоит в том, чтобы просто представить это как массив.
Как видно здесь, в этом представлении есть некоторое дублирование данных. Если информация относительно статична, как в генеалогии, ваши родители и предки не изменятся, что упрощает управление этим массивом. Однако в нашем примере с корпоративной структурой, когда что-то меняется и происходит реструктуризация, вам нужно будет обновить иерархию по мере необходимости. Это все еще небольшие затраты по сравнению с преимуществами, которые вы можете получить, если не будете все время рассчитывать деревья.
Пример использования
Каталоги товаров — еще один очень хороший пример использования шаблона «Дерево». Часто товары относятся к категориям, которые являются частью других категорий. Например, твердотельный накопитель может находиться в разделе Жесткие диски , который находится в разделе Хранилище , который находится в разделе Компоненты компьютера . Иногда организация категорий может меняться, но не слишком часто.
Обратите внимание в документе выше на поле ancestor_categories
, которое отслеживает всю иерархию. У нас также есть поле родительская_категория
. Дублирование непосредственного родителя в этих двух полях — это передовой метод, который мы разработали после работы со многими клиентами, использующими древовидный шаблон. Включение поля «родительский» часто бывает удобным, особенно если вам нужно поддерживать возможность использования $graphLookup в ваших документах.
Хранение предков в массиве дает возможность создать многоключевой индекс для этих значений.