Урок 04

Документы и коллекции

Что такое документ

Документ — BSON-объект, аналог строки в SQL.

{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "email": "alice@example.com",
  "name": "Алиса",
  "age": 30,
  "tags": ["vip", "premium"],
  "address": {
    "city": "Москва",
    "street": "Ленина, 5"
  },
  "created_at": ISODate("2026-10-15T10:00:00Z")
}

Ключевое:

  • Вложенные объекты.
  • Массивы.
  • Разные типы.
  • Не требуется одинаковый набор полей для всех документов коллекции.

Ограничения документа

  • 16 MB — максимум документа.
  • _id — обязательное поле, уникальное.
  • Имена полей — не начинаются с $, не содержат . и \0.
  • Глубина вложенности — обычно до 100 уровней.

Что делать при >16 MB:

  • GridFS — разбивает файлы на чанки.
  • Реструктуризация — вынести часть в другую коллекцию.

Коллекция

Аналог таблицы, но без схемы.

  • Создаётся неявно при первой вставке.
  • Или явно — db.createCollection().
  • Валидация — опционально ($jsonSchema).
db.users.insertOne({ name: 'Алиса' })   // коллекция создана
db.createCollection('products')          // явно

Типы данных BSON

Основные:

  • string — UTF-8.
  • int32, int64 — целые.
  • double — 8 байт.
  • decimal128 — точные числа (деньги).
  • bool — true / false.
  • date — timestamp (мс).
  • null.
  • array — массив любых значений.
  • object — вложенный документ.
  • ObjectId — 12 байт.
  • binData — бинарные данные.
  • regex — регулярка.

Специфичные:

  • Decimal128 — точные дроби.
  • Timestamp — внутренний, для oplog.
  • MinKey / MaxKey — сортировочные крайности.

Вставка

Один:

db.users.insertOne({
    email: 'alice@example.com',
    name: 'Алиса',
    age: 30,
})

Сколько:

const result = db.users.insertOne({ ... })
result.insertedId   // ObjectId

Много:

db.users.insertMany([
    { email: 'a@b.com', name: 'Алиса' },
    { email: 'c@d.com', name: 'Боб' },
], { ordered: false })

ordered: false — продолжать даже при ошибке (например, дубликат _id).

С указанием _id:

db.users.insertOne({
    _id: new ObjectId(),
    email: 'x@y.com'
})

Правило: не задавать _id вручную без причины — ObjectId генерируется автоматически.

Чтение

Все:

db.users.find()

Один:

db.users.findOne({ email: 'alice@example.com' })

По _id:

db.users.findOne({ _id: ObjectId('507f1f77bcf86cd799439011') })

Проекция полей:

db.users.find({}, { email: 1, name: 1, _id: 0 })
// _id всегда включается, если не убрать явно

Лимит и скип:

db.users.find().limit(10).skip(20)

Сортировка:

db.users.find().sort({ created_at: -1 })
// 1 — ascending, -1 — descending

Обновление

Один:

db.users.updateOne(
    { _id: userId },
    { $set: { name: 'Новое имя' } }
)

Несколько:

db.users.updateMany(
    { age: { $lt: 18 } },
    { $set: { is_minor: true } }
)

Upsert:

db.users.updateOne(
    { email: 'a@b.com' },
    { $set: { name: 'Алиса' } },
    { upsert: true }
)

Что даёт: insert, если не найдено; update, если найдено.

Операторы обновления

$set — установить значение:

{ $set: { 'address.city': 'Питер' } }   // вложенное поле

$unset — удалить поле:

{ $unset: { legacy_field: '' } }

$inc — увеличить:

{ $inc: { login_count: 1 } }
{ $inc: { balance: -100 } }

$push — добавить в массив:

{ $push: { tags: 'new_tag' } }
{ $push: { tags: { $each: ['a', 'b'] } } }

$pull — удалить из массива:

{ $pull: { tags: 'old_tag' } }

$addToSet — добавить, если нет:

{ $addToSet: { tags: 'php' } }

$pop — удалить с конца (-1) или начала (1):

{ $pop: { tags: 1 } }

$rename — переименовать:

{ $rename: { 'old_name': 'new_name' } }

$currentDate — текущая дата:

{ $currentDate: { updated_at: true } }

$min / $max — только если меньше/больше:

{ $min: { min_price: 100 } }

$mul — умножить:

{ $mul: { price: 1.1 } }

Массивы

Добавить:

{ $push: { tags: 'php' } }
{ $push: { tags: { $each: ['php', 'laravel'], $position: 0 } } }
{ $push: { tags: { $each: ['a', 'b'], $slice: 5 } } }   // максимум 5
{ $push: { tags: { $each: ['a', 'b'], $sort: 1 } } }    // сортировать

Удалить:

{ $pull: { tags: 'php' } }
{ $pull: { tags: { $in: ['php', 'js'] } } }

Заменить элемент по индексу:

{ $set: { 'tags.0': 'ruby' } }

Обновить все элементы:

{ $set: { 'scores.$[]': 0 } }

Обновить элементы по условию:

{ $set: { 'scores.$[elem]': 0 } }
// с arrayFilters:
{ arrayFilters: [{ 'elem': { $lt: 0 } }] }

Удаление

Один:

db.users.deleteOne({ _id: userId })

Много:

db.users.deleteMany({ is_inactive: true })

Очистить коллекцию:

db.users.deleteMany({})

⚠️ deleteMany({}) удаляет всё, но не коллекцию.

_id

По умолчанию: ObjectId.

Можно свой тип:

db.users.insertOne({ _id: 'user-123', name: 'X' })
db.users.insertOne({ _id: 42, name: 'Y' })

⚠️ _id неизменяем. После вставки — только удалить документ.

Правило: для обычных случаев — ObjectId. Для ссылок из внешних систем — свой _id (slug, external_id).

Вложенные документы

Доступ через точку:

db.users.updateOne(
    { _id: userId },
    { $set: { 'address.city': 'Питер' } }
)

db.users.find({ 'address.city': 'Питер' })

Когда вкладывать:

  • Всегда читается вместе с родителем.
  • 1:1 или 1:мало.
  • Не нужен поиск по вложенному в других контекстах.

Когда отдельная коллекция:

  • Растёт неограниченно (иначе упрёшься в 16 MB).
  • Часто обновляется отдельно.
  • Нужен поиск и пагинация.

Пример: заказ

{
  "_id": ObjectId("..."),
  "order_number": "ORD-2026-00001",
  "user_id": ObjectId("..."),
  "items": [
    {
      "product_id": ObjectId("..."),
      "name": "iPhone 15",
      "price": NumberDecimal("99990.00"),
      "quantity": 1
    }
  ],
  "subtotal": NumberDecimal("99990.00"),
  "total": NumberDecimal("99990.00"),
  "status": "pending",
  "shipping": {
    "country": "RU",
    "city": "Москва",
    "street": "Ленина, 5"
  },
  "created_at": ISODate("2026-10-15T10:00:00Z")
}

Что здесь:

  • items — массив, а не отдельная коллекция. Позиции всегда читаются с заказом.
  • shipping — вложенный документ, snapshot адреса.
  • NumberDecimal — точные цены.

Коллекция vs отдельная

Вложенная коллекция нужна, если:

  • 1:мало (не растёт бесконечно).
  • Всегда читается вместе.
  • Атомарные обновления важны.

Отдельная коллекция, если:

  • Растёт неограниченно (например, сообщения в чате).
  • Нужен поиск по ней независимо.
  • Обновляется отдельно.
  • Используется в других контекстах.

Правило: документ — aggregate (целостная сущность). Разбивай, если части живут отдельной жизнью.

Массовые операции

Bulk write:

db.users.bulkWrite([
    { insertOne: { document: { email: 'a@b.com', name: 'Алиса' } } },
    { updateOne: {
        filter: { email: 'c@d.com' },
        update: { $set: { name: 'Боб' } },
    } },
    { deleteOne: { filter: { email: 'x@y.com' } } },
])

Что даёт: одна операция для нескольких изменений. Быстрее, чем по одной.

Порядок и ordered

db.users.insertMany([...], { ordered: true })   // default
// Остановится на первой ошибке

db.users.insertMany([...], { ordered: false })
// Продолжит, соберёт ошибки

Правило: для миграций и bulk — ordered: false.

Частые ошибки

1. Всё в один документ:

Растёт бесконечно → 16 MB лимит

Решение: отдельная коллекция.

2. Нет _id в проекции:

db.users.find({}, { email: 1 })
// _id всё равно вернётся

Решение: явно { _id: 0 }.

3. deleteMany({}) вместо drop():

Медленнее, индексы остаются

Решение: db.collection.drop().

4. Обновление через полную замену:

db.users.replaceOne({ _id }, { name: 'X' })
// стёрлись все остальные поля

Решение: $set.

5. Дубликаты через updateMany без upsert:

Несколько одинаковых документов

Решение: unique-индекс.

6. Хранение файлов в документе:

Быстро упрёшься в 16 MB

Решение: GridFS или S3.

7. find без индекса:

Collection scan

Решение: explain() + индекс.

Практика

1. Создайте коллекцию products:

db.products.insertMany([
    {
        sku: 'SKU001',
        name: 'iPhone 15',
        price: NumberDecimal('99990.00'),
        tags: ['phone', 'apple'],
        attributes: { color: 'black', storage: 256 },
    },
    {
        sku: 'SKU002',
        name: 'Galaxy S24',
        price: NumberDecimal('79990.00'),
        tags: ['phone', 'samsung'],
        attributes: { color: 'white', storage: 128 },
    },
])

2. Найдите товары с тегом apple:

db.products.find({ tags: 'apple' })

3. Обновите цену:

db.products.updateOne(
    { sku: 'SKU001' },
    { $set: { price: NumberDecimal('89990.00') }, $currentDate: { updated_at: true } }
)

4. Добавьте тег:

db.products.updateOne({ sku: 'SKU001' }, { $addToSet: { tags: 'sale' } })

5. Найдите по вложенному:

db.products.find({ 'attributes.color': 'black' })

6. Создайте индекс на tags:

db.products.createIndex({ tags: 1 })

Итог

  • Документ — BSON-объект, до 16 MB.
  • Коллекция — без схемы, но с валидацией.
  • _id — обязательное, неизменяемое.
  • $set, $inc, $push, $pull, $addToSet — операторы.
  • Вложенность — если всегда читается вместе.
  • Отдельная коллекция — если растёт или поиск отдельно.
  • ordered: false — для bulk.
  • GridFS — для файлов > 16 MB.

Дальше — CRUD в MongoDB.