При моделировании данных часто возникает задача объявления в таблице поля-ссылки, которое содержит указатель на другую запись в другой или этой же (если моделируется древовидная иерархия, например) heap-таблицы. Этого можно добиться простым хранением идентификатора записи в виде строкового поля. Однако, heap-таблицы поддерживают 2 специальных типа полей:
Использование этих типов предоставляет следующие удобства:
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, но применим в гораздо более узком наборе сценариев. Примеры:
В отличие от 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 всегда несколько сложнее, чем со значениями 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
работают как и везде.