Zaawansowane Aspekty Mongoose: Relacje

MongoDB, jako baza danych NoSQL, przechowuje dane w formie dokumentów, które mogą być zagnieżdżone lub połączone poprzez referencje. Mongoose, jako popularna biblioteka do obsługi MongoDB w Node.js, oferuje dwie główne metody tworzenia relacji między dokumentami: embeddowanie (czyli zagnieżdżanie dokumentów) oraz referencje (czyli odwołania do innych dokumentów). W tym artykule omówimy oba podejścia, pokażemy przykłady ich implementacji oraz wskażemy, kiedy warto stosować każde z nich.

Embeddowanie dokumentów: Schematy zagnieżdżone

Embeddowanie polega na zagnieżdżaniu jednego dokumentu wewnątrz innego dokumentu. Jest to metoda wygodna i wydajna, ponieważ przechowuje dane razem, co pozwala na szybkie odczytywanie informacji bez dodatkowych operacji łączenia.

Przykład embeddowania dokumentów

Załóżmy, że tworzymy aplikację, w której mamy kolekcję User, a każdy użytkownik ma kolekcję Address przechowującą jego adresy. Możemy zagnieździć dokument Address bezpośrednio w dokumencie User.

				
					const mongoose = require('mongoose');

// Schemat zagnieżdżony dla adresu
const addressSchema = new mongoose.Schema({
  street: String,
  city: String,
  zipCode: String
});

// Schemat użytkownika, który zawiera adres jako zagnieżdżony dokument
const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  addresses: [addressSchema]  // Pole addresses jest tablicą zagnieżdżonych dokumentów Address
});

const User = mongoose.model('User', userSchema);

				
			

Dzięki powyższej strukturze możemy przechowywać wiele adresów użytkownika bez potrzeby tworzenia osobnej kolekcji Address.

Przykład dodania użytkownika z zagnieżdżonym adresem

				
					const newUser = new User({
  name: 'Jan Kowalski',
  email: 'jan@example.com',
  addresses: [
    { street: 'Kwiatowa 5', city: 'Warszawa', zipCode: '00-001' },
    { street: 'Ogrodowa 12', city: 'Kraków', zipCode: '30-001' }
  ]
});

newUser.save()
  .then(() => console.log('Użytkownik zapisany z zagnieżdżonymi adresami'))
  .catch(error => console.log('Błąd zapisu:', error));

				
			

Powyższy kod tworzy użytkownika z dwoma adresami zapisanymi w jednym dokumencie User.

Kiedy używać embeddowania?

Embeddowanie dokumentów jest korzystne, gdy:

  • Dane są ze sobą mocno powiązane i są zwykle pobierane razem (np. adresy użytkownika),
  • Dokument zagnieżdżony jest mały i nie zmienia się niezależnie od głównego dokumentu.

Referencje: Relacje typu „populate”

Referencje polegają na przechowywaniu identyfikatora jednego dokumentu w innym dokumencie, zamiast pełnych danych. Dzięki temu możemy połączyć dokumenty między kolekcjami, a Mongoose oferuje funkcję populate(), która pozwala na automatyczne ładowanie danych z referencji.

Przykład tworzenia referencji

Załóżmy, że mamy kolekcje User i Post, w której przechowujemy posty użytkowników. W takim przypadku lepiej jest przechowywać referencje do użytkownika w dokumencie Post niż zagnieżdżać je bezpośrednio, co umożliwi nam pobranie wszystkich postów użytkownika w przyszłości.

				
					// Schemat użytkownika
const userSchema = new mongoose.Schema({
  name: String,
  email: String
});

const User = mongoose.model('User', userSchema);

// Schemat posta z referencją do użytkownika
const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }  // Referencja do użytkownika
});

const Post = mongoose.model('Post', postSchema);

				
			

Przykład dodania posta z referencją do użytkownika

Najpierw utworzymy użytkownika, a następnie dodamy post z referencją do tego użytkownika:

				
					const newUser = new User({
  name: 'Anna Nowak',
  email: 'anna@example.com'
});

newUser.save()
  .then(user => {
    const newPost = new Post({
      title: 'Mój pierwszy post',
      content: 'To jest zawartość posta',
      author: user._id  // Referencja do użytkownika
    });
    return newPost.save();
  })
  .then(() => console.log('Post zapisany z referencją do użytkownika'))
  .catch(error => console.log('Błąd zapisu:', error));

				
			

Pobieranie danych za pomocą populate()

Aby pobrać dane posta wraz z informacją o autorze, używamy metody .populate():

				
					Post.find()
  .populate('author')  // Ładuje dane autora do pola author w dokumencie Post
  .then(posts => console.log('Posty z załadowanymi autorami:', posts))
  .catch(error => console.log('Błąd pobierania:', error));

				
			

Dzięki populate() dane autora są automatycznie dołączane do wyników, co pozwala na wygodne odczytanie pełnych informacji o poście i jego autorze.

Kiedy używać referencji?

Referencje są szczególnie przydatne, gdy:

  • Dane są niezależne i mogą być pobierane osobno (np. użytkownik i jego posty),
  • Chcemy przechowywać duże zbiory danych powiązanych z danym dokumentem (np. posty użytkownika),
  • Dane często się zmieniają i wymagają niezależnych aktualizacji.

Różnice między embeddowaniem a referencjami i kiedy ich używać

EmbeddowanieReferencje
Dane przechowywane są bezpośrednio wewnątrz dokumentuPrzechowywany jest identyfikator dokumentu innej kolekcji
Pobieranie danych jest szybsze (bez dodatkowych zapytań)Pobieranie wymaga dodatkowego zapytania (populate())
Nadaje się do danych, które są często pobierane razem (np. adresy użytkownika)Nadaje się do danych, które są często pobierane osobno (np. posty użytkownika)
Idealne dla małych, niezależnych zestawów danychIdealne dla dużych, powiązanych zestawów danych
Aktualizacja dokumentów zagnieżdżonych wymaga aktualizacji całego dokumentuKażdy dokument może być aktualizowany niezależnie

Podsumowując:

  • Embeddowanie jest świetne do przechowywania małych, nierozłącznych danych, które są często używane razem z głównym dokumentem.
  • Referencje sprawdzają się, gdy mamy do czynienia z dużymi zestawami danych, które są często pobierane osobno i wymagają niezależnych aktualizacji.

Podsumowanie

Zarówno embeddowanie, jak i referencje są przydatne, w zależności od wymagań aplikacji i typu danych:

  • Embeddowanie jest szybkie i pozwala na przechowywanie małych, powiązanych danych.
  • Referencje umożliwiają tworzenie bardziej elastycznych struktur danych, które można pobierać niezależnie i skalować.

Wybierając między tymi metodami, należy kierować się rozmiarem danych, ich powiązaniami i częstotliwością dostępu. Dzięki temu nasze aplikacje będą wydajne, elastyczne i łatwe w zarządzaniu.