MongoDB – I Deo

MongoDB je veoma popularna baza u tzv. NoSQL svetu. Prvi put je javno objavljen 2009. godine. Dizajniran je kao skalabilna baza podataka čiji su glavni ciljevi performanse i lak pristup podacima. Naziv Mongo potiče od reči humongous, koja označava nešto izuzetno veliko. MongoDB je tzv. dokument baza podataka, koja omogućava da podaci budu smešteni u ugnježdenom obliku. Može da izvršava upite nad takvim podacima gotovo trenutno. MongoDB nema šemu, pa dokumenti mogu da sadrže opciona polja ili tipove koja ostali dokumenti iz iste kolekcije ne poseduju.

Uvod

Mongo predstavlja sredinu između velikih mogućnosti upita relacionih baza i distribuirane prirode servisa za čuvanje podataka. Mongo je JSON dokument baza, mada su tehnički gledano podaci smešteni u binarnu formu JSON-a, poznatiju kao BSON. Mongo dokument može biti posmatran kao red u tabeli relacione baze bez šeme, čije vrednosti mogu biti ugnježdene do proizvoljne dubine.

Možete primetiti da JSON dokument ima polje _id koje je ObjectId. Ovo polje služi da jedinstveno odredi dokument. ObjectId je uvek dužine 12 bajtova i sastavljen je od vremenskog žiga (engl. *timestamp*), ID klijentske mašine, ID klijentskog procesa i 3-bajtnog uvećavajućeg brojača.

Mongo je izvanredan izbor za klasu uvek rastućih web projekata sa velikim zahtevima za smeštanje podataka i malim budžetom za kupovinu skupog hardvera. Zahvaljujući nedostatku šeme, Mongo može da raste i menja se u korak sa modelom.

Kreiranje, čitanje, ažuriranje i brisanje

Kreiranje baze i podataka

Za kreiranje baze po imenu book potrebno je da u komandnoj liniji pokrenemo dole prikazanu komandu. Komanda će pokrenuti interfejs iz komandne linije.

$ mongo book

Nakon izvršavanja gore pomenute komande nalazimo se u „book“ bazi podataka, ostale baze možemo prikazati sa show dbs i možemo se prebaciti na bilo koju od njih sa komandom use.

Za kreiranje kolekcija u Mongu nije potrebno definisanje nikakvih šema. Sledeći kod kreira i dodaje towns kolekciju.

> db.towns.insert({
    name: "New York",
    population: 22200000,
    last_census: ISODate("2009-07-31"),
    famous_for: [ "statue of liberty", "food" ],
    mayor : {
        name : "Michael Bloomberg",
        party : "I"
    }
})

U prethodnoj sekciji smo pomenuli da su dokumenta u kojima se podaci čuvaju JSON (zapravo BSON), što znači da smo dodali novi dokument u JSON formatu, gde vitičaste zagrade {…} označavaju objekat sa ključevima i vrednostima, a uglaste zagrade […] koje označavaju niz.

Komanda show collections služi za pregled kolekcija, a pomoću nje možemo videti da kolekcije sada postoje.

> show collections
towns

Komanda find() se koristi za prikaz sadržaja kolekcije (izlaz je formatiran radi lakšeg čitanja):

> db.towns.find()
{
    "_id" : ObjectId("4d0ad975bb30773266f39fe3"),
    "name" : "New York",
    "population": 22200000,
    "last_census": "Fri Jul 31 2009 00:00:00 GMT-0700 (PDT)",
    "famous_for" : [ "statue of liberty", "food" ],
    "mayor" : { "name" : "Michael Bloomberg", "party" : "I" }
}

Mongov primarni jezik je JavaScript.

> db.help()
> db.towns.help()

Gornje komande će izlistati dostupne funkcije nad izabranim objektom. Db je JavaScript objekat koji sadrži informacije o trenutnoj bazi. Db.x je JavaScript objekat koji predstavlja kolekciju pod imenom x. Komande su zapravo JavaScript funkcije.

> typeof db
object
> typeof db.towns
object
> typeof db.towns.insert
function

Izvorni kod neke funkcije možete videti pozivanjem iste samo bez parametara ili bez zagrada. Sada ćemo napraviti funkciju koja prima parametre koji opisuju grad, i dodaje grad u kolekciju gradova:

mongo/insert_city.js
function insertCity(name, population, last_census, famous_for, mayor_info)
{
    db.towns.insert({
        name:name,
        population:population,
        last_census: ISODate(last_census),
        famous_for:famous_for,
        mayor : mayor_info
    });
}

Pozivom funkcije sa parametrima dodajemo gradove u kolekciju:

insertCity("Punxsutawney", 6200, '2008-31-01', ["phil the groundhog"], { name : "Jim Wehrle" })
insertCity("Portland", 582000, '2007-20-09', ["beer", "food"], { name : "Sam Adams", party : "D" })

Čitanje

Do sada smo koristili funkciju find() bez parametara kako bismo prikazali sve dokumente. Da bismo pristupili tačno određenom dokumentu potrebno je da funkciji prosledimo _id dokumenta tipa ObjectId:

db.towns.find({ "_id" : ObjectId("4d0ada1fbb30773266f39fe4") })
{
    "_id" : ObjectId("4d0ada1fbb30773266f39fe4"),
    "name" : "Punxsutawney",
    "population" : 6200,
    "last_census" : "Thu Jan 31 2008 00:00:00 GMT-0800 (PST)",
    "famous_for" : [ "phil the groundhog" ],
    "mayor" : { "name" : "Jim Wehrle" }
}

Funkcija find() takođe prihvata opcioni drugi parametar, objekat polja kojim možemo definisati koja polja želimo da budu prikazana. Ukoliko želimo da pored _id-a vidimo samo naziv, potrebno je kao drugi parametar da prosledimo name sa vrednošću 1 ili *true*:

db.towns.find({ _id : ObjectId("4d0ada1fbb30773266f39fe4") }, { name : 1 })
{
    "_id" : ObjectId("4d0ada1fbb30773266f39fe4"),
    "name" : "Punxsutawney"
}

Da bismo prikazali sve osim imena, umesto 1 postavimo 0 (ili false ili null):

db.towns.find({ _id : ObjectId("4d0ada1fbb30773266f39fe4") }, { name : 0 })
{
    "_id" : ObjectId("4d0ada1fbb30773266f39fe4"),
    "population" : 6200,
    "last_census" : "Thu Jan 31 2008 00:00:00 GMT-0800 (PST)",
    "famous_for" : [ "phil the groundhog" ]
}

Moguće je kreirati i upite koji filtriraju vrednosti iz određenog opsega, kao i upite sa više kriterijuma. Ukoliko želimo da nađemo sve gradove koji počinju slovom P i imaju populaciju manju od 10000 stanovnika, možemo koristiti regularne izraze:

db.towns.find(
    { name : /^P/, population : { $lt : 10000 } },
    { name : 1, population : 1 }
)
{ "name" : "Punxsutawney", "population" : 6200 }

Kondicioni operatori u Mongo-u se navode u formatu field : {$op : value}, gde je $op operator. Moguće je kreirati operator kao objekat, što ćemo pokazati na primeru gde populacija mora biti između 10000 i milion:

var population_range = {}
population_range['$lt'] = 1000000
population_range['$gt'] = 10000
db.towns.find(
    { name : /^P/, population : population_range },
    { name: 1 }
)
{ "_id" : ObjectId("4d0ada87bb30773266f39fe5"), "name" : "Portland" }

Pretraživanje po datumu se realizuje na sledeći način:

db.towns.find(
    { last_census : { $lte : ISODate('2008-31-01') } },
    { _id : 0, name: 1 }
)
{ "name" : "Punxsutawney" }
{ "name" : "Portland" }

Kopanje u dubinu

Za potrebe demonstracije sledećih primera potrebna nam je nova kolekcija koja će sadržati države. Ovog puta ćemo samostalno postaviti _id na proizvoljan string:

db.countries.insert({
    _id : "us",
    name : "United States",
    exports : {
        foods : [
            { name : "bacon", tasty : true },
            { name : "burgers" }
        ]
    }
})
db.countries.insert({
    _id : "ca",
    name : "Canada",
    exports : {
        foods : [
            { name : "bacon", tasty : false },
            { name : "syrup", tasty : true }
        ]
    }
})
db.countries.insert({
    _id : "mx",
    name : "Mexico",
    exports : {
        foods : [
            {name : "salsa", tasty : true, condiment : true}
        ]
    }
})

Ugnježdeni nizovi su česta pojava u Mongo-u. Možemo ih jednostavno pretraživati zadavanjem tačne vrednosti:

db.towns.find(
    { famous_for : 'food' },
    { _id : 0, name : 1, famous_for : 1 }
)
{ "name" : "New York", "famous_for" : [ "statue of liberty", "food" ] }
{ "name" : "Portland", "famous_for" : [ "beer", "food" ] }

A može se pretraživati i podudaranjem delova vrednosti:

db.towns.find(
    { famous_for : /statue/ },
    { _id : 0, name : 1, famous_for : 1 }
)
{ "name" : "New York", "famous_for" : [ "statue of liberty", "food" ] }

Moguće je tražiti i podudaranje svih vrednosti:

db.towns.find(
    { famous_for : { $all : ['food', 'beer'] } },
    { _id : 0, name:1, famous_for:1 }
)
{ "name" : "Portland", "famous_for" : [ "beer", "food" ] }

Tu je i primer u kome nema podudaranja:

db.towns.find(
    { famous_for : { $nin : ['food', 'beer'] } },
    { _id : 0, name : 1, famous_for : 1 }
)
{ "name" : "Punxsutawney", "famous_for" : [ "phil the groundhog" ] }

Međutim, prava snaga Mongo-a jeste u tome što može da kopa u dubinu dokumenta i vrati vrednost duboko zakopanog poddokumenta. Kako bismo ovo demonstrirali, potrebno je da nazive polja razdvojimo tačkom. Kao primer možemo naći gradove čiji su gradonačelnici nezavisni, označeni sa „I“:

db.towns.find(
    { 'mayor.party' : 'I' },
    { _id : 0, name : 1, mayor : 1 }
)


{
    "name" : "New York",
    "mayor" : {
        "name" : "Michael Bloomberg",
        "party" : "I"
    }
}

Ili gradonačelnike koji nemaju partiju:

db.towns.find(
    { 'mayor.party' : { $exists : false } },
    { _id : 0, name : 1, mayor : 1 }
)
{ "name" : "Punxsutawney", "mayor" : { "name" : "Jim Wehrle" } }

Prethodni upiti jesu efikasni ukoliko želimo da pretražujemo po jednom polju, ali hajde da probamo da uradimo pretragu po više polja. Možemo da pronađemo države koje izvoze slaninu, ali ne bilo kakvu već samo one koje izvoze ukusnu slaninu:

db.countries.find(
    { 'exports.foods.name' : 'bacon', 'exports.foods.tasty' : true },
    { _id : 0, name : 1 }
)

{ "name" : "United States" }
{ "name" : "Canada" }

Možete primetiti da jesmo pronašli države koje izvoze slaninu, ali u rezultatu se pojavljuje i Kanada, čija slanina nije ukusna :)). Za rešavanje ovog problema možemo koristiti $elemMatch, a njime možemo da odredimo dokumente ili poddokumente koji sadrže tačno određene vrednosti za tačno određena polja:

db.countries.find(
{
    'exports.foods' : {
        $elemMatch : {
            name : 'bacon',
            tasty : true
        }
    }
},
    { _id : 0, name : 1 }
)

{ "name" : "United States" }

Do sada smo koristili samo „i“ operator, a postoje i mnogi drugi operatori. Kao primer pokazaćemo upotrebu „ili“ operatora. Ukoliko želimo da nađemo zemlju sa vrednošću „mx“ za polje „_id“ ili vrednošću „United States“ za polje „name“, to možemo uraditi na sledeći način. Kompletnu listu operatora je moguće pronaći na zvaničnom sajtu MongoDB.

db.countries.find(
{
    $or : [
        { _id : "mx" },
        { name : "United States" }
    ]
},
    { _id:1 }
)

{ "_id" : "us" }
{ "_id" : "mx" }

Čitanje pomoću korisnički definisanog koda

Moguće je pokrenuti klijentski definisanu funkciju koja će se izvršiti na svim dokumentima određene kolekcije, mane su te što ne možemo da koristimo indekse, kao i to što Mongo ne može da optimizuje ove upite. Ali, nekada je ovo jedini način na koji možete da rešite problem. Kao primer možemo da nađemo sve gradove čija je populacija između 6000 i 600000:

db.towns.find( function() {
    return this.population > 6000 && this.population < 600000;
} )

Ažuriranje

Funkcija update(criteria,operation) zahteva dva argumenta. Prvi je kriterijum upita i to je isti argument koji smo prosleđivali komandi find(), a drugi parametar je takođe objekat čija će polja biti zamenjena u pronađenim dokumentima. U sledećem primeru koristićemo operator $set, kako bismo postavili državu na „OR“ za grad „Portland“:

db.towns.update(
    { _id : ObjectId("4d0ada87bb30773266f39fe5") },
    { $set : { "state" : "OR" } }
);

Potrebno je koristiti operator $set, zato što je Mongo orjentisan ka dokumentima, pa u slučaju da izostavimo operator, Mongo će pronađeni dokument zameniti novim koji mu prosledimo. Zato, budimo oprezni pri korišćenju komande update(). Takođe postoje i mnogi drugi operatori, poput $inc kojim inkrementiramo broj. Kompletan spisak operatora možete pronaći na sajtu MongoDB.

Reference

U MongoDB-u nije moguće izvršavanje pridruživanja (engl. Join) zbog svoje distribuirane prirode, ali je moguće referencirati dokumente između sebe. Najbolje je koristiti sledeću sintaksu za kreiranje referenci: { $ref : „collection_name“, $id : „reference_id“ }, što ćemo pokazati na sledećem primeru:

db.towns.update(
    { _id : ObjectId("4d0ada87bb30773266f39fe5") },
    { $set : { "state" : "OR" } }
);

Sada možemo da zapamtimo Portland u lokalnoj promenjivoj:

var portland = db.towns.findOne({ _id : ObjectId("4d0ada87bb30773266f39fe5") })

Nakon toga, možemo da pronađemo u državama određenu državu po referenci grada:

db.countries.findOne({ _id: portland.country.$id })

Brisanje

Za brisanje dokumenata je potrebno promeniti find() sa remove(), i svi dokumenti koji zadovoljavaju kriterijum će biti obrisani. Najbolje je da pre komande remove() pokrenete komandu find(), kako biste potvrdili da zaista te dokumente želite da obrišete. Pokazaćemo na primeru kako možemo obrisati sve zemlje koje izvoze neukusnu slaninu:

var bad_bacon = {
    'exports.foods' : {
        $elemMatch : {
            name : 'bacon',
            tasty : false
        }
    }
}
db.countries.remove( bad_bacon )

Nakon brisanja možete pokrenuti komandu count() i uveriti se da sada postoje samo dve države.

Indeksi

Mongo ima ugrađenu podršku za indekse kako bi poboljšao performanse izvršavanja upita. Mongo podržava sledeće vrste struktura za indeksiranje:

  • B-stabla,
  • dvodimenzionalne geografske indekse i
  • sferične geografske indekse.

U nastavku ćemo se baviti indeksima koji koriste strukturu B-stabla. Kako bismo demonstrirali moć indeksa moraćemo da napravimo novu kolekciju i da je popunimo velikom količinom dokumenata. Kao primer kreiraćemo kolekciju telefona, i napravićemo funkciju koja će je popuniti:

populatePhones = function(area,start,stop) {
    for(var i=start; i < stop; i++) {
        var country = 1 + ((Math.random() * 8) << 0);
        var num = (country * 1e10) + (area * 1e7) + i;
        
        db.phones.insert({
            _id: num,
            components: {
                country: country,
                area: area,
                prefix: (i * 1e-4) << 0,
                number: i
            },
            display: "+" + country + " " + area + "-" + i
        });
    }
}

Pokrenite funkciju sa trocifrenim brojem za area i opsegom od dva sedmocifrena broja:

populatePhones( 800, 5550000, 5650000 )
db.phones.find().limit(2)

{ "_id" : 18005550000, "components" : { "country" : 1, "area" : 800, "prefix" : 555, "number" : 5550000 }, "display" : "+1 800-5550000" }
{ "_id" : 88005550001, "components" : { "country" : 8, "area" : 800, "prefix" : 555, "number" : 5550001 }, "display" : "+8 800-5550001" }

Svaki put kada je nova kolekcija kreirana, Mongo kreira indeks nad njenim poljem _id. Indeks za određenu kolekciju možete prikazati komandom getIndexes():

db.phones.getIndexes()
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "book.phones"
    }
]

Pre nego što kreiramo indeks nad drugim poljima, valjalo bi da proverimo trenutnu brzinu izvršavanja upita, kako bismo kasnije mogli da proverimo unapređenje nakon dodavanja indeksa. Ovo možemo uraditi na sledeći način (od celokupnog izlaza je izdvojen samo deo koji nas interesuje posto je izlaz opširniji):

db.phones.find({display: "+1 800-5650001"}).explain("executionStats")

{...
"executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 54,
            "works" : 2,
...}

U konkretnom slučaju testiranja, broj milisekundi verovatno neće biti isti kao ovde, ali svakako zapamtite vrednost koju ste dobili. Sada ćemo kreirati indeks pozivanjem ensureIndex(fields,options) nad kolekcijom. Atribut fields prihvata objekat u kojem su definisana polja iz kolekcije nad kojima želimo da kreiramo indeks, a atribut options služi da odredimo vrstu indeksa koju želimo. U našem slučaju kreiraćemo jedinstveni indeks koji odbacuje duplikate nad poljem display.

db.phones.ensureIndex(
    { display : 1 },
    { unique : true, dropDups : true }
)

Sada ponovo pokušajte komandu find() od malopre kako bismo uočili unapređenje:

db.phones.find({display: "+1 800-5650001"}).explain("executionStats")

{...
"executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 0,
            "works" : 2,
...}

Uspeli smo da sa 54 milisekundi spustimo izvršavanje upita na 0 milisekundi što je neverovatno ubrzanje. Ubrzanje je ostvareno tako što Mongo više ne mora da pretražuje celu kolekciju, već samo indeks, kako bi pronašao šta je traženo. Indekse takođe možemo kreirati i nad ugnježdenim podacima korišćenjem tačke, a savetuje se kreiranje indeksa u pozadini što se može uraditi korišćenjem opcije { background : 1 }:

db.phones.ensureIndex({ "components.area": 1 }, { background : 1 })

U narednom delu pozabavićemo se naprednijim temama od pretrage podataka, i to pre svega obradom u vidu agregiranja podataka i nekim ne baš uobičajenim tipovima podataka koje podržava MongoDB.

Korisni linkovi