MongoDB
Документная модель
MongoDB хранит данные в формате BSON (Binary JSON). Каждый документ -- самодостаточная единица данных, которая может содержать вложенные объекты и массивы.
Документ vs Строка
<?php
declare(strict_types=1);
/**
* Document model: embed related data in one document
*/
// Relational approach: 3 tables, 2 JOINs
// orders -> order_items -> products
// Document approach: one document
$orderDocument = [
'_id' => 'ord_abc123',
'user' => [
'id' => 'user_456',
'name' => 'John Doe',
'email' => '[email protected]',
],
'items' => [
[
'product_id' => 'prod_1',
'name' => 'Laptop',
'price' => 999.99,
'quantity' => 1,
],
[
'product_id' => 'prod_2',
'name' => 'Mouse',
'price' => 29.99,
'quantity' => 2,
],
],
'total_amount' => 1059.97,
'status' => 'completed',
'shipping_address' => [
'street' => '123 Main St',
'city' => 'Moscow',
'zip' => '101000',
],
'created_at' => new \DateTimeImmutable(),
];
Когда embedding, когда referencing
| Стратегия | Когда использовать | Пример |
|---|---|---|
| Embedding | Данные читаются вместе, 1:1, 1:few | Адрес в заказе |
| Referencing | Данные читаются отдельно, many:many | Пользователь в заказе |
| Hybrid | Embedding часто используемых полей | Имя пользователя + reference на полный профиль |
Архитектура MongoDB
Replica Set
Группа серверов MongoDB с автоматическим failover:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Primary │────>│ Secondary │────>│ Secondary │
│ │ │ │ │ │
│ Reads + │ │ Reads │ │ Reads │
│ Writes │ │ (optional) │ │ (optional) │
│ │ │ │ │ │
│ oplog ─────┼─────┤ oplog │ │ oplog │
└─────────────┘ └─────────────┘ └─────────────┘
│
Election при
падении Primary
- Primary -- принимает все записи
- Secondary -- реплицирует данные, может обслуживать чтения
- Arbiter -- участвует только в голосовании (не хранит данные)
Sharding
Горизонтальное распределение данных по нескольким серверам:
┌──────────┐
│ mongos │ (Router)
│ (router) │
└────┬─────┘
│
┌────┴──────────────────────────────────┐
│ Config Servers │
│ (metadata, chunk mapping) │
└────┬──────────────┬───────────────┬───┘
│ │ │
┌────┴────┐ ┌─────┴────┐ ┌─────┴────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ RS │ │ RS │ │ RS │
│ │ │ │ │ │
│user_id │ │user_id │ │user_id │
│ A-F │ │ G-O │ │ P-Z │
└─────────┘ └──────────┘ └──────────┘
Shard Key Selection
| Тип shard key | Плюсы | Минусы |
|---|---|---|
| Hashed | Равномерное распределение | Нет range queries |
| Ranged | Range queries эффективны | Hotspots возможны |
| Compound | Баланс между распределением и запросами | Сложнее выбрать |
Правило: shard key выбирается один раз и не может быть изменён без пересоздания коллекции. Выбирайте ключ с высокой кардинальностью и частым использованием в запросах.
CRUD операции из PHP
<?php
declare(strict_types=1);
/**
* MongoDB CRUD operations with PHP driver
*/
final class MongoOrderRepository
{
private \MongoDB\Collection $collection;
public function __construct(
\MongoDB\Client $client,
string $database = 'shop',
) {
$this->collection = $client->selectCollection($database, 'orders');
}
/**
* Create indexes
*/
public function ensureIndexes(): void
{
$this->collection->createIndexes([
['key' => ['user.id' => 1, 'created_at' => -1]],
['key' => ['status' => 1]],
['key' => ['created_at' => 1], 'expireAfterSeconds' => 86400 * 365], // TTL: 1 year
]);
}
/**
* Insert document
*/
public function create(array $orderData): string
{
$result = $this->collection->insertOne([
'user' => $orderData['user'],
'items' => $orderData['items'],
'total_amount' => $orderData['total_amount'],
'status' => 'pending',
'created_at' => new \MongoDB\BSON\UTCDateTime(),
]);
return (string) $result->getInsertedId();
}
/**
* Find with filters
*/
public function findByUser(string $userId, int $limit = 20): array
{
$cursor = $this->collection->find(
['user.id' => $userId],
[
'sort' => ['created_at' => -1],
'limit' => $limit,
'projection' => [
'user.email' => 0, // Exclude sensitive data
],
]
);
return $cursor->toArray();
}
/**
* Atomic update with operators
*/
public function updateStatus(string $orderId, string $newStatus): bool
{
$result = $this->collection->updateOne(
['_id' => new \MongoDB\BSON\ObjectId($orderId)],
[
'$set' => [
'status' => $newStatus,
'updated_at' => new \MongoDB\BSON\UTCDateTime(),
],
'$push' => [
'status_history' => [
'status' => $newStatus,
'changed_at' => new \MongoDB\BSON\UTCDateTime(),
],
],
]
);
return $result->getModifiedCount() > 0;
}
/**
* Bulk write operations
*/
public function bulkUpdateStatuses(array $orderStatuses): int
{
$operations = array_map(
fn(array $item) => [
'updateOne' => [
['_id' => new \MongoDB\BSON\ObjectId($item['id'])],
['$set' => ['status' => $item['status']]],
],
],
$orderStatuses
);
$result = $this->collection->bulkWrite($operations);
return $result->getModifiedCount();
}
}
Aggregation Pipeline
Aggregation pipeline -- мощный инструмент для обработки и анализа данных в MongoDB:
<?php
declare(strict_types=1);
/**
* MongoDB Aggregation Pipeline examples
*/
final class OrderAnalytics
{
public function __construct(
private readonly \MongoDB\Collection $orders,
) {}
/**
* Revenue by category per month
*/
public function revenueByCategory(int $year): array
{
$pipeline = [
// Stage 1: Filter by year
['$match' => [
'created_at' => [
'$gte' => new \MongoDB\BSON\UTCDateTime(mktime(0, 0, 0, 1, 1, $year) * 1000),
'$lt' => new \MongoDB\BSON\UTCDateTime(mktime(0, 0, 0, 1, 1, $year + 1) * 1000),
],
'status' => 'completed',
]],
// Stage 2: Unwind items array
['$unwind' => '$items'],
// Stage 3: Group by month and category
['$group' => [
'_id' => [
'month' => ['$month' => '$created_at'],
'category' => '$items.category',
],
'revenue' => ['$sum' => ['$multiply' => ['$items.price', '$items.quantity']]],
'order_count' => ['$sum' => 1],
]],
// Stage 4: Sort
['$sort' => ['_id.month' => 1, 'revenue' => -1]],
// Stage 5: Reshape output
['$project' => [
'_id' => 0,
'month' => '$_id.month',
'category' => '$_id.category',
'revenue' => ['$round' => ['$revenue', 2]],
'order_count' => 1,
]],
];
return $this->orders->aggregate($pipeline)->toArray();
}
/**
* Top customers by spending
*/
public function topCustomers(int $limit = 10): array
{
$pipeline = [
['$match' => ['status' => 'completed']],
['$group' => [
'_id' => '$user.id',
'name' => ['$first' => '$user.name'],
'total_spent' => ['$sum' => '$total_amount'],
'order_count' => ['$sum' => 1],
'avg_order' => ['$avg' => '$total_amount'],
'last_order' => ['$max' => '$created_at'],
]],
['$sort' => ['total_spent' => -1]],
['$limit' => $limit],
];
return $this->orders->aggregate($pipeline)->toArray();
}
}
Consistency Levels
MongoDB предлагает настраиваемый уровень консистентности:
| Write Concern | Описание | Производительность |
|---|---|---|
w: 0 |
Fire and forget | Максимальная |
w: 1 |
Подтверждение от primary | Высокая |
w: "majority" |
Подтверждение от большинства | Средняя |
| Read Concern | Описание |
|---|---|
local |
Данные из primary (могут быть откачены) |
majority |
Данные, подтверждённые большинством |
linearizable |
Самые актуальные, подтверждённые данные |
Для финансовых операций:
writeConcern: majority+readConcern: majority. Для логов и метрик:w: 1достаточно.
Когда использовать MongoDB
Подходит
- Каталоги продуктов с переменными атрибутами
- CMS с гибкой структурой контента
- IoT данные с разной схемой от разных устройств
- Прототипирование с быстро меняющейся схемой
Не подходит
- Финансовые транзакции с многотабличными JOIN
- Сложная аналитика (лучше ClickHouse)
- Данные с жёсткой нормализованной схемой
- Когда нужны foreign key constraints
Итоги
- Документная модель подходит для данных с переменной структурой
- Embedding vs Referencing -- ключевое решение при моделировании
- Replica Set обеспечивает высокую доступность и автоматический failover
- Sharding масштабирует запись горизонтально, но выбор shard key критичен
- Aggregation Pipeline -- мощный инструмент аналитики внутри MongoDB
- Используйте MongoDB когда гибкость схемы важнее строгих транзакций