← Назад к вопросам

Можно ли реализовать связи между коллекциями в MongoDB?

2.2 Middle🔥 151 комментариев
#Базы данных и SQL#Кэширование и NoSQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Связи между коллекциями в MongoDB

Краткий ответ

Да, в MongoDB можно реализовать связи между коллекциями несколькими способами, хотя MongoDB — это документоориентированная БД и работает иначе, чем реляционные БД с внешними ключами.

Основные подходы

1. Embedding (встраивание документов)

Простейший способ — встроить связанные данные прямо в документ:

// Коллекция: users
db.users.insertOne({
    _id: ObjectId("123"),
    name: "Иван",
    email: "ivan@example.com",
    // Вложенный документ
    address: {
        city: "Москва",
        street: "Тверская",
        zipcode: "101000"
    },
    // Вложенный массив
    phones: ["123-456", "789-101"]
})

Плюсы:

  • Быстро: вся информация в одном документе
  • Простая конструкция
  • Атомарные обновления

Минусы:

  • Дублирование данных
  • Сложность обновления в нескольких местах

2. Referencing (ссылки по ID)

Другой способ — хранить ID связанного документа:

// Коллекция: users
db.users.insertOne({
    _id: ObjectId("user123"),
    name: "Иван",
    addressId: ObjectId("addr456")  // Ссылка на address
})

// Коллекция: addresses
db.addresses.insertOne({
    _id: ObjectId("addr456"),
    city: "Москва",
    street: "Тверская"
})

Плюсы:

  • Нет дублирования
  • Гибко для сложных связей

Минусы:

  • Требует несколько запросов
  • Сложнее в запросах

3. $lookup — JOIN операция (MongoDB 3.2+)

Dля связывания коллекций в одном запросе используй $lookup:

// Получить всех пользователей с их адресами
db.users.aggregate([
    {
        $lookup: {
            from: "addresses",           // Какую коллекцию присоединить
            localField: "addressId",     // Поле в текущей коллекции
            foreignField: "_id",         // Поле в присоединяемой коллекции
            as: "address"                // Как назвать результат
        }
    }
])

Результат:

{
    _id: ObjectId("user123"),
    name: "Иван",
    addressId: ObjectId("addr456"),
    address: [  // Результат $lookup
        {
            _id: ObjectId("addr456"),
            city: "Москва",
            street: "Тверская"
        }
    ]
}

Примеры со связями один-ко-многим

Вариант 1: Embedding

// Коллекция: orders (заказы с товарами внутри)
db.orders.insertOne({
    _id: ObjectId("order123"),
    userId: ObjectId("user456"),
    date: ISODate("2025-03-22"),
    items: [  // Товары встроены
        { productId: ObjectId("prod1"), quantity: 2, price: 100 },
        { productId: ObjectId("prod2"), quantity: 1, price: 50 }
    ],
    total: 250
})

Вариант 2: Referencing с $lookup

// Коллекция: orders
db.orders.insertOne({
    _id: ObjectId("order123"),
    userId: ObjectId("user456"),
    itemIds: [ObjectId("item1"), ObjectId("item2")]  // Ссылки
})

// Коллекция: orderItems
db.orderItems.insertMany([
    { _id: ObjectId("item1"), productId: ObjectId("prod1"), quantity: 2 },
    { _id: ObjectId("item2"), productId: ObjectId("prod2"), quantity: 1 }
])

// Запрос с $lookup
db.orders.aggregate([
    { $match: { _id: ObjectId("order123") } },
    {
        $lookup: {
            from: "orderItems",
            localField: "itemIds",
            foreignField: "_id",
            as: "items"
        }
    }
])

Сложные примеры со связями много-ко-многим

// Коллекция: students
db.students.insertOne({
    _id: ObjectId("student1"),
    name: "Петр",
    courseIds: [ObjectId("course1"), ObjectId("course2")]  // Несколько курсов
})

// Коллекция: courses
db.courses.insertOne({
    _id: ObjectId("course1"),
    title: "Java Basics"
})

// Получить студента со всеми его курсами
db.students.aggregate([
    { $match: { _id: ObjectId("student1") } },
    {
        $lookup: {
            from: "courses",
            localField: "courseIds",
            foreignField: "_id",
            as: "courses"  // Получим массив курсов
        }
    }
])

На Java с Spring Data MongoDB

import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;

public class UserRepository {
    @Autowired
    private MongoTemplate mongoTemplate;
    
    public List<UserWithAddress> getUsersWithAddresses() {
        // Создаём $lookup операцию
        LookupOperation lookup = LookupOperation.newLookup()
            .from("addresses")
            .localField("addressId")
            .foreignField("_id")
            .as("address");
        
        // Выполняем aggregation
        Aggregation aggregation = Aggregation.newAggregation(
            lookup,
            Aggregation.unwind("address")  // Разворачиваем массив
        );
        
        return mongoTemplate.aggregate(
            aggregation,
            "users",
            UserWithAddress.class
        ).getMappedResults();
    }
}

// Модели
@Document(collection = "users")
public class User {
    @Id
    private ObjectId id;
    private String name;
    private ObjectId addressId;  // Ссылка
}

public class UserWithAddress {
    private ObjectId id;
    private String name;
    private Address address;  // Заполнится из $lookup
}

Когда использовать какой подход?

ПодходКогда использоватьПример
EmbeddingСвязанные данные часто используются вместеАдрес внутри User, товары внутри Order
ReferencingДанные используются независимоUser и Order (обновляются отдельно)
$lookupЧитаем связанные данные редкоАналитика, отчёты

Ограничения MongoDB

❌ Нет внешних ключей (Foreign Keys) как в SQL

❌ Нет каскадного удаления автоматически

❌ $lookup может быть медленнее, чем SQL JOIN

✅ Но есть flexibility — выбираешь сам структуру

Транзакции между коллекциями (MongoDB 4.0+)

Для атомарных операций через несколько коллекций:

Session session = mongoTemplate.getSession();
session.startTransaction();

try {
    // Обновляем user
    mongoTemplate.updateFirst(
        new Query(Criteria.where("_id").is(userId)),
        new Update().set("balance", balance - 100),
        User.class
    );
    
    // Добавляем запись о платеже
    mongoTemplate.insert(new Payment(userId, 100));
    
    session.commitTransaction();
} catch (Exception e) {
    session.abortTransaction();
    throw e;
} finally {
    session.close();
}

Вывод

✅ Да, связи в MongoDB возможны несколькими способами

Embedding — для часто используемых вместе данных

Referencing + $lookup — для сложных связей

❌ MongoDB не заменяет SQL для сложных транзакций

✅ Выбирай подход в зависимости от структуры данных и паттернов доступа