Объединения
Объединение - один список над несколькими моделями: лента из постов и рекламы, холст из разных фигур, смешанные результаты поиска. Используйте его, когда сущности разных видов текут через один экран, сохраняя свои поля и поведение.
Варианты идентифицируются ссылками на модели - никаких строк-«типов», которые нужно придумывать и держать уникальными.
Примеры на этой странице выполняются в активном скоупе - см. Коллекции и экземпляры.
Объявление объединения
import { union } from "@virentia/core/models";
const Timestamped = trait({
data: { createdAt: f.date().indexed("ord") },
});
const Post = staticModel({
with: [Timestamped],
data: { title: f.string(), likes: f.number(0) },
});
const Ad = staticModel({
with: [Timestamped],
data: { budget: f.number(0), active: f.boolean(true) },
});
const FeedItem = union(Post, Ad).by((json) =>
"budget" in json ? Ad : Post,
);
const feed = collection(FeedItem);by говорит объединению, какому варианту принадлежит JSON-объект. Union-коллекция - представление: экземпляры живут в collection(Post) и collection(Ad), и обе стороны видят одни и те же данные.
Добавление через объединение
feed.add({ title: "привет", likes: 3 }); // by → Post
feed.add({ budget: 100 }); // by → Ad
feed.count; // 2
collection(Post).count; // 1
collection(Ad).count; // 1Что происходит:
- неизвестные id идут через
by- дискриминация по форме на полных объектах; - известный id мержится в свой вариант напрямую, без
by- частичным обновлениям различительные ключи не нужны; - полный объект другого варианта с занятым id - ошибка: варианты не мигрируют;
- без
.by(...)у объединения нетadd- ни в типах, ни в рантайме.
Общие поля
Поле общее для объединения, когда каждый вариант получает его из одного трейта:
feed.where(FeedItem.createdAt.gte(today)).count;
feed.sort(FeedItem.createdAt.desc).items;Оба работают, потому что Post и Ad компонуют Timestamped. Каждый вариант использует свой индекс; отсортированные результаты сливаются между вариантами.
Совпадение имён - не общность: две модели, каждая со своим createdAt в data, не дают FeedItem.createdAt, и обращение к нему об этом скажет. Кладите общие поля в трейт.
Фильтр по вариантам: match
Вариант-специфичные условия идут через match - по ветке на вариант:
feed.match(
[Post, (p) => p.likes.gte(100)], // p - дескрипторы Post
[Ad, (a) => a.active.eq(true)],
);Ветка возвращает предикат либо true/false - оставить или выкинуть вариант целиком. match исчерпывающий: пропущенный вариант - ошибка компиляции (сообщение его называет) и ошибка рантайма. match свободно сочетается в цепочке с where, sort и take:
feed
.where(FeedItem.createdAt.gte(today))
.match([Post, (p) => p.likes.gte(10)], [Ad, () => false])
.sort(FeedItem.createdAt.desc)
.take(20);Сужение элементов
Результаты типизированы объединением экземпляров вариантов; in сужает:
for (const item of feed.items) {
if ("likes" in item) {
item.likes.value; // Post
} else {
item.budget.value; // Ad
}
}Объединение в связях
Целью связи может быть объединение - значением или санком:
const Bookmark = staticModel({
data: { target: refs.one(FeedItem) },
});
bookmark.target.value = post; // экземпляр любого варианта
bookmark.json().target; // idКогда связь записывают экземпляром, вариант известен точно - всё остаётся надёжным, даже когда два варианта делят одну строку id. Id, загруженный из JSON, разрешается поиском по вариантам при первом чтении; если он есть в двух - чтение падает с понятной ошибкой: по одному только id вариант не определить.
Дети с union-целью встраивают JSON каждого ребёнка, а by ре-дискриминирует их на загрузке.
Контракт
function union(...variants: Model[]): Union;
// дискриминатор входа: json типизирован как Dto<V1> | Dto<V2> | ...
// пока .by(...) не объявлен, у объединения нет add - ни в типах, ни в рантайме
union.by((json) => Variant): Union;
// union-коллекция - Query по всем вариантам, плюс:
feed.add(json) // маршрут через by / мерж по известному id
feed.get(id) // ищет по вариантам
feed.remove(id)
feed.match(...[Variant, (descriptors) => Predicate | boolean][])Общие дескрипторы (FeedItem.field) существуют для полей, которые каждый вариант несёт из одного трейта.
Частые кейсы
Используйте объединения для:
- лент и таймлайнов, смешивающих виды сущностей;
- холстов и деревьев слоёв из разнородных элементов;
- поиска сразу по нескольким моделям;
- полиморфных ссылок -
refs.one(FeedItem)вместо поля-тега типа.
Связанные разделы
- Трейты - источник общих полей.
- Запросы и индексы - всё, что наследует union-запрос.
- Связи - цели связей в целом.