Перейти к основному содержанию

Иерархические сущности

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

TypeORM поддерживает шаблоны "Список смежности" и "Таблица замыканий" для хранения древовидных структур. Подробнее о работе с иерархическими таблицами смотрите в презентации Билла Карвина.

Список смежности (Adjacency list)

Список смежности — простая модель с самореференциальными связями. Обратите внимание: TreeRepository не поддерживает этот подход. Преимущество — простота реализации, недостаток — невозможность загрузки больших деревьев целиком из-за ограничений JOIN. Подробнее о применении списков смежности читайте в статье Мэтью Шинкела. Пример:

import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
OneToMany,
} from "typeorm"

@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@Column()
description: string

@ManyToOne((type) => Category, (category) => category.children)
parent: Category

@OneToMany((type) => Category, (category) => category.parent)
children: Category[]
}

Вложенные множества (Nested set)

Вложенные множества — альтернативный подход для хранения деревьев в БД. Обеспечивает эффективное чтение, но неудобен для операций записи. Не поддерживает несколько корневых элементов. Пример:

import {
Entity,
Tree,
Column,
PrimaryGeneratedColumn,
TreeChildren,
TreeParent,
TreeLevelColumn,
} from "typeorm"

@Entity()
@Tree("nested-set")
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@TreeChildren()
children: Category[]

@TreeParent()
parent: Category
}

Материализованный путь (Materialized Path)

Материализованный путь (также известный как перечисление путей) — ещё один метод хранения древовидных структур. Простой и эффективный подход. Пример:

import {
Entity,
Tree,
Column,
PrimaryGeneratedColumn,
TreeChildren,
TreeParent,
TreeLevelColumn,
} from "typeorm"

@Entity()
@Tree("materialized-path")
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@TreeChildren()
children: Category[]

@TreeParent()
parent: Category
}

Таблица замыканий (Closure table)

Таблица замыканий хранит связи между родителями и потомками в отдельной таблице специальным образом. Эффективна как для чтения, так и для записи. Пример:

import {
Entity,
Tree,
Column,
PrimaryGeneratedColumn,
TreeChildren,
TreeParent,
TreeLevelColumn,
} from "typeorm"

@Entity()
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@TreeChildren()
children: Category[]

@TreeParent()
parent: Category
}

Имя таблицы замыканий и/или имена её столбцов можно указать через опциональный параметр options в @Tree("closure-table", options). ancestorColumnName и descandantColumnName принимают функции обратного вызова, которые получают метаданные первичного столбца и возвращают имя столбца.

@Tree("closure-table", {
closureTableName: "category_closure",
ancestorColumnName: (column) => "ancestor_" + column.propertyName,
descendantColumnName: (column) => "descendant_" + column.propertyName,
})

Работа с иерархическими сущностями

Для связывания сущностей укажите родительский элемент в дочерней сущности перед сохранением:

const a1 = new Category()
a1.name = "a1"
await dataSource.manager.save(a1)

const a11 = new Category()
a11.name = "a11"
a11.parent = a1
await dataSource.manager.save(a11)

const a12 = new Category()
a12.name = "a12"
a12.parent = a1
await dataSource.manager.save(a12)

const a111 = new Category()
a111.name = "a111"
a111.parent = a11
await dataSource.manager.save(a111)

const a112 = new Category()
a112.name = "a112"
a112.parent = a11
await dataSource.manager.save(a112)

Для загрузки дерева используйте TreeRepository:

const trees = await dataSource.manager.getTreeRepository(Category).findTrees()

Результат (trees) будет иметь структуру:

[
{
"id": 1,
"name": "a1",
"children": [
{
"id": 2,
"name": "a11",
"children": [
{
"id": 4,
"name": "a111"
},
{
"id": 5,
"name": "a112"
}
]
},
{
"id": 3,
"name": "a12"
}
]
}
]

TreeRepository предоставляет специальные методы для работы с иерархиями:

  • findTrees - Возвращает все деревья в базе данных вместе со всеми дочерними элементами, элементами второго уровня и т.д.
const treeCategories = await dataSource.manager
.getTreeRepository(Category)
.findTrees()
// returns root categories with sub categories inside

const treeCategoriesWithLimitedDepth = await dataSource.manager
.getTreeRepository(Category)
.findTrees({ depth: 2 })
// returns root categories with sub categories inside, up to depth 2
  • findRoots - Находит все корневые сущности (без предков).
    Не загружает листья дочерних элементов.
const rootCategories = await dataSource.manager
.getTreeRepository(Category)
.findRoots()
// returns root categories without sub categories inside
  • findDescendants - Получает всех потомков указанной сущности. Возвращает их в виде плоского массива.
const children = await dataSource.manager
.getTreeRepository(Category)
.findDescendants(parentCategory)
// returns all direct subcategories (without its nested categories) of a parentCategory
  • findDescendantsTree - Получает всех потомков указанной сущности. Возвращает их в виде дерева с вложенной структурой.
const childrenTree = await repository.findDescendantsTree(parentCategory)
// returns all direct subcategories (with its nested categories) of a parentCategory
const childrenTreeWithLimitedDepth = await repository.findDescendantsTree(
parentCategory,
{ depth: 2 },
)
// returns all direct subcategories (with its nested categories) of a parentCategory, up to depth 2
  • createDescendantsQueryBuilder - Создает конструктор запросов для получения потомков сущностей в дереве.
const children = await repository
.createDescendantsQueryBuilder(
"category",
"categoryClosure",
parentCategory,
)
.andWhere("category.type = 'secondary'")
.getMany()
  • countDescendants - Возвращает количество потомков сущности.
const childrenCount = await dataSource.manager
.getTreeRepository(Category)
.countDescendants(parentCategory)
  • findAncestors - Получает всех предков указанной сущности. Возвращает их в виде плоского массива.
const parents = await repository.findAncestors(childCategory)
// returns all direct childCategory's parent categories (without "parent of parents")
  • findAncestorsTree - Получает всех предков указанной сущности. Возвращает их в виде дерева с вложенной структурой.
const parentsTree = await dataSource.manager
.getTreeRepository(Category)
.findAncestorsTree(childCategory)
// returns all direct childCategory's parent categories (with "parent of parents")
  • createAncestorsQueryBuilder - Создает конструктор запросов для получения предков сущностей в дереве.
const parents = await repository
.createAncestorsQueryBuilder("category", "categoryClosure", childCategory)
.andWhere("category.type = 'secondary'")
.getMany()
  • countAncestors - Возвращает количество предков сущности.
const parentsCount = await dataSource.manager
.getTreeRepository(Category)
.countAncestors(childCategory)

Следующие методы поддерживают передачу параметров:

  • findTrees

  • findRoots

  • findDescendants

  • findDescendantsTree

  • findAncestors

  • findAncestorsTree

Доступные параметры:

  • relations - Определяет, какие связи сущности должны быть загружены (упрощённая форма LEFT JOIN).

Примеры:

const treeCategoriesWithRelations = await dataSource.manager
.getTreeRepository(Category)
.findTrees({
relations: ["sites"],
})
// automatically joins the sites relation

const parentsWithRelations = await dataSource.manager
.getTreeRepository(Category)
.findAncestors(childCategory, {
relations: ["members"],
})
// returns all direct childCategory's parent categories (without "parent of parents") and joins the 'members' relation