Moduł 3 - Zajęcia 5 - Baza danych Mongo DB
1.1 Podstawy MongoDB
Współczesnej aplikacji webowej potrzebny jest nieulotny magazyn danych. Bardzo długo w tym celu wykorzystywana była prawie zawsze baza danych SQL, ale już od dłuższego czasu istnieją alternatywy w postaci baz NoSQL.
Tego rodzaju bazy danych wzięły pod uwagę rozproszoną naturę sieci i podobnie jak Node.js, skupiły się na deserializacji i decentralizacji w celu skalowania wydajności. Przeanalizujemy jedną z najpopularniejszych, zorientowaną na dokumenty, bazę danych MongoDB.
MongoDB jest nierelacyjną bazą danych typu NoSQL. Taka baza opiera się na modelu dokumentów (documents) - obiektów danych - przechowywanych w zbiorach nazywanych kolekcjami (collections).
1.2 Kolekcje i dokumenty
Dane w MongoDB grupują się w kolekcjach. Kolekcja to zbiór dokumentów o określonej nazwie. Kolekcja podobna jest do tabeli w bazie danych SQL, ale odróżnia się tym, że dla kolekcji nie musimy określać ścisłego schematu danych, więc poszczególne dokumenty kolekcji mogą mieć różną strukturę.
Dokument to pojedynczy obiekt zawierający informację w bazie danych. Mogą one składać się z zagnieżdżonych obiektów i ten model danych jest bardzo wygodny dla aplikacji webowych. Maksymalny rozmiar pojedynczego dokumentu w MongoDB jest ograniczony do 16 MB.
W Mongo DB jako język zapytań wykorzystuje się JavaScript i struktury JSON. Wybór języka zapytań nie jest zaskoczeniem, biorąc pod uwagę to, że MongoDB wykorzystuje format JSON do przedstawienia dokumentów i przesyłania wyników.
Fizycznie struktury JSON przechowywane są w binarnym formacie BSON.
Dokumenty (tj. obiekty) mogą więc zawierać dobrze nam znane typy danych z JavaScript i formatu JSON (ale nie tylko)
W poniżej tabeli przedstawione są analogiczne koncepty i terminologia dla SQL i MongoDB.
| SQL | MongoDB |
| baza danych | baza danych |
| tabela | kolekcja |
| łańcuch | dokument |
| kolumna | pole |
| indeks | indeks |
| joins | $lookup |
1.3 Klucz główny
W SQL musimy ustawić jedną kolumnę lub kombinację kolumn jako klucz główny (primary key) który identyfikuje w unikalny sposób dany rekord w danej tablicy.
W MongoDB klucz główny automatycznie ustawia się jako pole _id. Zmienna _id to obiekt typu ObjectId.
_id: ObjectId('5f15996fbbde793a107af359');
Każdy ObjectId() zawiera 12 bajtów, które dzielą się na następujące grupy:
- 4 - bajtowa wartość (5f15996f) oznaczająca sekundy, zaczynając od poprzedniego zapisu ObjectId;
- 3 - bajtowa wartość (bbde79) oznaczająca identyfikator maszyny na której znajduje się baza;
- 2 - bajtowa wartość (3a10) oznaczająca identyfikator procesu MongoDB;
- 3 - bajtowy licznik (7af359), zaczynający się od losowej wartości.
1.4 MongoDB Atlas
Zamiast instalować naszą bazę danych lokalnie, będziemy wykorzystywać usługę w chmurze MongoDB Atlas.
MongoDB Atlas to platforma oferująca usługę zarządzanej bazy danych MongoDB w chmurze, z darmowym poziomem klastra bazy.
Przejdź rejestrację, najlepiej przez swoje konto Google.
Utwórz swój pierwszy klaster - Build a Database
Wybierz darmową opcję M0
W tym samym kroku:
- Wybierz providera (nie ma to większego znaczenia na potrzeby naszych ćwiczeń)
- Wybierz najbliższy dla siebie region geograficzny, dla którego dostępny jest darmowy tier M0.
- Podaj nazwę klastra (możesz zostawić domyślne Cluster0)
Następnie kliknij na Create, jeśli musisz wypełnij Captche
W kolejnym kroku musisz podać nazwę użytkownika i hasło które zostanie wykorzystane do połączenia z bazą danych
Pamiętaj, żeby skopiować i zapisać hasło, później będziesz musiał je zresetować jeśli je zgubisz
Kliknij na Create User i gotowe, teraz powinieneś móc zobaczyć swój klaster na stronie głównej (Deployment → Database w nawigacji bocznej)
Następnym krokiem będzie skonfigurowanie dostępu do bazy, przejdźmy więc do zakładki Security → Network Access
Tutaj możesz dodać swój adres ip (bądź dowolną ich ilość), lub ewentualnie zezwolić na dostęp z dowolnego dresu ustawiając wartość IP 0.0.0.0/0 (to rozwiązanie nie jest zalecane)
Jeśli potrzebujesz utworzyć kolejnego użytkownika dla naszej bazy danych, możesz przejść do zakładki Security → Database Access i kliknąć na Add New Database User.
Wybierz jako metodę uwierzytelnienia hasło. Wymyśl nazwę i hasło użytkownika i zapamiętaj je, umieścimy je dalej w zmiennych środowiskowych naszego programu.
Ustaw prawa użytkownika na odczytywanie i zapisywanie w dowolnej bazie danych.
Wstępna konfiguracja jest zakończona i teraz można wrócić do zakładki Database. Tutaj wybieramy klikamy w przycisk Connect przy naszym klastrze.
W wyskakującym okienku wybieramy Drivers
Następnie wybieramy odpowiednio Driver Node.js i wersję 4.1 or later
Jak widzisz w kroku 3 podany jest specjalny string który pozwoli na połączenie się z bazą danych
Musimy pamiętać żeby zamienić w nim dwa symbole zastępcze:
- <username> - nazwa użytkownika którego stworzyliśmy
- <password> - hasło tego użytkownika
Przy pomocy tego stringa (nazywanego SRV) będziemy łączyć się z naszą bazą danych w chmurze.
2.1 MongoDB GUI
Istnieje wiele graficznych narzędzi zarządzania MongoDB. Te programy zwiększają ułatwiają projektowanie i administrowanie bazą danych. Są wygodne i często dostarczają również wewnętrzną konsolę do pracy z bazą danych. Takie oprogramowanie nazywamy też czasem "klientem bazy danych"
Przeanalizujemy dwa popularne narzędzia graficzne dla MongoDB i dowiemy się, jak podłączyć się w nich do bazy danych w chmurze. Te dwa narzędzia nie tylko w pełni wystarczą na potrzeby kursu, ale są również zupełnie wystarczające do pracy komercyjnej.
2.2 MongoDB Compass link
Graficzny interfejs dla MongoDB od jego twórców. Dostarcza dobrą wizualizację danych i pełną funkcjonalność CRUD. Dostępny jest dla platform Linux, Mac lub Windows.
Podłączenie do bazy danych następuje przy pomocy SRV, otwieramy więc naszego klienta bazy, wklejamy nasze SRV z uzupełnionym hasłem i nazwą użytkownika i klikamy na Save & Connect, a następnie podajemy nazwę i kolor pod którymi chcemy zapisać dane połączenie na później
Po połączeniu zobaczymy mniej więcej to:
Po lewej będzie menu z naszymi bazami i kolekcjami, możemy tam utworzyć naszą pierwszą bazę danych. Po wyborze bazy i kolekcji dostaniemy się do panelu zarządzania kolekcją.
Oprócz konsoli dla poleceń MONGOSH, Compass, dostarcza on panelu do wyszukiwania i filtrowania po naszych dokumentach oraz pełną funkcjonalność dla operacji CRUD na dokumentach, bez potrzeby wykorzystania czystego języka zapytań. Compass jest dość prosty ale przez to bywa ograniczony i utrudnia bardziej skomplikowane operacje.
2.3 Robo 3T link
Popularny, bezpłatny klient MongoDB. To dosyć zaawansowane narzędzie z otwartym kodem źródłowym. Dostępne jest na wielu platformach. Zostało zaprojektowane przez 3T Software, firmę stojącą za Studiem 3T, płatną i bardziej rozbudowana wersją Robo 3T
Przy starcie pojawi się okno pokazujące zapisane połączenia do bazy danych i umożliwiające utworzenie nowego przez przycisk Create (na górze okienka)
Nasz string SRV możemy wkleić do okienka obok przycisku From URI, który następnie wciskamy, a Robo3T samo odczyta wprowadzone w nim hasło i użytkownika oraz host naszej bazy danych
Następnie możemy przetestować nasze połączenie klikając na Test w dolnym lewym rogu okienka, jeśli testy przejdą poprawnie, możemy zapisać nasze połączenie (podaj swoją nazwę w polu Name)
Potem z listy naszych połączeń wybieramy to świeżo dodane i zobaczymy następujący interfejs
Jest on nieco podobny do poprzedniego programu, ale zamiast paneli na górze widzimy konsolę w której wprowadzamy zapytania do bazy danych. Na przykład pokazanie zawartości kolekcji dogs to:
db.getCollection('dogs').find({});
Wykonanie poleceń zachodzi przy pomocy klawisza F5 lub kombinację klawiszy Ctrl+Enter (Command+R dla MacOS).
W tym i w następnym module będziemy wykorzystywać Robo3T do pracy z Mongo.
3.1 Podstawowe komendy MongoDB
Po podłączeniu do bazy w chmurze MongoDB kliknij prawym przyciskiem myszy na dowolną bazę w lewym panelu Robo 3T. W menu kontekstowym wybierz Open shell - otworzy się konsola, w której możemy pracować.
Pierwsze polecenie to:
use test
Pamiętaj aby wykonać to polecenie przy użyciu odpowiedniego skrótu klawiszowego
Jeżeli chcemy dowiedzieć się, jaka baza danych jest w tym momencie wykorzystywana, możemy użyć polecenia:
db
Wykorzystując polecenie db.stats(), można otrzymać statystykę wybranej bazy danych, możemy też doprecyzować, że chodzi nam tylko o statystyki kolekcji dogs
db.dogs.stats();
3.2 Dodanie dokumentów do kolekcji
Można w tym celu wykorzystać kilka metod, między innymi:
- insertOne: dodaje jeden dokument;
- insertMany: dodaje kilka dokumentów;
- insert: może dodawać zarówno jeden, jak i więcej dokumentów.
Kliknijmy najpierw na bazę test (rozwijając ja), potem na Collections, a następnie na kolekcję dogs. W konsoli wykonajmy polecenie
db.dogs.insertOne({
name: 'burek',
age: 3,
features: ['zjada kapcie', 'pozwala się głaskać', 'rudy'],
});
Otrzymamy rezultat zbliżony do:
{
"acknowledged": true,
"insertedId": ObjectId("5f837f855ba83a4f1829ca5b")
}
Oznacza on, że podany dokument dodany został do kolekcji dogs. Podwójnym kliknięciem na nazwę dogs kolekcji w lewym menu otwieramy nową zakładkę w konsoli, gdzie automatycznie wykonuje się polecenie.
db.getCollection('dogs').find({});
Istnieje kilka ograniczeń przy tworzeniu nazw kluczy w dokumentach:
- Symbol $ nie może być pierwszym symbolem w nazwie klucza.
- Nazwa klucza nie może zawierać symbolu kropki .
- Nazwa _id jako zazwyczaj zarezerwowana nie jest rekomendowana do ręcznego ustawiania.
Dodajmy teraz dwa dokumenty naraz
db.dogs.insertMany([
{
name: 'Mamba',
age: 2,
features: ['poluje na samochody', 'szczerzy zęby', 'czarna'],
},
{
name: 'Pirat',
age: 4,
features: ['ma tylko jedno oko', 'pozwala się głaskać', 'kudłaty'],
},
]);
Rezultat odpowiedzi będzie podobny do tego - zwrócono nam dwa unikalne identyfikatory w ponownie utworzonych dokumentach:
{
"acknowledged": true,
"insertedIds": [
ObjectId("5f837f895ba83a4f1829ca5c"),
ObjectId("5f837f895ba83a4f1829ca5d")
]
}
Moglibyśmy też wykonać podobne polecenie przy użyciu komendy insert
db.dogs.insert([
{
name: 'Azor',
age: 10,
features: ['świszczy'],
},
{
name: 'Kajtek',
age: 15,
features: ['zjada trawę', 'poluje na Azora'],
},
]);
Po tym działaniu w naszej kolekcji powinno znajdować się pięć dokumentów
3.3 Szukanie w zbiorze
Dla odnalezienia dokumentów wykorzystuje się metodę find:
db.dogs.find();
Wynikiem tego działania powinno być coś podobnego (twoje _id będą odmienne) do:
/* 1 */
{
"_id" : ObjectId("64596ed4e50efc2ba61c4bee"),
"name" : "burek",
"age" : 3.0,
"features" : [
"zjada kapcie",
"pozwala się głaskać",
"rudy"
]
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bef"),
"name" : "Mamba",
"age" : 2.0,
"features" : [
"poluje na samochody",
"szczerzy zęby",
"czarna"
]
}
/* 3 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bf0"),
"name" : "Pirat",
"age" : 4.0,
"features" : [
"ma tylko jedno oko",
"pozwala się głaskać",
"kudłaty"
]
}
/* 4 */
{
"_id" : ObjectId("64597065e50efc2ba61c4bf3"),
"name" : "Azor",
"age" : 10.0,
"features" : [
"świszczy"
]
}
/* 5 */
{
"_id" : ObjectId("64597065e50efc2ba61c4bf4"),
"name" : "Kajtek",
"age" : 15.0,
"features" : [
"zjada trawę",
"poluje na Azora"
]
}
W MongoDB w zapytaniach można wykorzystać między innymi następujące konstrukcje przy pomocy operatorów porównania:
- $eq (równo)
- $gt (więcej niż)
- $lt (mniej niż)
- $gte (więcej lub równo)
- $lte (mniej lub równo)
db.dogs.find({ age: { $lte: 5 }, features: 'pozwala się głaskać' });
Wynik:
/* 1 */
{
"_id" : ObjectId("64596ed4e50efc2ba61c4bee"),
"name" : "burek",
"age" : 3.0,
"features" : [
"zjada kapcie",
"pozwala się głaskać",
"rudy"
]
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bf0"),
"name" : "Pirat",
"age" : 4.0,
"features" : [
"ma tylko jedno oko",
"pozwala się głaskać",
"kudłaty"
]
}
3.4 Project
Zdarza się, że potrzebujemy tylko niektóre pola z odnalezionych dokumentów, lub chcemy niektóre z nich ominąć z wyników, możemy do tego użyć konstrukcji projekcji w następujący sposób:
db.dogs.find({ age: { $lte: 5 }, features: 'pozwala się głaskać' }, { name: 0 });
Jako drugi argument metody find podaliśmy obiekt project który usunął z wyników wyszukiwania pole name (dotyczy to tylko aktualnych wyników wyszukiwania, nie wpływa to w żaden sposób na oryginalne dokumenty)
/* 1 */
{
"_id" : ObjectId("64596ed4e50efc2ba61c4bee"),
"age" : 3.0,
"features" : [
"zjada kapcie",
"pozwala się głaskać",
"rudy"
]
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bf0"),
"age" : 4.0,
"features" : [
"ma tylko jedno oko",
"pozwala się głaskać",
"kudłaty"
]
}
Użyjmy teraz tego obiektu odwrotnie, czyli zostawimy tylko dwa niezbędne pola:
db.dogs.find(
{ age: { $lte: 5 }, features: 'pozwala się głaskać' },
{ name: 1, age: 1 }
);
Wynik:
/* 1 */
{
"_id" : ObjectId("64596ed4e50efc2ba61c4bee"),
"name" : "burek",
"age" : 3.0
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bf0"),
"name" : "Pirat",
"age" : 4.0
}
Trzeba wspomnieć, że pole _id domyślnie zawsze jest zwracane, chyba, że celowo je ominiemy poprzez właściwość w obiekcie projekcji: _id: 0.
3.5 Zapytania o zagnieżdżone właściwości obiektów
Wstawmy nowy dokument z właściwością owner o wartości obiektu:
db.dogs.insert({
name: 'Klusia',
age: 10,
features: ['w kształcie kadłuba', 'szara'],
owner: { name: 'Jola', age: 27, address: 'Kraków' },
});
Aby wyszukiwać zgodnie z wartością zagnieżdżonej wartości, trzeba użyć kropki i cudzysłowiu: 'owner.name':
db.dogs.find({ 'owner.name': 'Jola' });
Wynik:
{
"_id" : ObjectId("64597228e50efc2ba61c4bf6"),
"name" : "Klusia",
"age" : 10.0,
"features" : [
"w kształcie kadłuba",
"szara"
],
"owner" : {
"name" : "Jola",
"age" : 27.0,
"address" : "Kraków"
}
}
3.6 Dodatkowa konfiguracja zapytań:
Aby ograniczyć ilość zwróconych wyników, wykorzystuje się funkcję limit. Na przykład aby pokazać pierwsze trzy dokumenty w kolekcji wywołamy ją tak:
db.dogs.find().limit(3);
Aby ominąć jakąś ilość dokumentów w wyborze, wykorzystuje się funkcję skip. Na przykład aby ominąć pierwsze trzy dokumenty w wyborze:
db.dogs.find().skip(3);
Sortowanie wyników wykonuje się poprzez funkcję sort, która przyjmuje obiekt z polami służącymi do sortowania, dla których podajemy kierunek poprzez wartości
Na przykład posortujmy wyniki po polu name rosnąco (w tym przypadku alfabetycznie)
db.dogs.find().sort({ name: 1 });
Przy pomocy funkcji count() możemy otrzymać ilość elementów w kolekcji (możemy też doprecyzować filtr w argumencie metody tak jak przy find())
db.dogs.count();
Wynik: 6
Operator $exists pozwala wyciągnąć tylko te dokumenty, w których określony klucz istnieje lub nie.
db.dogs.find({ owner: { $exists: true } }); // klucz owner istnieje
db.dogs.find({ owner: { $exists: false } }); // klucz owner nie istnieje
Operator $type odflitruje tylko te dokumenty, w których określony klucz ma wartość podanego typu, na przykład string lub number:
db.dogs.find({ age: { $type: 'number' } });
Operator $regex przyjmuje regularne wyrażenie, które musi spełnić wartość danego pola w dokumencie
db.dogs.find({ name: { $regex: 'A' } });
Wynik:
/* 1 */
{
"_id" : ObjectId("64597065e50efc2ba61c4bf3"),
"name" : "Azor",
"age" : 10.0,
"features" : [
"świszczy"
]
}
Operator logiczny lub - $or, pozwala wybrać dokumenty spełniające przynajmniej jeden z warunków:
db.dogs.find({ $or: [{ name: { $regex: 'A' } }, { age: { $lte: 3 } }] });
Wynik:
/* 1 */
{
"_id" : ObjectId("64596ed4e50efc2ba61c4bee"),
"name" : "burek",
"age" : 3.0,
"features" : [
"zjada kapcie",
"pozwala się głaskać",
"rudy"
]
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bef"),
"name" : "Mamba",
"age" : 2.0,
"features" : [
"poluje na samochody",
"szczerzy zęby",
"czarna"
]
}
/* 3 */
{
"_id" : ObjectId("64597065e50efc2ba61c4bf3"),
"name" : "Azor",
"age" : 10.0,
"features" : [
"świszczy"
]
}
Operator iloczynu logicznego $and, zwróci tylko te dokumenty które spełniają wszystkie warunki
db.dogs.find({ $and: [{ name: { $regex: '.*a.*' } }, { age: { $lte: 10 } }] });
Wynik:
/* 1 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bef"),
"name" : "Mamba",
"age" : 2.0,
"features" : [
"poluje na samochody",
"szczerzy zęby",
"czarna"
]
}
/* 2 */
{
"_id" : ObjectId("64596f84e50efc2ba61c4bf0"),
"name" : "Pirat",
"age" : 4.0,
"features" : [
"ma tylko jedno oko",
"pozwala się głaskać",
"kudłaty"
]
}
/* 3 */
{
"_id" : ObjectId("64597228e50efc2ba61c4bf5"),
"name" : "Klusia",
"age" : 10.0,
"features" : [
"w kształcie kadłuba",
"szara"
],
"owner" : {
"name" : "Jola",
"age" : 27.0,
"address" : "Kraków"
}
}
3.7 Kursory
Wynik zapytania, otrzymany przy pomocy funkcji find, nazywany jest kursorem. Kursory zawierają w sobie zbiór otrzymanych z bazy danych dokumentów.
Wykorzystując składnię języka JavaScript i metodę kursora, możemy po kolei pracować na otrzymanych dokumentach
const cursor = db.dogs.find();
while (cursor.hasNext()) {
obj = cursor.next();
print(obj['name']);
}
Wynik:
burek
Mamba
Pirat
Azor
Kajtek
Klusia
3.8 Zapisywanie dokumentów
Zapisywanie dokumentów możemy osiągnąć też metodą save.
W nowym dokumencie jako pole można przekazać parametr _id. Jeżeli metoda znajduje dokument z taką wartością _id, to dokument zaktualizuje się. Jeżeli jednak nie ma dokumentów z podanym _id to nowy dokument jest tworzony.
db.dogs.save({ name: 'Bars', age: 3 });
3.9 Aktualizacja dokumentów
Więcej opcji dla aktualizacji dokumentu oferuje funkcja updateOne (i jej podobna konstrukcja updateMany która może aktualizować wiele elementów. Przyjmuje ona trzy parametry:
- filter: filtr który odnajduje dokument który chcemy zmodyfikować
- update: określa w jaki sposób chcemy zmodyfikować dokument
- options: określa dodatkowe parametry dla aktualizacji dokumentów, między innymi upsert.
Jeżeli parametr upsert ma wartość true, to MongoDB będzie aktualizować dokument, jeśli zostanie on znaleziony lub stworzy nowy, jeżeli takiego dokumentu nie ma.
Jeżeli jednak przyjmuje on wartość false, to MongoDB nie będzie tworzyć nowego dokumentu w przypadku, w którym filter nie znajdzie żadnego pasującego dokumentu.
Poniższa operacja zaktualizuje dokument w którym klucz name ma wartość Bars, nada mu wartości z obiektu update.$set. Z uwagi na flagę upsert, jeśli nie odnajdziemy dokumentu po filter, zostanie on utworzony przed aktualizacją.
db.dogs.updateOne(
{ name: 'Bars' },
{ $set: { name: 'Tom', age: 5 } },
{ upsert: true }
);
Jeśli chcemy zaktualizować wiele dokumentów, użyjemy metody updateMany
Dla usunięcia klucza wykorzystuje się operator $unset:
UWAGA: jeśli zastosujemy metodę updateOne, a elementów pasujących do filter jest więcej niż jeden, to zaktualizowany zostanie tylko pierwszy z nich.
3.10 Usunięcie dokumentu
Do usunięcia dokumentów w MongoDB przewidziana jest metoda deleteOne lub deleteMany:
Usunięcie wszystkich dokumentów pasujących do danego filtra
db.dogs.deleteMany({ name: 'Tom' });
Analogicznie jeżeli chcemy usunąć tylko jeden dokument:
db.dogs.deleteOne({ name: 'Tom' });