# Чого ви навчитеся
- Зрозумієте структуру документів BSON та MongoDB.
- Виконаєте основні CRUD-операції з документами.
- Оберете між вбудовуванням та посиланнями.
- Ефективно змоделюєте зв’язки один до одного, один до багатьох та багато до багатьох.
Для цього курсу необхідні базові знання Laravel.
# Що таке BSON?
Binary JSON (BSON) — це бінарне кодування документів, подібних до JSON, в MongoDB. Воно містить чітку інформацію про типи та довжину, що забезпечує швидке проходження та ефективне зберігання порівняно з простим JSON.
BSON є форматом, що використовується на диску та в мережі, тоді як у коді зазвичай працюють зі структурами, схожими на JSON.
BSON – це матеріал успіху MongoDB.
Приклад BSON
# Типи даних BSON
BSON розширює JSON додатковими типами даних, такими як дата, ObjectId та бінарні дані.
| Тип даних | Опис | Приклад |
|-----------|-------------|---------|
| String | UTF-8 рядок | `"John Doe"` |
| Integer | Ціле число 32 або 64 біта | `28` |
| Double | Число з плаваючою комою 64 біти | `3.14` |
| Boolean | true/false | `true` |
| Date | Дата у форматі UTC | `ISODate("2025-01-15")` |
| ObjectId | Унікальний 12-байтовий ідентифікатор | `ObjectId("507f...")` |
| Array | Список значень | `["tag1", "tag2"]` |
| Object | Вбудований документ | `{"city": "NY"}` |
| Null | Порожнє значення | `null` |
# Анатомія документа
Ось приклад документа BSON з наведеними вище полями даних.
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "John Doe",
"email": "john@example.com",
"age": 28,
"created_at": ISODate("2025-01-15T10:30:00Z"),
"profile": {
"bio": "Software developer",
"avatar": "avatar.jpg"
},
"tags": ["developer", "laravel", "mongodb"]
}
# Що таке колекція?
Колекція — це група документів. На відміну від SQL-таблиць, документи в одній колекції не повинні мати ідентичну схему.
Порівняння термінів MongoDB та SQL:
| MongoDB | SQL |
|---|---|
| Колекція | Таблиця |
| Документ | Рядок |
| Поле | Стовпець |
# Основи моделювання даних
MongoDB пропонує два основних підходи до зв’язків між даними: вбудовування та посилання.
# Вбудовування
Вбудовування зберігає пов'язані дані в одному документі.
Основне правило: Дані, що отримуються разом, слід зберігати разом.
Приклад — інформація профілю, вбудована в документ користувача:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"username": "john.doe123",
"email": "john@example.com",
"profile": {
"full_name": "John Doe",
"bio": "Software developer and tech enthusiast",
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=123e4567",
"website": "https://example.com",
"location": "San Francisco, USA"
}
}
// app/Models/User.php
// У цьому прикладі використовуються фабрики бази даних Laravel разом із Faker для генерації тестових даних.
...
'profile' => [
'full_name' => $firstName . ' ' . $lastName,
'bio' => fake()->sentence(10),
'avatar' => 'https://api.dicebear.com/7.x/avataaars/svg?seed=' . fake()->uuid(),
'website' => fake()->url(),
'location' => fake()->city() . ', ' . fake()->country(),
],
...
// app/Models/UserController.php
...public function updateProfile(Request $request)
{
$validated = $request->validate([
'full_name' => 'sometimes|string|max:100',
'bio' => 'sometimes|string|max:500',
'website' => 'sometimes|url',
'location' => 'sometimes|string|max:100',
]);
$user = auth()->user();
$user->update([
'profile' => array_merge($user->profile, $validated),
]);
}
Ще один приклад вбудовування — статистика користувача:
{
"_id": ObjectId("507f1f77bcf86cd799439012"),
"username": "jane.smith456",
"stats": {
"posts_count": 42,
"followers_count": 1250,
"following_count": 87
}
}
// app/Models/User.php
...
'stats' => [
'posts_count' => 0,
'followers_count' => 0,
'following_count' => 0,
],
...
Переваги
- Один запит для отримання злагоджених даних
- Одне атомарне оновлення для всього блоку
Недоліки
- Великі документи можуть перевищити ліміт у 16 МБ (великі масиви або вбудовані документи можуть перевищити цей ліміт).
- Можливе дублювання, коли одні й ті ж дані зустрічаються в інших місцях.
# Посилання
Посилання з’єднує документи з різних колекцій за допомогою їхніх ID. Це відповідає іноземним ключам у реляційних базах даних.
Приклад зв’язку користувачів у контакті та доступу
Приклад: пости посилаються на користувачів. Пости можуть постійно зростати, тому посилання зберігає документи користувачів малими та незалежними.
{
"_id": ObjectId("507f1f77bcf86cd799439013"),
"user_id": ObjectId("507f1f77bcf86cd799439011"),
"content": "My awesome post!",
"stats": {
"likes_count": 342,
"comments_count": 28
}
}
// app/Models/Post.php
// Опис моделі
protected $fillable = [
'user_id', // Посилання на користувача
'content',
'media',
'stats',
'tags',
];
// Відношення
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
// UserController.php
...public function show(User $user)
{
$user->load(['posts' => function($q) {
$q->orderBy('created_at', 'desc')->limit(10);
}]);
$isFollowing = in_array(
$user->_id,
auth()->user()->following_ids ?? []
);
return view('users.show', compact('user', 'isFollowing'));
}
Коментарі посилаються як на пост, так і на користувача:
{
"_id": ObjectId("507f1f77bcf86cd799439014"),
"post_id": ObjectId("507f1f77bcf86cd799439013"),
"user_id": ObjectId("507f1f77bcf86cd799439012"),
"parent_id": null,
"content": "Great post! I totally agree.",
"likes_count": 15,
"created_at": ISODate("2024-11-03T10:30:00Z"),
"updated_at": ISODate("2024-11-03T10:30:00Z")
}
// app/Models/Post.php
// Опис моделі
protected $fillable = [
'post_id', // Посилання на пост
'user_id', // Посилання на користувача
'parent_id', // Посилання на батьківський коментар
'content',
'likes_count',
'created_at',
'updated_at',
];
// Відносини
public function post()
{
return $this->belongsTo(Post::class, 'post_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
Загалом, посилання часто використовуються у відносинах багато до багатьох.
Переваги
- Уникає дублювання в різних колекціях
- Зберігає документи малими, підтримує незалежні запити
Недоліки
- Вимагає виконання кількох запитів або $lookup з’єднань для агрегацій
# Взаємозв'язки моделей
# Один до одного
Якщо дані завжди отримуються разом, використовуйте вбудовування.
// app/Models/Post.php
'stats' => [
'likes_count' => fake()->numberBetween(0, 1000),
'comments_count' => fake()->numberBetween(0, 50),
'shares_count' => fake()->numberBetween(0, 20),
],
// Приклад документа
{
"_id": ObjectId("507f1f77bcf86cd799439013"),
"content": "My awesome post!",
"stats": {
"likes_count": 342,
"comments_count": 28,
"shares_count": 5
}
}
# Один до багатьох
Варіанти кардинальності один до багатьох
- Один до кількох: невеликі, обмежені набори; краще використовувати вбудовування
{
"media": [
{ "type": "image", "url": "..." },
{ "type": "image", "url": "..." },
{ "type": "video", "url": "..." }
]
...
}
- Один до багатьох (обмежений): Вбудовування, якщо елементи обмежені та отримуються разом з батьком; приклад: відповіді, обмежені 50 на коментар.
Comment::where('parent_id', $commentId)->get()
- Один до багатьох (необмежений): Посилання, якщо елементи зростають без обмежень або запитуються незалежно; приклад: коментарі до постів.
// User.php
public function comments()
{
return $this->hasMany(Comment::class, 'user_id');
}
// Comment.php
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
- Один до непомірних: Коли зв’язок може зрости до тисяч чи мільйонів, завжди використовуйте посилання з "багатьох" сторін на "одну"; приклад: лайки на вірусному пості.
{
"_id": ObjectId("507f1f77bcf86cd799439015"),
"user_id": ObjectId("507f1f77bcf86cd799439012"),
"likeable_type": "App\\Models\\Post",
"likeable_id": ObjectId("507f1f77bcf86cd799439013")
}
// Запит на лайки: Пагіновані лайки на пост
Like::where('likeable_type', Post::class)
->where('likeable_id', $postId)
->paginate(20);
# Багато до багатьох
Це зазвичай вирішується через посилання; приклад: взаємини з підписками.
class User extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['name', 'email', 'following_ids'];
protected $casts = [
'following_ids' => 'array'
];
}
// Підписатися на користувача
$user->push('following_ids', $targetUserId);
// Відписатися
$user->pull('following_ids', $targetUserId);
// Отримати користувачів, на яких підписано
$following = User::whereIn('_id', $user->following_ids)->get();
Зверніть увагу: багато до багатьох можна також змоделювати за допомогою окремої колекції при необхідності зберігати метадані, такі як часові позначки або статуси.
Додатковий приклад (поза кейсом):
Продукти ↔ Теги з колекцією-з’єднанням
Коли вам потрібні метадані про саму взаємозв'язок, створіть окрему колекцію-з’єднання.
- Колекції: продукти, теги, product_tags
- Метадані зв'язку: хто додав тег, коли, та необов'язковий бал релевантності
// продукти
{ _id: ObjectId("6560..."), name: "UltraSoft Hoodie", sku: "HD-001" }
// теги
{ _id: ObjectId("6561..."), name: "winter" }
{ _id: ObjectId("6562..."), name: "sale" }
// product_tags (колекція-з’єднання)
{
_id: ObjectId("6570..."),
product_id: ObjectId("6560..."),
tag_id: ObjectId("6561..."),
added_by: ObjectId("user123..."),
added_at: ISODate("2025-10-15T09:12:00Z"),
relevance: 0.87
}
// app/Models/ProductTag.php
class ProductTag extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['product_id', 'tag_id', 'added_by', 'added_at', 'relevance'];
protected $casts = [
'added_at' => 'datetime',
'relevance' => 'float',
];
}
// Прикріплення тега з метаданими
ProductTag::create([
'product_id' => $productId,
'tag_id' => $tagId,
'added_by' => auth()->id(),
'added_at' => now(),
'relevance' => 0.87,
]);
Цей шаблон узагальнюється на інші сфери, як-от:
- студенти ↔ курси (з оцінкою і семестром).
- користувачі ↔ організації (з роллю і датою початку).
- автори ↔ публікації (з порядком і відсотком внеску).
# Вбудовування проти посилання
| Фактор | Вбудовування | Посилання |
|---|---|---|
| Можливості зростання | Невелика кількість | Нескінченні |
| Шаблон доступу | Разом | Незалежно |
| Розмір документа | Залишатися малим | Ставати великим |
| Частота оновлення | Рідко | Часто |
# Висновок
Вибір між вбудовуванням і посиланнями залежить від кардинальності, зростання, шаблонів доступу та частоти оновлення.
Почніть з аналізу того, як ваша програма зчитує дані. Якщо дані завжди отримуються разом і мають обмеження, використовуйте вбудовування. Якщо зростання необмежене або елементи запитуються незалежно, використовуйте посилання.
Моделюйте відповідно до вашого робочого навантаження, перевіряйте на реальних запитах і вдосконалюйте процес, як тільки ваша програма розвивається.
# Додаткові ресурси
- Взаємозв’язки документів MongoDB
- Моделювання даних MongoDB
- Навчальний бедж з моделі реляцій до документу MongoDB
Проект соціальної мережі, що використовує наведені вище концепції (весь вихідний код для цього уроку)
# Відео-демонстрація:
Сподіваюсь, цей урок допоможе вам зрозуміти основи MongoDB на прикладі програми соціальної мережі. Бажаю успіхів у навчанні!