Связь (Relation)
Поле типа Связь позволяет создавать связи между записями разных коллекций.
Основные характеристики
- Тип данных: Ссылка на запись в другой коллекции
- Режимы: Одна запись (One-to-One) или множество записей (One-to-Many)
- Обратная связь: Автоматическое создание обратного поля
- Каскадное удаление: Настраиваемое поведение при удалении
Типы связей
One-to-One (Один к одному)
Одна запись связана с одной записью в другой коллекции.
{
"name": "profile",
"type": "relation",
"collection": "profiles",
"multiple": false
}
Пример: Пользователь → Профиль
One-to-Many (Один ко многим)
Одна запись связана с множеством записей в другой коллекции.
{
"name": "orders",
"type": "relation",
"collection": "orders",
"multiple": true
}
Пример: Клиент → Заказы
Many-to-Many (Многие ко многим)
Множество записей связаны с множеством записей.
{
"name": "tags",
"type": "relation",
"collection": "tags",
"multiple": true,
"backReference": {
"field": "articles",
"multiple": true
}
}
Пример: Статьи ↔ Теги
Настройки поля
При создании поля типа "Связь" доступны следующие настройки:
- Название поля: Отображаемое имя поля
- Системное имя: Уникальный идентификатор для API
- Описание: Подсказка для пользователей
- Связанная коллекция: Коллекция для связи
- Множественный выбор: Разрешить выбор нескольких записей
- Обратная связь: Создать обратное поле в связанной коллекции
- Каскадное удаление: Удалять связанные записи при удалении
- Обязательное поле: Требовать выбора записи
Примеры использования
Автор статьи (One-to-One)
{
"name": "author",
"type": "relation",
"collection": "users",
"multiple": false,
"required": true
}
Заказы клиента (One-to-Many)
{
"name": "orders",
"type": "relation",
"collection": "orders",
"multiple": true,
"backReference": {
"field": "customer",
"multiple": false
}
}
Категория товара
{
"name": "category",
"type": "relation",
"collection": "categories",
"multiple": false,
"required": true
}
Теги статьи (Many-to-Many)
{
"name": "tags",
"type": "relation",
"collection": "tags",
"multiple": true,
"backReference": {
"field": "articles",
"multiple": true
}
}
Работа через API
Создание записи со связью
// Создать статью с автором
const article = await emd.database.collection('articles').create({
title: 'Моя статья',
author: 'user_123' // ID пользователя
});
// Создать заказ с товарами
const order = await emd.database.collection('orders').create({
customer: 'customer_456',
items: ['product_1', 'product_2', 'product_3']
});
Получение связанных данных
const article = await emd.database.collection('articles').get(articleId);
console.log(article.author);
// {
// id: "user_123",
// name: "Иван Иванов",
// email: "ivan@example.com"
// }
Получение с populate (развертывание связей)
const article = await emd.database.collection('articles')
.get(articleId, {
populate: ['author', 'category', 'tags']
});
console.log(article.author.name);
console.log(article.category.title);
console.log(article.tags.map(t => t.name));
Обновление связи
// Изменить автора
await emd.database.collection('articles').update(articleId, {
author: 'user_789'
});
// Добавить тег
const article = await emd.database.collection('articles').get(articleId);
await emd.database.collection('articles').update(articleId, {
tags: [...article.tags.map(t => t.id), 'tag_new']
});
Удаление связи
// Удалить связь (установить null)
await emd.database.collection('articles').update(articleId, {
author: null
});
// Удалить из массива связей
const article = await emd.database.collection('articles').get(articleId);
const updatedTags = article.tags
.filter(t => t.id !== 'tag_to_remove')
.map(t => t.id);
await emd.database.collection('articles').update(articleId, {
tags: updatedTags
});
Поиск по связям
Поиск по связанной записи
// Статьи определенного автора
const articles = await emd.database.collection('articles').find({
author: 'user_123'
});
// Товары определенной категории
const products = await emd.database.collection('products').find({
category: 'category_456'
});
Поиск по множественным связям
// Статьи с определенным тегом
const articles = await emd.database.collection('articles').find({
tags: { $contains: 'tag_javascript' }
});
// Статьи с любым из тегов
const articles = await emd.database.collection('articles').find({
tags: { $in: ['tag_js', 'tag_react', 'tag_vue'] }
});
// Статьи со всеми указанными тегами
const articles = await emd.database.collection('articles').find({
tags: { $all: ['tag_js', 'tag_tutorial'] }
});
Поиск по полям связанной записи
// Статьи автора с определенным email
const articles = await emd.database.collection('articles').find({
'author.email': 'ivan@example.com'
});
// Товары категории с определенным названием
const products = await emd.database.collection('products').find({
'category.name': 'Электроника'
});
Обратные связи
Автоматическое создание обратного поля
{
"collection": "articles",
"field": {
"name": "author",
"type": "relation",
"collection": "users",
"backReference": {
"field": "articles",
"multiple": true
}
}
}
Теперь у пользователя автоматически появится поле articles со всеми его статьями.
Доступ к обратной связи
const user = await emd.database.collection('users').get(userId, {
populate: ['articles']
});
console.log(user.articles);
// [
// { id: "article_1", title: "Статья 1" },
// { id: "article_2", title: "Статья 2" }
// ]
Каскадное удаление
Удалить связанные записи
{
"name": "comments",
"type": "relation",
"collection": "comments",
"multiple": true,
"onDelete": "cascade" // Удалить все комментарии при удалении статьи
}
Установить null
{
"name": "author",
"type": "relation",
"collection": "users",
"onDelete": "set_null" // Установить author = null при удалении пользователя
}
Запретить удаление
{
"name": "category",
"type": "relation",
"collection": "categories",
"onDelete": "restrict" // Запретить удаление категории, если есть товары
}
Фильтрация связанных записей
Ограничить выбор
{
"name": "category",
"type": "relation",
"collection": "categories",
"filter": {
"is_active": true,
"type": "product"
}
}
Сортировка связанных записей
{
"name": "comments",
"type": "relation",
"collection": "comments",
"multiple": true,
"sort": {
"created_at": -1
}
}
Вложенные связи
Получение вложенных данных
const order = await emd.database.collection('orders').get(orderId, {
populate: [
'customer',
{
path: 'items',
populate: ['product']
}
]
});
console.log(order.customer.name);
console.log(order.items[0].product.title);
Агрегация по связям
Подсчет связанных записей
const users = await emd.database.collection('users').aggregate([
{
$lookup: {
from: 'articles',
localField: '_id',
foreignField: 'author',
as: 'articles'
}
},
{
$addFields: {
articlesCount: { $size: '$articles' }
}
}
]);
Группировка по связанной записи
const stats = await emd.database.collection('articles').aggregate([
{
$group: {
_id: '$category',
count: { $sum: 1 }
}
}
]);
Производительность
Индексация
// Создать индекс для быстрого поиска
await emd.database.collection('articles').createIndex({
author: 1
});
Ленивая загрузка
// Получить только ID, без populate
const article = await emd.database.collection('articles').get(articleId);
console.log(article.author); // "user_123" (только ID)
// Загрузить данные автора по требованию
const author = await emd.database.collection('users').get(article.author);
Пагинация связанных записей
const user = await emd.database.collection('users').get(userId);
const articles = await emd.database.collection('articles')
.find({ author: userId })
.limit(10)
.skip(20);
Рекомендации
- Используйте связи вместо дублирования данных
- Создавайте индексы для полей связей
- Используйте populate только когда необходимо
- Для больших коллекций используйте пагинацию
- Настраивайте каскадное удаление осторожно
- Используйте обратные связи для удобства
- Документируйте структуру связей
Отличие от других типов
- Relation vs User: Relation для любых коллекций, User специфичен для пользователей
- Relation vs ObjectId: Relation предоставляет полные данные записи, ObjectId только ID
- Relation vs JSON: Relation для связей между коллекциями, JSON для произвольных данных