MongoDB andmebaas Zone Virtuaalserveris

Virtuaalserverite võimekus muudkui kasvab. Nüüd lisandus võimalus kasutada MongoDB NoSQL andmebaasi. Selgitame millega tegu.

MongoDB logo

Virtuaalserverite kasutajatele on nüüd saadaval MongoDB, maailma üks tuntumaid NoSQL andmebaasimootoreid. Ühtlasi toob MongoDB ja Node.js ühine toetamine meie klientideni niinimetatud MEAN arenduskeskkonna toe, mis koosneb komponentidest MongoDB, Express, Angular ja Node.js.

MongoDB kasutamise võimalus on olemas alates Virtuaalserveri teenuspaketist II ja esialgu loeme selle küpsusastmeks “beeta”, ehk eelkõige soovitame seda entusiastidele, kes on valmis meile tagasisidet andma.

Siinkohal tahaksime lausuda tänusõnad ja teha kummarduse kõigile kasutajatele, kes on meile uute võimaluste väljatöötamisega kaasnevatel beetaperioodidel suureks abiks olnud ja vajadusel ka suurt kannatlikkust üles näidanud.

Enne kui MongoDB baaside loomise juurde asume, on asjakohane meelde tuletada, mis vahe on relatsioonilisel andmebaasil nagu MySQL ning MongoDB-l ehk dokumendipõhisel andmebaasil. Ühtlasi mõni sõna sellest, millal üht või teist eelistada.

Dokumendibaas vs relatsiooniline baas

Suures plaanis on vahe selles, kuidas andmebaasi “kõhus” andmeid salvestatakse.

Relatsioonilise baasi puhul salvestatakse andmed ennikutena (tuples), kus üks ennik moodustab ühe tabeli rea ning kõik read on samasuguse struktuuriga. Dokumendibaasi puhul salvestatakse kirjed hierarhilise puuna, mis ühest küljest annab vabaduse eri kujuga dokumentide moodustamiseks, kuid samas kaotab “odava” tabelite liidestamise võimaluse ehk JOIN-id. Ennikute puhul on tunduvalt lihtsam eri tabelite ridu omavahel kokku viia, kui dokumentide puhul, sest viimane nõuab igast dokumendist eraldi õige liikumistee lahendamist. Relatsioonilise baasi puhul lahendub iga võti juba esimesel tasandil, hierarhilise puu korral aga võib otsitud võti asuda kusagil väga sügaval, näiteks mingi listi alamlisti-alamlisti all.

Dokumendibaasi tugevuseks on parem ühilduvus ORM-idega, sest ORM’i poolt näidatud andmeobjekt vastabki sellisel juhul enamvähem andmebaasi salvestatud dokumendile, erinevalt relatsioonilisest baasist, kus ORM-i objekti koostamine tähendab eri kirjete kokku lappimist. Samuti on dokumendibaas väga hea erinevate one-to-many andmete haldamiseks, kus alaminfo on esitatud listina. Näiteks kui meil on baasis kasutaja andmete kirje, siis samasse kirjesse saab massiivina salvestada ka kasutaja viimased sisselogimised, samas kui relatsioonilise baasi puhul tähendaks see tõenäoliselt eraldi tabeli loomist.

Üheks täiendavaks dokumendibaasi tugevuseks on ka kergem replikeeritavus. Kõik vajalikud andmed kipuvad olema ühes kirjes koos ja seega saame seda kirjet vabalt erinevate serverite vahel kopeerida, ilma et peaks muretsema, kas mingi teine tabel ikka on piisavalt kiirelt ligipääsetav, mis on ülioluline relatsioonilise liidestuse korral.

Dokumendibaasi näide

Kollektsioon “Kasutajad”:

[{
  nimi: 'Jaan Tamm',
  viimasedSisselogimised: [
    {ip: '1.2.3.4', aeg: '2017-01-01'},
    {ip: '1.2.3.2', aeg: '2017-01-02'}
  ]
}]

Otsime üles kõik kasutajad, kes on sisse loginud IP aadressilt 1.2.3.4

db.collection('Kasutajad').find({'viimasedSisselogimised.ip': '1.2.3.4'})
Relatsioonilise baasi näide

Selle näite puhul asuvad kasutajad ja sisselogimised erinevates tabelites, mis on kokku liidetud Kasutajad.Id väärtuse alusel.

Tabel “Kasutajad”:

| Id | Nimi      |
|----|-----------|
|  1 | Jaan Tamm |

Tabel “Sisselogimised”:

| Kasutaja | Ip      | Aeg        |
|----------|---------|------------|
|        1 | 1.2.3.4 | 2017-01-01 |
|        1 | 1.2.3.2 | 2017-01-02 |

Otsime üles kõik kasutajad, kes on sisse loginud IP aadressilt 1.2.3.4

SELECT * FROM Kasutajad 
  LEFT JOIN Sisselogimised ON 
    Sisselogimised.Kasutaja=Kasutajad.Id
  WHERE Sisselogimised.Ip='1.2.3.4'

Dokumendibaasi nõrkuseks on samas many-to-one seosed. Reeglina tähendab see sellist olukorda, kus kirje juures on viitav ID mingi palju kasutatud väärtuse juurde, näiteks “Riik: 123”, kus riigi nimi tuleb teisest tabelist “123: Eesti” ning seal tabelis nime muutes muutub riigi nimi ka kõikide sellele viitavate kirjete jaoks. Dokumendibaasi puhul peaksime riigi nime salvestama pigem kirje enda juures “{riik: ‘Eesti’}”, mis tähendab, et kui soovime seda nime hiljem muutma, peame muutma seda igas taolises kirjes eraldi.

Dokumendibaasi korral lahendatakse sellised many-to-one olukorrad tavaliselt pigem rakenduse tasemel, kus rakendus ise oskab arvet pidada, et mis kus asub. Dokumendi kirjesse salvestatakse näiteks ikkagi ainult ID, aga andmete päringul ei liideta tabeleid mitte JOIN’iga nagu relatsioonilise baasi korral, vaid laetakse vajalikud väärtused eraldi ning liidetakse siis juba rakenduse poolel.

Üldiselt kehtib põhimõte, et iga töö jaoks oma tööriist. Kui salvestatavate andmete struktuur on pigem keeruline ja palju varieeruv (ühes kirjes ühel kujul, teises teisel kujul), siis tasuks pigem kaaluda dokumendibaasi. Kui on oluline liidestada eri tüüpi andmeid kokku mitmel eri viisil ning andmete struktuur on lihtne ja ette teada, siis pole mõtet relatsioonilisest baasist kaugemale vaadata.

MongoDB kasutamine Virtuaalserveris

Esimese sammuna tuleks MongoDB oma konto jaoks seadistada. Selle jaoks võib virtuaalserveri administreerimisliideses valida vasakust menüüst Andmebaasid ja selle alt MongoDB ning avanenud lehel see sisse lülitada. Kui andmebaas on loodud, avaneb meile järgmine pilt koos kasutusandmetega (host, port, kasutajanimi, parool jne). Neid andmeid on meil hiljem vaja oma rakenduses MongoDB ligipääsuks.

MongoDB puhul ei ole meil vaja tegeleda (vähemalt alguses) tabelite, või MongoDB mõistes kollektsioonide, haldamisega. MySQL puhul alustaksime ilmselt phpMyAdmin liidesest, kus me looks tabelite struktuurid jne, kuid MongoDB puhul tekitatakse kollektsioon automaatselt siis, kui me esimest korda sinna midagi lisada proovime. Kuna kirje struktuur ei ole ette määratud (ainus fikseeritud võti on _id, mis on kirje primaarvõtmeks), siis ei ole vaja ka midagi eelnevalt ära kirjeldada.

Väikeseks erandiks oleks siin küll indeksite loomine, aga alustamisel ei ole see eriti oluline, nii et me sellega praegu arvestama ei hakka

MongoDB ja Node.js

Kõigepealt proovime saada MongoDB tööle Node.js rakenduses. Selle jaoks on meil vaja eelnevalt näidatud andmebaasiga ühendamise andmeid ning mongodb moodulit, mille saame tõmmata npm käsuga (eeldusel, et oleme ssh abil oma Virtuaalserveri kontole sisse loginud):

$ npm install mongodb

Teeme näidisena sellise rakenduse, mis käivitamisel leiab hetke aja Unix ajatempli ning salvestab selle MongoDB andmebaasi uue kirjena, seejärel loeb baasist kõik sellised kirjed ja väljastab need ekraanile. Rakenduse ülevaatlikkuse huvides ei tegele see veahaldusega, eeldame et kõik tegevused õnnestuvad.

Salvestame testrakenduse faili mongotest.js

const MongoClient = require('mongodb').MongoClient;
// ühenduse andmed on kujul
// 'mongodb://USER:PASS@HOST:PORT/DB'
const mongourl = 'mongodb://mongodb_ABC:pass@virt00000.loopback.zonevs.eu:5678/mongodb_DEF';

MongoClient.connect(mongourl, function (err, db) {
  // Otsime välja viite tabeli "documents" juurde.
  // Kui tabelit veel ei ekistseeri, siis see luuakse
  // esimese elemendi sisestamisel
  let collection = db.collection('documents');

  // Lisame tabelisse uue elemendi, milleks on
  // objekt kujul {type:'time', time:unix_timestamp}
  collection.insertOne({
    type: 'time',
    time: Math.ceil(Date.now() / 1000)
  }, (err, result) => {
    // Otsime nüüd välja kõik kirjed, mille puhul type='time'
    // Otsingu väljundi sunnime toArray meetodi abil
    // massiiviks (vaikimisi on *kursor*)
    collection.find({
      type: 'time'
    }).toArray((err, docs) => {
      // väljastame ükshaaval kõikide leitud dokumentide
      // _id ning time väärtused
      docs.forEach(doc => {
        console.log('%s, %s', doc._id, doc.time);
      });
      // paneme baasi kinni, kuna meil pole seda
      // rohkem vaja
      db.close();
    });
  });
});

Kui me nüüd seda rakendust paar korda käivitame:

$ node mongotest.js

võiks selle väljund välja näha umbes selline:

586cb3a7c1b3c75b3547a142, 1483518887
586cb3a9c1b3c75b3547a143, 1483518889
586cb3abc1b3c75b3547a144, 1483518891
....

Täpsema dokumentatsiooni Node.js MongoDB mooduli kasutamiseks leiab MongoDB kodulehelt.

MongoDB ja PHP

Laienduse aktiveerimine

Kui Node.js puhul piisas MongoDB mooduli lisamisest, siis PHP puhul on tegevused veidi keerukamad. Esiteks tuleks meil aktiveerida PHP MongoDB laiendus, mida saame teha Virtuaalserveri PHP seadetest. Selle jaoks vali Veebiserveri menüüst Seaded ning avanenud lehe PHP seadete bloki järel kliki nupul Muuda.

Peale seda peaks avanema aken PHP seadistusetega ning meid huvitab siit eelkõige PHP laienduste muutmise nupp.

Avenenud aknas märgi linnuke MongoDB lahtisse.

Kui MongoDB laienduse aktiveerimist nimekirjas veel ei leidu, siis tuleks veidi oodata, kuni see võimalus ka sellesse serverisse tekib

Composer

MongoDB PHP laiendust on otse üsna keeruline kasutada, kuna see on mõeldud eelkõige madalama taseme API-le ligipääsu andmiseks ja seetõttu kasutatakse selle ees mõnd abistavat lisakihti. Üks populaarsemaid selliseid on MongoDB PHP Library, mille dokumentasiooni leiab siit. Kõigepealt peaksime selle siiski kuidagi hankima.

Kõige parem viis MongoDB teegi laadimiseks oleks kasutada PHP komponentide haldurit Composer. Meie Virtuaalserverites Composer vaikimisi paigaldatud ei ole, sest tõsised arendajad tahavad selliste vahendite versioonid sättida oma vajaduste ja käe järgi, kuid selle saab väga kergelt endale ise hankida, käivitades ssh kaudu Composeri allalaadimise lehel näidatud käsklused. Seda võiks teha samas kaustas, kuhu tuleb meie testrakendus, kuid võib ka mujal, meie jaoks on oluline nende käskude tulemusel tekkiv composer.phar fail.

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('SHA384', 'composer-setup.php') === '55d6ead61b29c7bdee5cccfb50076874187bd9f21f65d8991d46ec5cc90518f447387fb9f76ebae1fbbacf329e583e30') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"
PHP teegi hankimine

Kui meil on serveris vajalik composer.phar olemas (selle võib liigutada kuskile kergesti leitavasse kausta, kuna seda võib ka muude asjade jaoks vaja minna), siis võimegi selle abil vajaliku teegi alla laadida

$ php composer.phar require \
    mongodb/mongodb --ignore-platform-reqs

--ignore-platform-reqs argument on vajalik seetõttu, et ssh terminalis kasutatav PHP erineb veebiserveri kaudu käivitatavast PHPst ning kuigi veebiserveri jaoks on meil MongoDB laiendus aktiveeritud, siis käsureal ei pruugi see olla saadaval

Nüüd peakski meil olema kõik eeldused täidetud, et oma testrakendus käima saada.

PHP testrakendus

Samasse kausta, kus käivitasime composer require käsu, salvestame järgmise testrakenduse, näiteks nimega mongotest.php. Erinevalt Node.js testrakendusest peab see kaust olema veebiserveri kaudu ligipääsetav, kuna me ei käivita rakendust mitte käsurealt, vaid brauseri abil.

Testrakendus teeb täpselt sama tegevust, mida Node.js rakenduski. Salvestab baasi hetke aja ning väljastab kõik salvestatud kirjed. Mõlemad rakendused kasutavad sama baasi, seega saab neid ka paralleelselt käivitada ning kummagi töö mõjutab teise rakenduse poolt kättesaadavaid andmeid.

<?php
// lae sisse Composeri autoloader, mis tekitab
// vajaliku MongoDB nimeruumi
require 'vendor/autoload.php';

// väljundiks ei ole html, vaid vabatekst
header('content-type: text/plain; charset=utf-8');

// Kasutame ühenduse loomiseks MongoDB nimeruumist
// klassi Client koos ühenduse andmetega
$client = new MongoDB\Client(
'mongodb://mongodb_ABC:parool@virt00000.loopback.zonevs.eu:5678/mongodb_DEF');

// Otsime välja viite tabeli "documents" juurde.
// Kui tabelit ei eksisteeri, siis see luuakse
// esimese elemendi sisestamisel
$collection = $client->DBNAME->documents;

// Lisame tabelisse uue elemendi, milleks on
// massiiv kujul {type:'time', time:unix_timestamp}
$result = $collection->insertOne([
  type => 'time',
  'time' => time()
]);

// Otsime nüüd välja kõik kirjed, mille puhul type='time'
$cursor = $collection->find(['type' => 'time']);

// Väljastame ükshaaval kõikide leitud
// dokumentide _id ning time väärtused
foreach ($cursor as $document) {
  echo "{$document['_id']}, {$document['time']}\n";
}

Käivitame rakenduse brauseri kaudu, avades veebilehe aadressil http://minuserver/kaustatee/mongotest.php

Avanema peaks sarnane väljund nagu Node.js rakenduse korralgi.

Konksud

Kuna MongoDB on veel beeta-staatuses, siis tähendab see paraku ka mõningaid konkse selle kasutamisel.

Üheks olulisemaks MongoDB omaduseks on kerge replikeeritavus, st. et saab väga kergelt moodustada MongoDB klastreid. Ütled ühele instantsile, kust leida teine instants ja ongi suures plaanis kogu lugu. Virtuaalserverites see veel nii lihtne ei ole, kuna serverile ei saa lihtsalt väljastpoolt ligi (va. pordi suunamise korral). Seega saab MongoDB baasi kasutada eelkõige üksikinstantsi režiimis.

Teiseks konksuks on operatiivmälu maht, mis on MongoDB instantside puhul Virtuaalserverites piiratud. Parimaks talituseks tahab MongoDB üsna palju operatiivmälu, kuhu MongoDB puhverdab siis andmebaasi enim kasutatavat osa. Kuna Virtuaalserverite puhul jagavad eri kasutajad samu ressursse, siis ei ole võimalik igale andmebaasile väga palju mälu eraldada. Väiksemate mahtude korral ei tohiks see olla samas mingi probleem ning suuremate mahtude korral tasuks mõelda juba niikuinii kraad kangema arhitektuuri peale, näiteks koostada Pilveserverite abil MongoDB klastri või kasutusele võtta üks või mitu Privaatserverit vmt.

Manuaalne andmete varundamine/taastamine

Kolmas konks on seotud andmete varundamisega. MongoDB töötab esialgu veebiserveris. Veebiserveris varundatakse kettal olevaid faile, sealhulgas ka MongoDB andmebaasifaile, kuid lihtsalt failide kopeerimine ei ole andmebaasi kontekstis tavaliselt hea varundusstrateegia, sest see ei taga andmete terviklikkust.

Näiteks, kui kettalt kopeeritakse MongoDB faile nende muutmise hetkel, siis laekub varundusserverisse andmetest puudulik koopia – osad failid on juba õigesti muudetud, osad veel mitte. Suures plaanis peaks selline varundamine ikkagi töötama, eriti kui tegu on madala aktiivsusega andmebaasiga, kuid me ei saa andmete konsistentsust kuidagi garanteerida.

Väikese lisavaevaga saab sellest murest siiski üle, kasutades käsklusi mongodump ja mongorestore, mis on mõlemad Virtuaalserveris paigaldatud.

Järgmine käsklus loob andmebaasi varukoopia kausta ~/mongodump/mongodb_ABC (pane tähele, et –out parameetri puhul on näidatud ainult varukoopia põhikaust, sinna sisse tekitatakse siis igale varundatavale andmebaasile oma eraldi alamkaust).

mongodump \
 --host virt00000.loopback.zonevs.eu \
 --port 5678 \
 --db mongodb_ABC \
 --username mongodb_DEF \
 --password parool \
 --out ~/mongodump

Varundatud andmetega kausta võib siis juba kokku pakkida ja kuhugi kataloogi tõsta, kust meie korrapärane failide varundus selle üles korjab.

Loomulikult saab need operatsioonid automatiseerida meie pakutava Crontab funktsionaalsuse abil.

Kui ühel hetkel vajab andmebaas varukoopiast taastamist, siis saab seda teha käsuga mongorestore. Seekord peame sisendiks andma mitte varukoopia põhikausta ~/mongodump, nagu oli mongodump käsu juures, vaid juba varundatava andmebaasi kausta ~/mongodump/mongodb_ABC.

mongorestore \
 --host virt00000.loopback.zonevs.eu \
 --port 5678 \
 --db mongodb_ABC \
 --username mongodb_DEF \
 --password parool \
 ~/mongodump/mongodb_ABC

Taastamise korral tuleb arvestada, et juhul kui taastatavas kollektsioonis on mingid kirjed juba olemas, siis vaikimisi sama _id väärtusega kirjet üle ei kirjutata ja need jäävad nii nagu parasjagu on. Sellisel juhul tuleks olemasolevad kirjed kas ise eelnevalt eemaldada või kasutada täiendavalt argumenti --drop, mis enne andmete taastamist kustutab baasist ka varukoopias leiduvad kollektsioonid. Kollektsioonid, mida varukoopia ei sisalda, --drop ei puutu.

Kokkuvõte

Loodame väga, et MongoDB kasutusvõimalus avardab meie kasutajate ees valla olevaid võimalusi märkimisväärselt ja peagi kuuleme huvitavatest rakendustest, mis tänu sellele meie juures endale hea kodu leidnud.

Autor: Andris Reinman

Andris on üks Zone.ee infosüsteemi arhitekte. Lisaks arendab ta Zone MTA nimelist SMTP serverit (https://github.com/zone-eu/zone-mta), Node.js e-posti moodulit Nodemailer (https://nodemailer.com) ja postiloendite haldamise tarkvara Mailtrain (http://mailtrain.org/)