Работа со связями (ссылками) между записями heap-таблиц
При моделировании данных часто возникает задача объявления в таблице поля-ссылки, которое содержит указатель на другую запись в другой или этой же (если моделируется древовидная иерархия, например) heap-таблицы. Этого можно добиться простым хранением идентификатора записи в виде строкового поля. Однако, heap-таблицы поддерживают 2 специальных типа полей:
- Heap.RefLink — ссылка на запись заранее заданной таблицы.
- Heap.GenericLink — ссылка на запись любой таблицы.
Использование этих типов предоставляет следующие удобства:
- Принимают на вход heap-объекты, а не только идентификаторы.
- Валидидация идентификатора и типа (для RefLink) записи таблицы.
- Контроль целостности (отключаемый): невозможно удалить запись, на которую "ссылается" другая запись.
- В runtime значения таких полей представлены экземплярами специальных классов с поддержкой метода .get(ctx) , который позволяет удобно прочитать запись, на которую ссылаемся.
- Упрощает создание автоматических интерфейсов редактирования записей таблицы (админок).
- Облегчает чтение кода за счёт точного семантического обозначения предназначения поля.
Описание и способы объявления RefLink
RefLink - полный аналог внешних ключей (foreign keys) в реляционных СУБД и позволяет моделировать связи между сущностями. В отличие от реляционных БД, поле типа RefLink может быть не только на верхнем уровне таблицы, но и на любой глубине во вложенных объектах и массивах.
При объявлении поля с помощью функции Heap.RefLink необходимо указать heap-таблицу, на записи которой будут указывать значения поля. Это можно сделать одним из двух способов:
-
Непосредственно передать репозиторий целевой таблицы. Это рекомендованный способ. Он более эффективный с точки зрения внутренней реализации и с ним меньше шансов ошибиться. Кроме того, это единственный доступный способ, если таблица объявлена в коде другого аккаунта. Но с помощью этого способа невозможно создать циклические ссылки.
import { Heap } from '@app/heap' import { Products } from '@external/heap' const Deals = Heap.Table('deals', { product: Heap.RefLink(Products), }) -
Указать строку с названием целевой таблицы. Этот способ рекомендуется использовать только в случае циклических ссылок, например, когда необходимо смоделировать иерархию с помощью поля
parent, которое "смотрит" на свою же таблицу.import { Heap } from '@app/heap' const Tree = Heap.Table('tree', { parent: Heap.Nullable(Heap.RefLink('tree')), })
В силу своей динамической природы, поля-ссылки не поддерживают значения по умолчанию. При добавлении в существующие непустые heap-таблицы, их следует оборачивать в Heap.Optional или Heap.Nullable.
При объявлении RefLink важно использовать непосредственно импортированный объект
Heap, то есть нельзя использовать производную переменную с другим именем или переданную как аргумент в функцию.
Это необходимо, т.к. на этапе сборки исходных текстов специальный трансформер находит такие поля и "дописывает" служебную информацию для корректной работы.
Контроль ссылочной целостности
Использование полей типа RefLink и GenericLink автоматически включает контроль ссылочной целостности. Это означает, что система по умолчанию не даст удалить запись, на которую ссылается какое-либо RefLink- или GenericLink-поле.
Если по каким-либо причинам такое поведение мешает и разработчик "понимает что делает", то его можно выключить для каждого поля в отдельности, передав опцию onDelete: 'none' в последнем аргументе при объявлении поля (подробнее об опции см. Heap.RefLink и Heap.GenericLink).
Следует понимать, что при выключении контроля целостности значения поля могут содержать ссылки на несуществующие записи, и использование метода RefLink.get или getById будут бросать runtime-исключения, которые, скорее всего, будут неожиданными.
Поддержка
onDelete: 'cascade'иonDelete: 'set-null', которые обычно присутствуют в реляционных СУБД, для heap-таблиц не реализована.
GenericLink и его отличия от RefLink
GenericLink во многом похож на RefLink, но применим в гораздо более узком наборе сценариев. Примеры:
- Поле в таблице инцидентов службы поддержки, которое может содержать ссылку на объект в информационной системе компании, для которого создан этот инцидент.
- Поле "набор ссылок" в таблица документов в системе документооборота компании, которое содержит список ссылок на разные объекты в информационной системе, относящиеся к данному документу.
Особенности записи значений GenericLink
В отличие от RefLink, у которого информация о типе (таблице) записи, на которую ссылаемся, содержится в схеме таблицы, в GenericLink эта информация хранится непосредственно в значении поля рядом с идентификатором записи. Поэтому в операциях create/update значения поля типа GenericLink нельзя передавать в виде простого идентификатора, поддерживается только передача самого объекта, на который ссылаемся, либо экземпляра класса GenericLink, полученного при выборке ранее.
const Products = Heap.Table('products', {
refLink: Heap.RefLink(SomeTable),
genLink: Heap.GenericLink(),
})
const someRecord = await SomeTable.findOneBy(ctx)
await Products.create(ctx, {
// OK
refLink: someRecord.id,
// ошибка!
genLink: someRecord.id,
// правильно:
genLink: someRecord,
})
Особенности работы со значениями GenericLink
Поскольку при выборке поля типа GenericLink в общем случае код "не знает", какого типа запись, на которую ссылаемся, работа со значениями GenericLink всегда несколько сложнее, чем со значениями RefLink, и содержит ветвление в зависимости от типа конкретной записи.
Тут на помощь приходят свойства и методы вспомогательного класса GenericLink, экземплярами которого представлены значения. Ветвление можно огранизовать следующими способами:
- С помощью свойств
GenericLink.typeиHeapTableRepo.type.Сравнивая значения свойстваtype GenericLink-полясо свойствомtypeодной из возможных целевых таблиц, можно определить таблицу, к которой относится идентификатор, хранящийся в поле GenericLink и использовать метод getById для получения целевой записи:
import { Table1, Table2 } from './tables'
const Products = Heap.Table('products', {
genLink: Heap.GenericLink(),
})
const product1 = await Products.findOneBy(ctx)
if (product1.genLink.type === Table1.type) {
const linkRecord = await Table1.getById(ctx, product1.genLink.id)
// ... Table1 related logic
} else if (product1.genLink.type === Table2.type){
const linkRecord = await Table2.getById(ctx, product1.genLink.id)
// ... Table2 related logic
}
- С помощью методов
GenericLink.get()иHeapTableRepo.isMyRecord(). МетодGenericLink.get()достаточно "умный", чтобы найти нужную таблицу и осуществить выборку записи этой таблицы. Однако, чтобы "понять", запись какого типа в итоге возвращена, всё равно необходимо "вручную" перебирать варианты, используя метод isMyRecord, который является ещё и type-предикатом и "приводит" запись к правильному типу на уровне typescript-кода:
import { Table1, Table2 } from './tables'
const Products = Heap.Table('products', {
genLink: Heap.GenericLink(),
})
const product1 = await Products.findOneBy(ctx)
const linkRecord = await product1.genLink.get(ctx)
if (Table1.isMyRecord(linkRecord)) {
// ... Table1 related logic
} else if (Table2.isMyRecord(linkRecord)){
// ... Table2 related logic
}
Моделирование связей много-ко-многим
В отличие от реляционных баз данных, для моделирования отношений многие-ко-многим не обязательно создавать отдельную heap-таблицу. В большинстве случаев гораздо проще будет объявить "массив ссылок":
import { Heap } from '@app/heap'
const Tags = Heap.Table('tags', {
name: Heap.String(),
})
const Products = Heap.Table('products', {
tags: Heap.Array(Heap.RefLink(tags)),
})
const product1 = await Products.findOneBy(ctx)
const product1Tags = await Promise.all(
product1.tags.map(link => link.get(ctx))
)
Фильтрация
Поля-ссылки поддерживают фильтрацию:
-
По идентификатору целевой записи:
import { Table1, Table2 } from './tables' const Products = Heap.Table('products', { refLink: Heap.RefLink(Table1), genLink: Heap.GenericLink(), }) const table1Record = await Table1.findOneBy(ctx) const table2Record = await Table2.findOneBy(ctx) await Products.findBy(ctx, { refLink: table1Record.id, genLink: table2Record.id, }) -
По списку идентификаторов (как
INв SQL):await Products.findBy(ctx, { refLink: [table1Record.id], genLink: [table1Record.id, table2Record.id], }) -
Для GenericLink поддерживается ещё и фильтрация по типу (целевой таблице) и по списку типов:
await Products.findBy(ctx, { genLink: { type: Table1.type }, }) await Products.findBy(ctx, { genLink: { type: [Table1.type, Table2.type] }, })
При фильтрации по идентификатору в качестве значений фильтра принимаются:
-
Непосредственно строковой идентификатор (примеры выше).
-
Экземпляр RefLink или GenericLink.
const product = await Table1.findOneBy(ctx) await Products.findBy(ctx, { refLink: product.refLink, genLink: product.genLink, }) -
Целевая запись целиком.
await Products.findBy(ctx, { refLink: table1Record, genLink: table2Record, }) -
Любую комбинацию вышеперечисленного в списке.
await Products.findBy(ctx, { refLink: [table1Record.id, product.refLink, table1Record], genLink: [table1Record.id, product.refLink, table2Record], })
Операторы сравнения для полей-ссылок не поддерживаются.
Операторы$not,$and,$orи$noopработают как и везде.
