mongoDB – dokumentová databáza 2 – aktualizácie objektov

V minulom dieli sme videli

V predošlom dieli sme si ukázali úvod do práce s mongoDB — inštaláciu, vkladanie a vyhľadávanie podľa rozličných kritérii.

Dnes si ukážeme ďalšie možnosti súvisiace hlavne s aktualizáciou dát.

Ak si pamätáte, naša databáza vyzerala nasledovne:

db.zvierata.insert({ meno : "hroch Karol", vek : 10})
db.zvierata.insert({ meno : "nosorožec Štefan", "vek" : 45})
db.zvierata.insert({ meno : "had Boris", "vek" : 7})
db.zvierata.insert({ meno : "baran Marián", "vek" : 3})

Už dávno ste si museli všimnúť, že dáta v tejto databáze by možno mohli vyzerať krajšie. Keby sme v ZOO mali viac hrochov či baranov, vyhľadávanie podľa jednotlivých druhov by bolo možné len s použitím regulárnych výrazov.

db.zvierata.insert({meno : "had Kaa", "vek" : 27})
db.zvierata.insert({meno : "baran Šón", "vek" : 2})

Aktualizácia dát

Na aktualizáciu dát slúži základná metóda update(). Má premenný počet argumentov a umožňuje vykonávať zmeny rozličnými spôsobmi. Ak chceme napríklad premenovať hada Kaa na Nagini a sme si istí, že taký had je len jeden, použime nasledovný príkaz:

db.zvierata.update( { meno : "had Kaa" }, { $set : { meno : "had Nagini"}})

Prvý parameter je objekt, ktorý sa má zmeniť a druhý parameter využíva modifikujúci operátor $set˛, v ktorom nastavíme novú hodnotu atribútu meno. Tento príkaz je ekvivalentný klasickému

UPDATE zvierata SET meno = 'had Nagini' WHERE meno = 'had Kaa'

Navyše je tento update atomický, čo znamená že prebehne naraz a nemôže sa stať, že počas aktualizácie nejaký iný prístup (vlákno, aplikácia) pod rukou pristúpi či nebodaj zmení aktualizované dáta. Popri tom vyžaduje minimum prenosu po sieti — nemusí sa totiž prenášať celý objekt, upraviť a poslať naspäť. Namiesto toho sa serveru pošle len samotný príkaz na aktualizáciu jedného atribútu.

Atomicita funguje len v prípade, keď aktualizujeme jeden objekt.

Hromadné aktualizácie

Ak chceme všetky zvieratá „zostarnúť“ o rok, teda každému z nich zvýšiť vek o 1 rok, môžeme použiť rozšírenú štvorparametrovú metódu:

db.zvierata.find( {}, {$inc : { vek : 1} }, false, true)    

V tomto prípade:

  • argument {} indikuje aktualizáciu všetkých objektov
  • atomicý operátor $inc zvýši hodnotu veku o 1 rok. (V tomto prípade sa atomicita neprejaví, keďže aktualizujeme viacero objektov.)
  • tretí argument false súvisí s tzv. upsertom, o ktorom sa zmienime nižšie. V tomto prípade nás nezaujíma a použijeme hodnotu false
  • štvrtý argument multi hovorí o aplikovaní aktualizácie na viacero objektov.

Nahrádzanie objektov

Operáciu update môžeme použiť aj na kompletné nahrádzanie objektov. Premenovanie hada Kaa môžeme vykonať aj inou syntaxou. Druhý parameter môže obsahovať kompletný nový objekt, ktorým sa má nahradiť nájdený objekt.

nagini = { meno : "had Nagini", vek : 27 }
db.zvierata.update( { meno : "had Kaa" }, nagini)

Alternatívne môžeme použiť aj jednoriadkovú syntax:

db.zvierata.update( { meno : "had Kaa" }, { meno : "had Nagini", vek : 27 })

Tento spôsob však nie je atomický a je náročnejší na sieťovú premávku než atomické použitie pomocou $set. V niektorých prípadoch je však nevyhnutný.

Ďalšie operátory pre aktualizáciu možno nájsť v dokumentácii.

Upserty — „ak neexistuje, vlož ho“

Spomínali sme tretí záhadný argument súvisiaci s tzv. upsertami (upsert = update or insert). Ak používate niektorý z nástrojov objektovo-relačného mapovania, napr. Hibernate, poznáte sémantiku save or update. Nemusíte sa starať o to, či objekt už v databáze existuje alebo nie. Jediná metóda sama usúdi, či má aktualizovať už existujúci objekt alebo či ho má nanovo vložiť do databázy, pretože v nej neexistuje.

harold = { meno : "baran Harold", vek : 10}
db.zvierata.update( harold, harold, true)

Prvý harold je kritérium pre vyhľadávanie, druhý zodpovedá aktualizovanému objektu. Parameter true hovorí o upserte.

Dôležitá zásada hovorí, že upsert funguje len pre vkladanie jedného objektu.

Metóda save() ako skratka pre upsert

MongoDB shell a niektoré ovládače dávajú k dispozícii metódu save(), ktorá je skráteninou pre uloženie alebo aktualizáciu jedného objektu. Zápis

db.zvierata.save( { meno : "srnka Bambi", vek : 3} )

je ekvivalentný zápisu

bambi =  { meno : "srnka Bambi", vek : 3} 
db.zvierata.update( { _id : bambi._id}, bambi, true)

Upserty s modifikátormi

Upserty možno používať aj s modifikátormi

db.zvierata.update( { meno : "srnka Bambi"}, { $inc : { vek : 1} } )

V takom prípade sa modifikátor použije na ukážkový objekt (teda prvý parameter; kritérium) a výsledný nájdený objekt sa aktualizuje, resp. vloží.

Modifikátor sa však nesmie odkazovať na identifikátor (_id) a dva modifikátory v aktualizácii sa nesmú vzťahovať na rovnaký atribút.

Overenie výsledku aktualizácie

Niekedy potrebujeme zistiť, či klasický update aj v skutočnosti aktualizoval objekt. Rovnako sa môže stať, že potrebujeme zistiť, či upsert vložil nový alebo len aktualizoval existujúci objekt.

Získať to môžeme zavolaním databázového príkazu getlasterror:

db.runCommand("getlasterror")

Výsledok môže vyzerať nasledovne:

{
    "updatedExisting" : true,
    "n" : 1,
    "connectionId" : 1,
    "err" : null,
    "ok" : 1
}

Ak je vo výsledku prítomný atribút updateExisting, znamená to, že prebehla požiadavka na aktualizáciu. Ak má tento atribút hodnotu true, požiadavka vyústila v skutočnú aktualizáciu. V opačnom prípade sa neaktualizoval žiaden existujúci objekt.

Ak prebehol upsert, kvôli ktorému sa vložil do databázy nový objekt, vo výsledku sa objaví atribút upserted s hodnotou čerstvo prideleného identifikátora objektu.

Padding – vypchávanie dát s lepšou výkonnosťou

Ak pri aktualizácii cez update sa dokument nezväčšil, úprava prebehne v in place režime. Znamená to, že databáza nemusí alokovať a zapísať nový dokument (a starý vymazať), ale zmeny vykonáva rovno na mieste. Zároveň to má pozitívny efekt aj pre indexy, pretože pri presune objektu by ich bolo nutné prebudovať.

Viac informácií o paddingu nájdete v dokumentácii, resp. na blogu mongoDB.

Blokovanie iných zápisov počas hromadnej aktualizácie

Pri bežnom spôsobe aktualizácie dát viacerých dokumentov naraz sa môže stať, že pomedzi jednotlivé zmeny sa môžu vyskytovať zápisy pochádzajúce z iných vlákien či aplikácii. Ak chceme zápisy izolovať a zabrániť týmto zmenám, vieme použiť príznak $atomic.

db.zvierata.update({$atomic: true}, {$inc : { vek : 1}}, false, true)

Pozor na to, že atomická operácia neznamená to isté ako transakčné spracovanie (teda, že operácia prebehne buď celá alebo vôbec). Garantuje to len to, že počas behu aktualizácie neprebehnú na aktualizovaných dokumentoch žiadne iné operácie.

Blokovanie však nefunguje pri shardingu, teda pri rozdelení databázy na viaceré servery.

Bližšie informácie o atomických operáciách možno nájsť v dokumentácii.