Scuola Politecnica e delle Scienze di Base
Corso di Laurea in Ingegneria Informatica
Elaborato finale in Basi di Dati
Database NoSQL: RethinkDB
Anno Accademico 2015/2016
Candidato:
Giulia Barone
matr. N46002012
Indice
1 Introduzione
2
1.1
Strumenti utilizzati . . . . . . . . . . . . . . . . . . . . . . . . . .
3
1.2
Il passaggio dal modello relazionale al modello non relazionale . .
4
2 I sistemi NoSQL
2.1
7
La classicazione dei sistemi NoSQL
. . . . . . . . . . . . . . . .
3 RethinkDB
9
12
3.1
I punti forza e i casi d'uso di RethinkDB . . . . . . . . . . . . . .
12
3.2
Le idee di progettazione
13
3.3
L'architettura di RethinkDB
3.4
Il modello dei dati
3.5
Le tecniche di relazione tra i documenti
3.6
3.7
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
14
. . . . . . . . . . . . . . . . . . . . . . . . . .
15
. . . . . . . . . . . . . .
17
Il linguaggio ReQL . . . . . . . . . . . . . . . . . . . . . . . . . .
18
3.6.1
Il parallelismo . . . . . . . . . . . . . . . . . . . . . . . . .
18
3.6.2
Query funzionali e avanzate . . . . . . . . . . . . . . . . .
19
3.6.3
Gli indici secondari . . . . . . . . . . . . . . . . . . . . . .
19
3.6.4
I campi nidicati . . . . . . . . . . . . . . . . . . . . . . .
21
3.6.5
L'operatore Join
. . . . . . . . . . . . . . . . . . . . . . .
21
3.6.6
Map-reduce e l'operatore lambda . . . . . . . . . . . . . .
24
3.6.7
Il changefeed
25
3.6.8
Query asincrone
. . . . . . . . . . . . . . . . . . . . . . .
25
3.6.9
Gli errori ReQL . . . . . . . . . . . . . . . . . . . . . . . .
25
La Web-Console
3.7.1
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
I limiti di RethinkDB
26
. . . . . . . . . . . . . . . . . . . .
27
. . . . . . . . . . . . . . . . . . . . . . . . . . .
29
3.8
Le API di ReQL
3.9
Installazione di RethinkDB
. . . . . . . . . . . . . . . . . . . . .
34
4 Progettazione sica del database
37
5 Conclusioni
39
1
1 Introduzione
Lo studio di questo lavoro di tesi riguarda i database non relazionali, sviluppati
negli ultimi anni per poter gestire al meglio l'aumento esponenziale dei volumi
dei dati, in particolare il caso d'uso scelto è stato il database NoSQL RethinkDB.
Al giorno d'oggi c'è una grande diusione di database relazionali, per questo
motivo si sono volute prima analizzare le cause del passaggio da un modello a
un altro, e quali sono state le conseguenze.
La domanda che viene posta, è quando eettivamente conviene utilizzare i database non relazionali, e se i miglioramenti apportati in alcuni indici di ecienza
e prestazioni vanno a discapito di altri.
Per dare una risposta a questo quesito, è stato necessario dapprima discutere in
generale dei database NoSQL, fornendo brevi informazioni storiche, esponendo
le varie tipologie, partendo dalle più primitive a quelle più evolute e diuse al
giorno d'oggi. In seguito è stato possibile presentare RethinkDB, indicandone
le caratteristiche, le funzionalità supportate, l'architettura, il modello dei dati
e il linguaggio di query ReQL.
RethinkDB è un database open-source orientato ai documenti, memorizzati
secondo lo standard JSON. E' stato progettato per fornire, agli utenti delle
applicazioni che ne fanno uso, aggiornamenti real-time delle modiche sui dati
contenuti. E' ancora in piena attività di sviluppo, l'ultima versione rilasciata è
stata a Maggio 2016, Fantasia 2.3.2, ed è installabile su ambienti OS X, Unix
e Linux. Al momento è disponibile anche una versione in fase di modica per
Windows.
Al ne di entrare maggiormente in contatto con questa analisi, si è fornita
una mini-guida per l'installazione di tutti gli strumenti necessari all'utilizzo di
RethinkDB, e inoltre, a titolo di esempio, si è progettato un semplice database,
da me proposto, di gestione voli di una compagnia aerea, con il quale si sono
implementate diverse funzionalità di manipolazione dati.
2
1.1 Strumenti utilizzati
Ubuntu 15.04
E' il sistema operativo sul quale sono state svolte le attività di creazione database
e manipolazione dati.
Questa scelta è stata fatta perchè a dierenza di altri ambienti, la versione di
RethinkDB per Unix e Linux è la più performante.
RethinkDB 2.3.2
E' la versione del database utilizzata per le attività di manipolazione dati.
Node.js
E' una piattaforma event-driven per JavaScript, su ambient Unix, che utilizza
meccanismi leggeri ed ecienti per la gestione di operazioni di I/O.
L'insieme dei package di Node.js, npm, è tra le librerie open-source più diuse
al mondo.
Javascript
E' un linguaggio di scripting orientato agli oggetti e agli eventi, utilizzato
specialmente per la gestione delle applicazioni lato-client.
Nato nel 1995, è stato standardizzato solo due anni più tardi, l'ultimo rilascio
risale a giugno 2015.
E' stato utilizzato il driver ucile di JavaScript fornito da RethinkDB.
3
1.2 Il passaggio dal modello relazionale al modello non
relazionale
Con l'avvento dell'era moderna non esiste attività che potrebbe essere svolta
senza usufruire di una base dati. Un database è una collezione di dati che viene
gestita da un DBMS, DataBase Management System (Sistema di Gestione di
Basi di Dati), un software specico che si interpone fra l'utente e i dati veri e
propri, in modo tale che non debba essere l'applicazione stessa ad accedere ai
dati, garantendo un alto livello di indipendenza fra il livello sico e il livello
applicativo. In qualsiasi applicazione che fa uso di un database, è necessario che
le informazioni, dopo essere state prelevate, siano adattate al contesto in cui si
trovano.
Ma come funziona tutto questo?
Il meccanismo che permette il funzionamento di questo macro-sistema sono i
largamente diusi sistemi di gestione di basi dati relazionali, anche detti RDBMS
(dall'acronimo inglese).
Questo termine fu introdotto per la prima volta da Codd, nel 1970, in un
testo del seminario A Relational Model of Data for Large Shared Data Banks,
(Un modello relazionale di dati per grandi banche dati condivise). Il termine
relazionale è soggetto a diverse interpretazioni, fonte di larghi dibattiti fra
i maggiori teorici dei modelli dei dati, ma Codd chiarì da subito quello che
intendeva, con la stesura delle famose 12 Regole di Codd.
Tuttavia queste regole non furono mai rispettate neanche dai primi sviluppi
di questo genere, così vennero stabilite delle condizioni meno vincolanti per
poter denire un sistema RDBMS:
ˆ
i dati devono essere rappresentati sotto forma di relazioni (ciò che poteva soddisfare questa proprietà era la rappresentazione tramite tabelle a
schema sso)
ˆ
deve esserci la possibilità di manipolare i dati grazie a operatori relazionali
deniti e collegamenti (join)
Il linguaggio standardizzato adottato per interfacciarsi a una base dati relazionale è SQL (Structured Query Language), che oltre alla possibilità di interventi
di inserimento, aggiornamento e cancellazione, ore molteplici clausole e operatori, fra i quali gli operatori di congiunzione. Uno di questi ultimi, il Join, è, in
alcuni casi, una problematica dei RDBMS. Consiste infatti nella combinazione
4
di risultati provenienti da più tabelle, e per questo motivo non è mai consigliato
costruire tabelle con molti campi (colonne), oppure con molti elementi (righe),
poiché comporterebbe onerosi rallentamenti. Inoltre non è desiderabile che un
sistema abbia uno schema sso di tabelle, come invece impongono i RDBMS,
perché potrebbe capitare che alcuni elementi non possiedono informazioni riguardo a relativi campi, per cui è necessario riempire quello stesso campo con
un valore NULL, con conseguente spreco di risorse. Tuttavia questi vincoli sono
conseguenze del processo di normalizzazione sui dati, ossia evitare che ci siano
ridondanze e inconsistenze.
Quindi questi sistemi, seppur abbiano preso il sopravvento su molte applicazioni, hanno dei limiti che risultano inaccettabili al giorno d'oggi, in quanto
poco essibili, e inoltre cresce sempre di più la necessità di scalare orizzontalmente con il crescere dei dati, caratteristica che non appartiene ai RDBMS. La
scalabilità, in informatica, ingegneria del software e altre discipline, in generale,
è la capacità che ha un sistema di aumentare le proprie prestazioni in maniera
scalare rispetto alle proprie risorse. Esistono due tipi di scalabilità, ognuno con
vantaggi e svantaggi.
Scalabilità verticale (Scale up)
Si ottiene quando per aumentare le prestazioni di un sistema, si aumentano le
risorse all'interno di un singolo nodo del sistema, per esempio inserendo processori più potenti, aumentando la memoria e così via. Il vantaggio è che qualsiasi
modica compiuta in questo contesto, non inuenza il livello applicativo del nostro sistema, tuttavia, lo svantaggio è che spesso questa scelta comporta pesanti
costi, e quindi converrebbe di più acquistare una nuova macchina.
Scalabilità orizzontale (Scale out)
Si ottiene quando al sistema vengono aggiunte più macchine, in modo da
poter parallelizzare il lavoro su più nodi, senza dover aggiungere RAM o dischi
di archiviazione condivisi.
I vantaggi di questa soluzione consistono nella no-
tevole riduzione di costi aggiungendo nodi più economici, e nella garanzia di
un elevata fault-tollerance, ossia la tolleranza ai guasti, infatti la caduta di
un nodo non inuenza le attività degli altri nodi.
Lo svantaggio risiede nella
dicoltà di dover adattare le applicazioni a questo tipo di scalabilità, e far in
modo che non aumentino troppo i costi di installazione.
La scalabilità orizzontale, o anche scalabilità di carico, è la problematica
dei RDBMS, che non riescono a gestire la distribuzione del carico dei dati su
più macchine server, ed è proprio questo il motivo principale per cui è stata
5
progettata una nuova serie di sistemi, in grado di far fronte a questi limiti, i
sistemi NoSQL.
6
2 I sistemi NoSQL
Il termine NoSQL fu usato per la prima volta nel 1998, per una base di
dati open source che non usava SQL. Al contrario di quanto si possa pensare,
NoSQL non signica No SQL, ma Not Only Sql, difatti questo sistema può
eventualmente usare le relazioni tra i dati, ma trovando una via di uscita dai
rigidi vincoli imposti nei RDBMS, quali per esempio:
ˆ
necessità di operazioni di congiunzione (Join) per poter unire più informazioni prelevate da diverse tabelle
ˆ
rigidità dello schema, in quanto vengono imposte le tabelle a schema sso,
e inoltre qualsiasi modica in atto sul database, impone di bloccare l'intero
database no a che la modica non è completata.
Tutto questo si traduce nella dicoltà di ottenere una scalabilità orizzontale,
che comporta enormi costi computazionali nei database relazionali, cosa che invece non accade in un database NoSQL.
In che modo i sistemi NoSQL si liberano dai vincoli relazionali?
Semplicemente non rinunciano alle relazioni tra i dati, ma si assumono la
libertà di poter modicare le strutture dati o le relazioni tra i dati stessi, se
necessario. Per questo motivo i dati hanno una struttura meno astratta rispetto
alla tabellare dei RDBMS, e non sono soggetti al processo di normalizzazione.
Infatti questi sistemi hanno la capacità di replicare, distribuire e gestire i dati
su più nodi server (scalabilità orizzontale).
Per evidenziare le dierenze di approccio alle basi dati fra i sistemi RDBMS e
i NoSQL, le loro caratteristiche sono sintetizzate nei loro acronimi caratteristici.
ACID
Atomicity
(Atomicità): qualsiasi intervento su una base dati deve essere indi-
visibile, o totale o nullo
Consistency
(Coerenza): prima e dopo di qualsiasi intervento, la base dati
deve essere coerenti, ossia fra i dati archiviati non devono esserci contraddizioni
7
Isolation
(Isolamento): le operazioni su una base dati non devono interferire
fra loro
Durability
(Persistenza): dopo aver eettuato una modica, questa non deve
esser persa.
BASE
Basically-Available
(Facilmente disponibile):
il dato è sempre disponibile
anche in presenza di errori, ed è replicato su più nodi
Soft-State:
lo stato del sistema può variare nel tempo, anche senza determinati
input (a dierenze dell'hard state dei sistemi RDBMS)
Eventually-Consistent
(eventualmente consistente): non si sa quando, ma
entro un lasso di tempo, i dati arriveranno a uno stato consistente.
L'adozione di sistemi NoSQL, con il relativo abbandono di tecnologie RDBMS,
è stato fatto da molti marchi mondiali, quali Facebook, Amazon, Twitter, Linkedln.
Per comprendere il motivo di queste scelte, facciamo un breve passo
indietro all'anno 2000.
Le proprietà necessarie che un sistema distribuito deve rispettare per essere eciente, furono enunciante proprio in quell'anno, da Eric Brewer, in una
conferenza che riguardava i principi dei sistemi distribuiti.
Brewer enunciò il Teorema CAP:
Consistency
(Coerenza): la modica su un nodo deve raggiungere tutti gli altri
nodi
Availability
Partition
(Disponibilità): il sistema è sempre in grado di dare risposte
Tollerance (Tolleranze al partizionamento): se un nodo fallisce, non
inuenza il sistema, che invece continua a essere disponibile.
Prima dell'avvento di questi sistemi, i principali requisiti per rendere un RDBMS
eciente erano la coerenza e la sicurezza dei dati. Tuttavia molte imprese, come
quelle sopra citate, avevano bisogno di un sistema che rendesse immediatamente
disponibili i dati, garantendo la tolleranza al partizionamento. Fu così che, tra
le 3 proprietà del CAP, si preferirono i sistemi che garantissero le proprietà AP,
8
availability e partition tollerance, a discapito della consistenza, che rispondessero subito alle richieste di altri client, rendendo il sistema però eventualmente
consistente. Ciò stava a signicare che apportando una modica al database,
non veniva bloccato l'intero sistema, ma quella stessa modica sarebbe stata recapitata dai nodi interessati entro un certo lasso di tempo. Quindi alcuni nodi
avrebbero potuto essere sedi di dati non consistenti, per questo motivo alla base
di questi sistemi è presente un ecace sistema di gestione dei conitti.
2.1 La classicazione dei sistemi NoSQL
Viste le dierenti esigenze a seconda del contesto, sono stati sviluppati vari
sistemi NoSQL con approcci leggermente diversi tra loro, ma sempre con gli
stessi principi di base.
ˆ
Key-Value
ˆ
Column-oriented
ˆ
Graph -oriented
ˆ
Document-oriented
Key-Value
E' l'implementazione più semplice e primitiva di un database NoSQL. Ad
ogni chiave viene associato un valore BLOB binario, di cui non si conosce il
contenuto, e può essere quindi di qualsiasi tipo, per questo è denito come un
sistema trasparente, poiché è possibile fare query solo sulle chiavi, ma non sui
valori. Possono essere dei sistemi molto performanti per alcune applicazioni che
necessitano di query semplici che restituiscono al massimo un valore, ma risultano poco utili quando c'è bisogno di fare operazioni più complesse, come per
esempio operare solo su una parte di un elemento.
Column-oriented
E' un'implementazione che prevede che i dati siano organizzati in righe e colonne, senza però dover denire le colonne a priori (schemaless). Memorizzare i
valori in ogni colonna consente di eettuare operazioni di lettura più veloci, poiché, anche su tabelle di grandi dimensioni, una query seleziona solo gli attributi
di interesse.
Inoltre ogni colonna ha un unico tipo di dati, quindi è possibile
9
applicare a ognuna di esse un dierente algoritmo di compressione dati, riducendo notevolmente lo spazio occupato. Un'altra caratteristica che permette a
questa implementazione di risparmiare la memoria, è che ogni riga può avere
un diverso numero di colonne, evitando così che tutte le altre abbiano i valori
nulli, e che le tabelle assumano dimensioni troppo grandi. Lo svantaggio è che la
scrittura di un elemento composto da più attributi, richiede tanti accessi quanti
sono i suoi attributi. Quindi questi sistemi sono i favoriti per basi dati di grandi
dimensioni e read-intensive.
Esempio di column-oriented database: Cassandra
Cassandra è un DBMS non relazionale distribuito sotto licenza Apache open
source, inizialmente sviluppato all'interno di Facebook, e poi diuso anche in
molte utenze, tra le quali Twitter, Digg e Netix. Adotta una struttura di memorizzazione di tipo chiave-valore. A ogni chiave corrispondono dei valori, che
sono raggruppati in famiglie di colonne. Alla creazione di una base dati viene
denita una famiglia di colonne, che tuttavia può essere ampliata in qualsiasi
momento, infatti una colonna viene aggiunta solo se viene specicata la chiave,
così a ogni chiave possono corrispondere diverse colonne di una famiglia di colonne.
Graph oriented
E' una tipologia di database che fa riferimento alla teoria dei gra, infatti
utilizza nodi e archi per rappresentare e archiviare l'informazione. Questo tipo
di rappresentazione è un gran vantaggio, perchè permette di utilizzare queste
implementazione quando c'è bisogno di gestire dati mutevoli su schemi evolutivi, a dierenza dei RDBMS che sono più veloci nelle rappresentazioni a schema
sso. E' in grado di gestire dati fortemente interconnessi tra loro con l'attraversamento, una query che stabilisce come passare da un nodo all'altro attraverso
le relazioni.
Document-oriented
E' l'evoluzione del key-value, ma più essibile. I dati vengono memorizzati,
invece che in tabelle, in documenti che possono avere inniti campi di illimitata
lunghezza. A ogni documento è possibile accedere tramite una chiave, che può
essere una stringa o un path, e il contenuto può essere di vario tipo. Per facilitare la ricerca il database mantiene anche un indice delle chiavi, in modo da
recuperare velocemente il documento di interesse.
Nonostante le somiglianze, ci sono alcune dierenze principali con l'implementazione key value:
10
ˆ
il supporto a indici secondari: ogni documento può incorporare al suo interno altri documenti, permettendo di rappresentare relazioni gerarchiche
molto complesse
ˆ
interrogazioni ad hoc: grazie a delle apposite API o a un linguaggio di
query, è possibile eettuare attività di recupero, facendo nel database delle ricerche ad hoc, basandosi sul valore di un determinato campo del
documento.
Esempio di document oriented database: RethinkDB
RethinkDB è un database open source annunciato nel 2009, distribuito con
licensa AGPL, e usa lo standard JSON per la memorizzazione di documenti,
Ciò che lo contraddistingue dagli altri database è che ha sull'applicazione un
eetto real-time, è possibile infatti riportare le modiche eettuate dalle query,
o i risultati scaturiti da esse, quanto prima possibile, riducendo i ritardi dovuti
alla scalabilità. Nonostante la sua dierente implementazione, ha un linguaggio
di query molto semplice, che lo rende adattabile alle applicazioni.
11
3 RethinkDB
RethinkDB è un database orientato ai documenti, open source, distribuito
sotto licenza AGPL, nato nel 2009. La prima versione fu distribuita nel 2012, e
nel maggio 2016 è stata rilasciata l'ultima versione stabile, la 2.3.2, Fantasia,
al momento registra circa 100000 utenti ed è il 43esimo database più popolare al
mondo. Con lo sviluppo di RethinkDB è stato fatto un passo molto importante
1
verso la gestione delle applicazioni real-time.
Dal punto di vista dei maggiori marchi mondiali (Google, Amazon, Twitter),
è complicato gestire il traco di informazioni con i database tradizionali, che
pur garantendo la consistenza e la sicurezza dei dati, non garantiscono la scalabilità e la disponibilità. La priorità è di avere un sistema che fornisce a tutti
gli utenti una risposta entro un certo tempo denito, real-time, e che riesca a
gestire un usso enorme di dati ogni secondo. RethinkDB ore un'alternativa
che può essere considerata l'equivalente del lato server di un lettore RSS, infatti
permette di apportare gli aggiornamenti alle applicazioni nel modo più veloce
possibile, e renderli immediatamente visibili agli utenti.
3.1 I punti forza e i casi d'uso di RethinkDB
ˆ
Modello orientato ai documenti
Ore i notevoli vantaggi tipici di queste implementazioni, quali la scalabilità
orizzontale, e la possibilità di gestire qualsiasi tipo di dato, il tutto riducendo
notevolmente la quantità di memoria utilizzata. Grazie al supporto ai documenti
nidicati, è possibile costruire relazioni gerarchiche molto complesse.
ˆ
Semplicità d'uso
E' dotato di un linguaggio di query molto semplice, ReQL, nato appositamente
per poter rendere questo modello più adattabile a tutte le applicazioni. Inol-
1
In informatica il termine real-time si usa per indicare i sistemi o le applicazioni, la
cui ecienza dipende non solo dalla correttezza dei risultati ma anche dai tempi di risposta.
Tuttavia questo non signica che un sistema real-time debba essere necessariamente veloce,
l'importante è che garantisca una risposta entro un tempo stabilito che dipende dal tipo di
computazione.
12
tre fornisce delle API molto semplici ed intuitive, e un facile meccanismo di
installazione.
ˆ
Linguaggi supportati
Fornisce dei driver uciali per alcuni dei linguaggi di programmazione più
comuni, quali Java, Python, Ruby e JavaScript.
ˆ
No vincoli hardware
Non ci sono particolari requisiti hardware, infatti il server di RethinkDB è scritto
in C++ ed è supportato da qualsiasi macchina a 32/64 bit, con sistema Linux
e OSX 10.7 in poi.
ˆ
Atomicità delle operazioni di scrittura sui documenti
Quando usare RethinkDB?
I casi d'uso che rendono l'architettura di RethinkDB una valida alternativa
sono:
ˆ
applicazioni web e mobile che richiedono un veloce interfacciamento con
l'utente
ˆ
applicazioni che devono gestire grossi ussi di dati
ˆ
giochi multiplayer
ˆ
dispositivi di connessione
3.2 Le idee di progettazione
Lo scopo del team di RethinkDB era di creare un'architettura che rendeva
le applicazioni real-time estremamente semplici, era considerata una vera e propria sda ingegneristica. Per questo motivo c'era bisogno di un approccio non
relazionale, per permettere la scalabilità orizzontale, proprietà che gestiva al
13
meglio i grandi ussi di dati. Quindi nelle idee base di progettazione c'era quella di creare un sistema che, invece di consultare il database per ogni modica,
rendeva disponibili le modiche apportate al database in real-time. La prima
soluzione a questo problema fu quella di consultare periodicamente il database,
ma non andò mai in porto perchè comportava innumerevoli problemi, come per
esempio la concorrenza fra più utenti connessi sul server, e i tempi di consultazione che provocavano un forte sovraccarico.
Il progetto di un sistema per
applicazioni real-time tutto nuovo, portò a dover implementare tutto partendo
da zero, compreso il motore di esecuzione delle query, il sistema distribuito e
di memorizzazione, e tutti i componenti per il database.
Per questo motivo
RethinkDB fu scritto in C++, nel giro di circa 5 anni.
3.3 L'architettura di RethinkDB
Lo sharding e la replicazione dei dati
RethinkDB utilizza lo sharding, un algoritmo che consente di distribuire i
dati di un database su più nodi del sistema distribuito, in modo tale da formare
un cluster di database. Lo sharding è chiamato anche partizione orizzontale, e
consente nei sistemi a scalabilità orizzontale, di poter partizionare il database in
due o più macchine, in modo tale da avere un cluster di database. E' dierente
dal partizionamento verticale, che invece consente di partizionare i dati, non
distribuendoli su più nodi, bensì dividendoli in più entità a seconda del contesto
d'uso. Quest'ultimo tipo di partizionamento è più utilizzato nei modelli relazionali. L'utente indica in quanti shard (letteralmente: cocci) vuole dividere
la sua base dati, e di quante repliche ha bisogno, ma non è compito dell'utente
indicare dove distribuire i dati, e su quali server porre le repliche. Sarà l'algoritmo di sharding, che attraverso dei meccanismi euristici, porrà le repliche dei
dati nei primi server disponibili. La distribuzione avviene invece uniformemente, individuando il giusto punto di rottura per cercare di porre su ogni pezzo
del cluster lo stesso numero di dati, basandosi solo sulle chiavi primarie. Nel
caso in cui ci siano più ricorrenze su un nodo, gli shard potrebbero sbilanciarsi,
allora è compito dell'utente distribuire meglio i propri dati.
Nonostante questa operazione non possa essere eettuata manualmente, per
poter consentire un maggior controllo sul sistema, i cluster in RethinkDB sono
organizzati in 3 livelli:
ˆ
Il primo livello si occupa dell'implementazione dell'infrastruttura distribuita, della replicazione delle copie su più server e della gestione delle
query
14
ˆ
Il secondo livello si occupa dei meccanismi euristici, delle attività di sharding e di replicazione secondo i vincoli specicati dall'utente.
ˆ
Il terzo livello fornisce all'utente strumenti di controllo dei cluster web
based e a linea di comando.
Anchè questa tecnica avvenga al meglio, RethinkDB possiede al suo interno
un tracking aggiornato automaticamente, che tiene traccia dello stato del cluster, quanti server sono disponibili, quanti dati sono memorizzati su ognuno e
così via. Per semplicare questo meccanismo, a ogni server può essere associato
da uno a più tag server, e l'utente attraverso i comandi ReQL può congurare
le proprie tabelle:
r.table('A').reconfigure(shards=2,replicas={'nord':2,'sud':2})
2
Il codice al di sopra permette di settare lo sharding e la replicazione.
La
tabella A viene divisa in due shards, che saranno scelti automaticamente dal sistema indipendentemente dall'utente. Le repliche dei dati di interesse dovranno
essere posizionate due nel server indicato col tag nord e due in quello indicato
come sud.
3.4 Il modello dei dati
Il database orientato ai documenti fornisce molti vantaggi al sistema di
RethinkDB, e permette di poter attuare la scalabilità orizzontale.
RethinkDB permette la gestione di molti tipi di dati, come interi, stringhe,
array, booleani e oggetti JSON.
JSON, dall'acronimo inglese Java Script Object Notation, è un semplice e
leggero formato per lo scambio di informazioni tra applicazioni client-server.
E' molto semplice da imparare e da usare, ed è completamente indipendente
da qualsiasi linguaggio di programmazione, pur mantenendo costrutti sintattici
molto simili a Java, JavaScript,C, C++, C#, Python e molti altri. Per poterlo
rendere adattabile a qualsiasi linguaggio di programmazione, si basa su due
strutture dati universali: coppie chiave valore, che si avvicina molto al concetto
di oggetto e ordinate liste di valori, che possono essere vettori, liste, sequenze o
altro.
In RethinkDB, il documento è un insieme di chiavi alle quali sono associati
dei valori.
2 Esempio
I valori possono essere di vario tipo, interi, stringhe, array, indici
di codice ReQL: per approfondimenti si rimanda alla sezione sul linguaggio
15
di altri documenti, array di documenti e così via.
Di seguito un esempio di
documento secondo lo standard JSON.
{
id: 1,
nome: John,
cognome : Smith
}
Un oggetto è denito nelle parentesi grae di apertura e chiusura. La chiave
è separata dal valore attraverso :, e i valori sono separati tramite una virgola ,.
Se questo oggetto fosse stato rappresentato in un database relazionale, sarebbe
stato opportuno dividerlo in più entità, e poi per poter ottenere una rappresentazione aggregata come questa ci sarebbe stato bisogno di eettuare un'operazione
di Join, che per tabelle di grandi dimensioni può portare rallentamenti.
Grazie invece a un database non relazionale orientato ai documenti, come
RethinkDB, è possibile:
ˆ
creare oggetti senza il vincolo di dover rispettare schemi ssi, come invece
avviene dei database relazionali, che impone di riempire con NULL i campi
senza valore
ˆ
rappresentare un oggetto mediante un unico documento, senza prima dover creare opportune tabelle e stabilire relazioni tra loro, questo permette
di poter inserire anche array all'interno degli oggetti stessi
ˆ
rappresentare l'aggregato di un oggetto facendo riferimento a un solo
documento, senza dover eettuare operazioni di join
Inoltre RethinkDB supporta altri formati specici di dati come:
Tabelle:
vengono usate quando si fa uso degli indici, dove a ogni elemento
della tabella corrisponde un documento JSON.
Stream:
sono come degli array, ma si comportano diversamente. Sono a sola lettura, poiché il loro meccanismo impone che al ritorno da una
funzione restituiscono un cursore che punta all'insieme di risultati. Quest'ultino non viene letto tutto in una volta, ma è possibile
scorrere un elemento alla volta a ogni iterazione.
16
Selection:
è il sottoinsieme di una tabella, eventualmente restituito con comandi come lter o get. Il risultato può essere di tipo Object,
Stream o Array. Può essere a lettura o a scrittura, infatti il risultato di queste operazioni di ltraggio, prelievo o simili, possono essere
passati anche a altri metodi come parametri di input, per poi essere
ulteriormente elaborati.
Pseudotipi: si basano sul formato di altri tipi, come per esempio i Binary Objects, simili ai BLOB in SQL (ossia campi utili alla memorizzazione di
immagini, le ecc), tipi di dati geometrici, tipi di dati raggruppati,
e temporali. Quest'ultima caratteristica è nativa di RethinkDB, con
precisione al millisecondo.
3.5 Le tecniche di relazione tra i documenti
All'esecuzione di una query, possono esserci due meccanismi in RethinkDB
che permettono di ricavare le informazioni desiderate.
ˆ
Array concatenati
ˆ
Tabelle multiple
Array Concatenati
Riettono maggiormente i principi di base dei database non relazionali, infatti se a vari elementi simili tra loro corrispondono informazioni di vario genere,
queste vengono rappresentate tutte in un unico documento per ogni elemento.
Il vantaggio è che per leggere l'informazione riguardo una tupla, non c'è bisogno di relazionarsi a diverse tabelle mediante meccanismi complessi, ma è tutto
contenuto in un unico documento. Purtroppo questo presenta anche molti svantaggi: per apportare modiche al documento è necessario prima caricarlo, poi
modicarlo, salvarlo e caricarlo nuovamente sul disco.
Tabelle multiple
Si avvicina molto alle tecniche che contraddistinguono i database relazionali.
Data una base dati, in questa vengono individuate delle entità che rappresentano
concetti separati tra di loro, e poi viene attivata un'operazione di linkaggio tra
i documenti. Questo avviene tramite i comandi join. (si rimanda alla sezione
sul linguaggio).
17
3.6 Il linguaggio ReQL
ReQL è il linguaggio di query in RethinkDB, ottimizzato per poter facilmente
manipolare documenti JSON. La sua semplicità d'uso è sicuramente un gran
vantaggio, che lo rende dierente da tutti gli altri linguaggi di database NoSQL.
Le principali caratteristiche sono:
ˆ
Le query si incorporano all'interno del linguaggio di programmazione usato
senza dover prima creare oggetti JSON o costrutti specializzati per la
connessione al database.
ˆ
Il package rethinkdb contiene al suo interno i metodi implementati di
ReQL.
ˆ
Per eettuare la connessione al server non vengono utilizzate stringhe da
passare al server stesso, ma viene solo digitato il comando run.
ˆ
Le operazioni sono concatenabili: attraverso . vengono elencate le operazioni da fare su una tabella, con il vantaggio della semplicità di scrittura,
lettura e modica delle query stesse.
r.table('Volo').getALL()3
Le query anche se vengono costruite sul client, sono eseguite sul server attraverso
il comando run, e non richiedono intermediari fra il client e il server, aumentando l'ecienza del linguaggio.
3.6.1 Il parallelismo
ReQL supporta le query parallele. Più query possono essere eseguite su diversi processori e diversi server, prelevando dati da più database del cluster, ma
pur avendo un'unica query molto complessa, è possibile dividere ques'ultima
in piccoli pezzi, eseguire ognuno di questi su diversi server, e poi combinare i
risultati di ciascuno per ottenerne uno nale completo.
3 Esempio
di query concatenata che restituisce tutti gli elementi all'interno della tabella
Volo
18
3.6.2 Query funzionali e avanzate
ReQL supporta anche costrutti di query condizionali e strutturalmente più
avanzati, come per esempio le sub-query.
r.table('Volo').filter(r.row['prezzo'] <50).run(conn);
Dalla tabella Volo vengono restituti solo i voli con un prezzo minore di 50.
3.6.3 Gli indici secondari
In informatica, un indice è una struttura dati realizzata per migliorare la
ricerca all'interno di un database. Se una tabella non ha indici, per ogni query è
necessario leggere tutti i dati presenti nella tabella no a trovare quello desiderato, invece grazie agli indici, è possibili impostare un ordine di organizzazione tra
i dati, e così velocizzare le ricerche. Tuttavia hanno lo svantaggio di rendere più
lente le operazioni di modica e di scrittura, aumentando l'uso della memoria
di massa.
RethinkDB supporta diversi tipi di indici:
ˆ
Indici semplici
ˆ
Indici composti
ˆ
Indici multipli
ˆ
Indici basati su espressioni arbitrarie
Indici semplici
A dei singoli campi della tabella viene associato un indice per poter semplicare specialmente le operazioni in lettura.
r.table("Persona").indexCreate("last_name").run(conn);
r.table("Persona").between("Ryder","Smith").optArg("index", "last_name").run(conn);
Si consideri che la tabella Persona sia ordinata in ordine alfabetico. Al campo
last_name viene associato un indice, in modo tale che quando viene eettuata
l'operazione di ricerca, non c'è bisogno di scorrere l'intera tabella, ma solo gli
elementi intermedi ai due valori indicati, grazie all'organizzazione dei dati che
19
assicura che al di fuori di questo intervallo di valori non ci sono elementi corrispondenti alla ricerca.
Indici composti
E' un meccanismo che permette di associare un indice a un array di campi
della tabella di interesse, in modo tale da poter migliorare non solo le ricerche
sui singoli campi, come avviene con gli indici semplici, ma anche quelle su più
campi.
r.table("Persona").indexCreate("full_name",[r.row("nome"), r.row("cognome")]);
Ai campi
full_name,
nome
e
cognome
della tabella Persona, viene associato l'indice
così da poter eettuare ricerche sull'intero campo
full_name.
Indici multipli
Più documenti possono avere lo stesso indice, in modo da essere tutti restituiti in seguito a una query. Per fare un esempio, se al di sotto di una immagine,
poniamo dei tag che risaltano gli elementi fondamentali contenuti in essa, allora
abbiamo utilizzato una tecnica di indicizzazione multipla. Infatti con una query
apposita, viene restituita quell'immagine, ma anche tutte le altre presenti nel
database con lo stesso tag.
r.table("Aereo").indexCreate("tags").optArg("multi",true).run(conn);
Indici basati su espressioni arbitrarie
E' possibile creare un indice basandosi su un espressione arbitraria attraverso il passaggio a una funzione, che deve essere deterministica.
In pratica
l'operazione che viene svolta nella funzione, non deve rimandare ad altre query
o svolgere altri comandi che attuano un processo di iterazione, ma eettuare
solo l'assegnazione dell'indice.
r.table("Persona").indexCreate("full_name", function(persona_object){
return r.add(persona_object("cognome"), "_", persona_object("nome"))
}).run(conn, callback);
20
3.6.4 I campi nidicati
Un documento ReQL è un oggeto JSON, un insieme di coppia chiave-valore,
dove a ogni valore possono corrispondere altre coppia, oppure contenuti di vario
tipo, o una lista di valori e così via.
Questa è una query innestata, che ricorda un po' il path di un le system,
perchè per accedere a un campo innestato, è necessario accedere prima al suo
contenitore. E' necessario utilizzare l'operatore [ ].
r.db(CompagniaAerea).table(Persona).insert( { id:1, nome: John,
cognome: Smith, contatti : { "cell": "238976", "home": "08563"
} });
r.table('Persona').get(1)['contact']['phone']['work'].run(conn);
Al tentativo di accedere a un campo inesistente, verrà restituito risultato
nullo.
3.6.5 L'operatore Join
RethinkDB supporta i costrutti di join per prelevare i dati da più tabelle.
Invocando questo meccanismo, automaticamente i join operano sui componenti
del cluster, da ogni componente viene prelevato il dato di interesse, e poi vengono combinati i risultati. Può essere utilizzato in relazioni uno a molti e molti
a molti.
Uno a molti
E' una relazione dove, a un elemento della prima tabella, possono corrispondere molti elementi della seconda, ma a ogni elemento della seconda tabella
corrisponde un solo elemento della prima.
Molti a molti
E' una relazione dove a ogni elemento della prima tabella possono corrispondere molti della seconda e viceversa. In questo caso il risultato è come quello
della relazione uno a molti, ma viene ritornata una sequenze di documenti per
ogni corrispondenza.
In generale, sono supportati 3 tipi di join:
ˆ
equal join
21
ˆ
inner join
ˆ
outer join
Equal Join
sequence.eqJoin(leftField, rightTable) sequence
E' l'operazione più eciente di congiunzione, e anche la più veloce.
I do-
cumenti sono restituita a coppie, dove per ogni coppia signica che il campo
specicato a sinistra esiste, non è nullo e ha una corrispondenza uguale nella
tabella specicata a destra.
Esempio di equal Join
Supponiamo di avere due tabelle
Elenco di passegeri
[
{ id:
1, 'nome':
'John', 'cognome': 'Smith', idVolo:
1 },
{ id:
2, 'nome':
'Mark', 'cognome': 'Shelly', idVolo:
2},
{ id:
]
3, 'nome':
'Will', 'cognome': 'Ryder' },
Elenco di voli
[
{ 'id':
{ 'id':
]
1, 'andata':
2, 'andata':
'Rome', 'ritorno': 'Milan' },
'London', 'ritorno': 'New York' },
Vogliamo un elenco di tutte le persone che hanno prenotato qualsiasi volo.
Il codice ReQL è il seguente:
r.table('Persona').eqJoin('idVolo', r.table('Volo')).run(conn, callback);
22
E ritorna questa sequenza di valori:
[
{ left" : { "idVolo" : 1, "id" : 1, "nome" : "John" },
"right" : { "id" : 1, 'andata': 'Rome', 'ritorno': 'Milan' }},
{ "left" : { "idVolo" : 2, "id" : 2, "nome" : "Mark" },
"right" : { "id" : 2, 'andata': 'London', 'ritorno': 'New York'
}
}]
Si nota che il risultato di ogni tabella viene restituito separatamente, indicando prima i campi che hanno permesso di trovare la corrispondenza, e poi gli altri.
Inner Join
sequence.innerJoin(otherSequence, predicate_function) stream
Questa operazione eettua un confronto tra le tabelle coinvolte. Ogni riga
di una tabella viene confrontata con quella dell'altra, e aggiunte allo stream di
ritorno solo se soddisfano una determinata proprietà espressa in una funzione.
Può anche essere applicata ad array.
Esempio di inner join
r.table('Volo').innerJoin(r.table('Persona'), function(volo, persona)
{ return volo.(id).eq(persona('id')) }).zip().run(conn, callback);
Ogni riga della tabella Volo viene confrontata con quella della tabella Persona e restituite solo se soddisfano la proprietà all'interno di function, ovvero in
questo caso eq, che restituisce gli elementi solo se i campi indicati sono uguali
fra loro.
Outer Join
sequence.outerJoin(otherSequence, predicate_function) stream
array.outerJoin(otherSequence, predicate_function) array
Questa operazione di join è molto simile alla Inner Join, con la dierenza
che ritorna solo i documenti appartenenti alla tabella passata come argomento,
e che rispettano la proprietà specica in predicate_function. E' più lenta delle
altre perchè si potrebbe ottenere lo stesso risultato mediante concatenazione di
23
altri comandi, infatti è sconsigliato usarla se possibile.
3.6.6 Map-reduce e l'operatore lambda
E' un modo di combinare tutti i risultati provenienti dalle operazioni fatte su
diversi componenti del cluster. I passi importanti per un'operazione di questo
tipo sono:
ˆ
opzionalmente dividere la sequenza di operazioni in più gruppi
ˆ
un'operazione di map che trasforma e ltra gli elementi di una sequenza
in una nuova sequenza
ˆ
un'operazione di reduce che raggruppa tutti i valori restituiti in un unico
valore.
RethinkDB adotta il modello GMR (group-map-reduce), tutti eseguibili con i
rispettivi comandi su un'unica linea di codice ReQL.
I comandi GMR
Supponiamo di voler contare quanti aerei sono presenti per categoria nella
nostra base dati.
E' necessario prima raggruppare tutte le categorie (group),
poi associare a ogni elemento il valore 1 (map), e poi sommare tutti gli elementi
appartenenti a una certa categoria (reduce).
r.table('Aereo').group(lambda aereo: aereo['tipo']).map( lambda
aereo: 1).reduce(lambda a, b: a + b).run(conn);
Lambda
è un costrutto sintattico che permette di eettuare operazioni di
ltraggio. In questo esempio, nella funzione
a una tipologia viene associata alla variabile
group, ogni aereo che appartiene
aereo, nella funzione map a ogni
aereo di un determinato gruppo viene fatto corrispondere il valore 1.
nella funzione
reduce
vengono dichiarate due variabili,
sempre aggiornata tramite la funzione
a+b,
associati a ciascun aereo di ogni gruppo.
24
a
e
b,
di cui
b
Inne
viene
che corrisponde a sommare i valori
3.6.7 Il changefeed
E' un meccanismo fornito da ReQL che permette di sottoscrivere l'utente
alle notiche riguardanti una tabella indicata. Il costrutto sintattico è:
r.table('Persona').changes().run(conn, function(err, cursor) {
cursor.each(console.log);
})
Se la tabella user viene modicata, l'utente non deve andare a controllare
qual'è stata l'ultima modica, ma gli viene fornita automaticamente dal cursore
che è ritornato dal comando change(). Questo infatti punta ad alcuni documenti
contenenti solo il valore precedente alla modica (old), e quello nuovo (new).
In seguito a un inserimento in tabella, il risultato sarà questo:
{ old_val: null,
new_val: {id: 1, nome:
}
'Jonh', cognome: 'Smith'}
3.6.8 Query asincrone
Alcuni driver di RethinkDb, come Javascript, supportano le query asincrone. In generale, un'operazione asincrona su un database, permette di fare una
richiesta al server, senza bloccare il programma chiamante, evitando in tal modo
l'uso di thread per la connessione. In JavaScript questo viene implementato in
modo molto semplice attraverso l'uso del comando
run().
3.6.9 Gli errori ReQL
RethinkDB supporta tre classi di errori, tutte derivanti dalla classe principale
ReqlError.
ReqlDriverError:
è una classe di errori che riguardano il driver.
In questi
casi potrebbero esserci bug nel driver stesso, oppure un comando non
riconosciuto.
ReqlRuntimeError:
E' la classe padre di tutti gli errori a tempo di esecuzione,
e quando viene riconosciuta questa tipologia di errore, in generale vengono
restituire le classi glie. Queste possono riguardare errori di memoria, di
timeout, di operazioni matematiche, di risorse inesistenti, o di operazioni
logicamente non possibili.
25
ReqlCompileError:
Sono gli errori avvenuti a tempo di compilazione.
Le
cause possono essere errori sintattici, parametri non conosciuti o altro. Il
server in questo caso non riesce a compilare il codice inviato, quindi restituisce errore.
3.7 La Web-Console
RethinkDB fornisce un'interfaccia web per permettere all'utente di controllare le attività del server, le distribuzioni dei dati sui cluster, le esecuzioni delle
query e molto altro. Grazie a questa interfaccia molto user-friendly, è possibile
anche manualmente creare e manipolare database e tabelle. Dopo aver avviato
rethinkdb, ci indirizziamo all'indirizzo
http://localhost:8080
, dove 8080 è
il port number di default per la connessione HTTP.
Notiamo che sono presenti diverse sezioni.
ˆ
Dashboard:
consente di controllare quanti server sono connessi, le tabelle
presenti nel database, gli indici, le risorse usate, e le performance di un
eventuale cluster secondo per secondo.
ˆ
Table:
ore la possibilità di creare database e tabelle tramite pulsante,
quindi l'interfaccia ci esonera dalla scrittura di codice per quanto riguarda
26
queste operazioni di creazione e cancellazione.
ˆ
Server:
ˆ
DataExplorer:
fornisce informazioni riguardo tutti i server connessi al cluster.
permette la scrittura di codice e la manipolazione dei dati
sul database. RethinkDB fornisce anche un ottimo correttore ortograco
e di suggerimento, controllando a ogni parola scritta, la correttezza della
sintassi.
ˆ
Logs:
espone un report di tutti gli eventi avvenuti sui server.
In questa schermata, è stato fatto un inserimento nella tabella Persona, con
un campo nidicato, contatti.
Cliccando su Run, viene fornito una sorta di report dell'operazione eseguita
dal server.
3.7.1 I limiti di RethinkDB
Uno dei punti forza di RethinkDB è di non avere grossi requisiti hardware
anchè funzioni.
Tuttavia ci sono delle piccole limitazioni che è bene prendere in considerazione:
ˆ
Non è possibile creare più di 64 shards per database, tuttavia il numero
di database e di tabelle interne al database è illimitato.
27
ˆ
Ogni tabella richiede circa 10MB di spazio sul disco, se vuota ne richiede almeno 4MB. Ogni replica distribuita per il cluster occupa 8MB di
memoria RAM.
ˆ
Nonostante sia possibile scrivere documenti di lunghezza illimitata, è buona abitudine non superare i 16MB per questioni di ecienza.
ˆ
L'uso della memoria
Facciamo delle brevi considerazioni riguardo questo ultimo punto: l'uso della
memoria.
Ciò che occupa maggiormente memoria è l'esecuzione delle query e la loro
distribuzione, una cache interna, e i metadati.
Al momento dell'attivazione di RethinkDB, viene messa a disposizione una
piccola quantità di memoria per i dati recenti, una cache, in modo da dover
eettuare meno accessi al disco.
La dimensione della cache interna dipende dalla quantità di spazio disponibile, infatti nel conteggio si considera anche la cache del sistema operativo su
cui gira la macchina, poichè se necessario viene liberata anche questa. Nel caso
che lo spazio non si suciente, di default vengono assegnati 100MB, in ogni caso
è possibile settarla automaticamente.
Invece riguardo i metadati, questi non sono altro che dei blocchi di dati che
vanno dai 512 bytes ai 4MB, utilizzati per garantire un accesso veloce ai dati.
Lo svantaggio è che l'overhead del database è dovuto in buona parte all'uso
dei metadati, se presenti.
Quindi...
Quando non usare RethinkDB?
Esiste ancora tutt'oggi il problema di installazione delle applicazioni, che può
essere anche molto onerosa, e se non è possibile mettere in pratica la scalabilità
orizzontale, si perde uno dei tanti vantaggi dei database non relazionali.
Infatti non tutte le macchine supportano database distribuiti, un eventuale
upgrade sarebbe troppo costoso, e certamente questo non è un buon requisito
se l'obiettivo è di creare un'applicazione alla portata di tutti, diusa tra milioni
di utenti.
Inne, in sistemi in cui è importante che i dati siano consistenti, a discapito
della velocità di secuzione delle query, è meglio utilizzare database relazionali.
Questi ultimi, grazie al processo di normalizzazione, mantengono una buona
consistenza di dati, senza repliche e ridondanze.
28
3.8 Le API di ReQL
Di seguito verranno mostrati solo alcuni dei principali comandi del linguaggio ReQL, per la creazione di database, tabelle, avvii di connessione, modiche,
cancellazioni, ricerche e manipolazioni dati.
require
var r = require('rethinkdb');
Essendo tutti i metodi delle API applicabili a oggetti di tipo database rethinkdb, allora è possibile semplicare la lettura di una query associando una
variabile al nome del database, tramite il comando require.
run
query.run(conn [, options], callback);
query.run(conn[, options]) promise
E' utilizzato per inviare la query al server.
query concatenate, il
run()
In un comando composto da
viene posto sempre in ultimo.
I parametri di interesse sono:
conn:
è il parametro di connessione
options:
stabilisce in che modo debba essere eettuata la connessione, ed è
un parametro opzionale.
Sono presenti molte scelte, ne consideriamo alcune tra le principali:
read-mode:
se è di tipo single, ritorna solo i valori che non hanno repliche sui
server, di tipo majority, ritorna i valori che hanno il maggior numero
di repliche sui server.-
timeFormat:
arrayLimit:
indica in che formato deve essere restituito il temporali
il massimo numero di array che possono essere ritornati dal risul-
tato della query
groupFormat:
il tipo di formato in cui devono essere raggruppati i dati restituiti
29
durability:
riguarda il trattamento delle operazioni al momento della ricezio-
ne. Può essere hard o soft. In caso di durability soft, se si invia per
esempio un'operazione di scrittura, questa viene immediatamente
accettata, ma non scritta sul disco nello stesso momento.
Nel caso che non siano previsti valori di ritorno delle query, cioè le callback,
viene ritornato un oggetto Promise.
La
callback in generale, corrisponde a una chiamata a funzione.
Indica una
funzione che viene passata come argomento alla funzione chiamante, in modo
tale che quest'ultima, possa eseguire una porzione di codice ad essa sconosciuta,
che in realtà viene eseguita dalla funzione passata come argomento.
I risul-
tati delle elaborazioni della funzione chiamata vengono restituiti alla funzione
chiamante.
Promise
è un oggetto del linguaggio JavaScript.
In una connessione asin-
crona client-server, permette al server di inviare una promessa al client, cioè
che per qualsiasi richiesta ricevuta invierà sicuramente un valore di ritorno, che
può essere di qualsiasi tipo. Per esempio in caso di fallimento viene restituita la
causa, oppure un valore di ack che indica lo stato dell'esecuzione e così via. Gli
oggetti possono essere inviati in un qualsiasi momento futuro della connessione
client-server.
connect
r.connect([options, ]callback);
Crea la connessione al server, e nell'argomento opzioni si hanno diverse scel-
host, port
password e timeout
te come
(di default 28015),
database, user
(di default
admin),
per l'apertura di una connessione (di default 20). Even-
tualmente può anche essere ritornato un oggetto Promise.
close
conn.close([{noreplyWait:
true}, ]callback);
Si invia questo comando quando si desidera chiudere la connessione.
Nel
parametro opzioni è possibile scegliere di avere una reply o meno, impostando
rispettivamente a true o false.
30
use
conn.use(dbName);
Sceglie il database da consultare per eseguire le query inviate al server.
next
cursor.next()->promise array.next()->promise
Prende il prossimo elemento in un array o in una lista.
each
cursor.each(callback[, onFinishedCallback]);
Esegue una serie di operazioni sul prossimo elemento di un array o di una
lista. La callback passata come primo argomento corrisponde al codice da eseguire su ogni elemento, invece la callback opzionale corrisponde a un eventuale
funzione da eseguire quando ogni elemento nisce di essere processato.
dBCreate
r.dbCreate(dbName) object
Si utilizza per creare il database su cui si eseguiranno le query.
tableCreate
r.tableCreate(tableName[, options]) object
Permette di creare le tabelle del database, che corrispondo a una collezione
di documenti JSON. I parametri opzionali possono riguardare la chiave primarie, gli eventuali shard o repliche della tabella e a modalità di scrittura, se deve
avvenire immediatamente o può rimanere in attesa.
indexCreate
table.indexCreate(indexName) object
Agli elementi di una tabella vengono associati gli indici in modo da facilitare
le operazioni di lettura e renderle più veloci.
Sono previsti anche parametri
opzionali riguardo la creazione di indici multipli.
31
insert
table.insert(object | [object1, object2, ...][, {durability:
object
"hard"}])
Inserimento di un oggetto, o di un'array di oggetti, nella tabella con eventuali parametri opzionali come per esempio la durability, riguardo la velocità di
accettazione della richiesta da parte del server.
update e replace
table.update(object | function ) object
Aggiorna un documento già presente in tabella sostituendone il valore presente. Viene passata una funzione se è necessario ricercare l'elemento da aggiornare tramite un campo identicativo, come la primary key.
possibile usare anche il comando
Eventualmente è
replace per poter rimpiazzare l'intero oggetto.
table.replace(object | function) object
delete
table.get(field).delete([{durability:
object
"hard", returnChanges:
Cancella una o più documenti da una tabella, specicandoli prima attraverso
un campo identicativo.
between
table.between(lowerKey, upperKey) table_slice
Restituisce una sotto tabella di cui il primo elemento corrisponde a quello
indicato nel primo valore, e l'ultimo a quello indicato nel secondo.
lter
selection.filter(predicate_function) selection
In una serie di elementi selezionati, restituisce solo quelli che rispettano il
predicato indicato.
32
false}])
withFields
sequence.withFields([selector1, selector2...])
stream
Restituisce solo gli elementi di una tabella che hanno valore non nullo per i
campi indicati.
concatMap
stream.concatMap(function) stream
Corrisponde all'aggiunta di un elemento a uno stream o a un array.
group
sequence.group(field | function) grouped_stream
Raggruppa gli elementi di una sequenza, o di una tabella, secondo il campo
indicato.
count
sequence.count([value | predicate_function]) number
Restituisce il numero di elementi nella sequenze indicata.
row
table.operation(r.row(field).operation());
Esamina tutti i documenti di una tabella solo facendo riferimento al campo
indicato. E' utile quando bisogna eettuare operazioni di selezione.
pluck
sequence.pluck([selector1, selector2...])
stream
Dato un documento, o una sequenza di documenti, estrae solo informazioni
relative ai campi indicati.
insertAt
array.insertAt(offset, value) array
33
Inserisci un elemento in un array specicando anche l'indice. Ritorna l'array
modicato.
3.9 Installazione di RethinkDB
RethinkDB ore package di installazione per piattaforme Ubuntu, Windows,
OS X, CentOS, Debian. Di seguito sarà descritta la procedura di installazione
su Ubuntu 15.04.
I passi fondamentali sono tre:
ˆ
Installazione delle repository di RethinkDB
ˆ
Installazione server
ˆ
Installazione client
Installazione delle repository di RethinkDB
La versioni fornite supportano hardware a 32/64 bit, con versione Ubuntu 12.0
in poi. Apriamo la shell di Ubuntu e inseriamo i seguenti comandi:
source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt
$DISTRIB_CODENAME main" |
sudo tee /etc/apt/sources.list.d/rethinkdb.list
wget -qO- https://download.rethinkdb.com/apt/pubkey.gpg |
sudo apt-key add - sudo apt-get update sudo apt-get install rethinkdb
Installazione server
Installiamo le dipendenze.
sudo apt-get install build-essential protobuf-compiler python \
libprotobuf-dev libcurl4-openssl-dev \
libboost-all-dev libncurses5-dev \ libjemalloc-dev wget m4
Dopo aver fatto ciò installiamo il pacchetto con estensione .tgz dal link passato a comando.
34
wget https://download.rethinkdb.com/dist/rethinkdb-2.3.5.tgz tar
xf rethinkdb-2.3.5.tgz
Ed ora possiamo installare il server.
cd rethinkdb-2.3.5 ./configure --allow-fetch make sudo make install
Installazione client
Possiamo scegliere fra molti driver uciali, come JavaScript, Ruby, Python
e Java. La procedura indicata farà riferimento a JavaScript. In questo caso sono
presenti alcuni requisiti, il primo è che sul calcolatore sia presente Node.js.
Node.js è una piattaforma event-driven per JavaScript, su piattaforma Unix.
Molti dei suoi moduli sono scritti in JavaScript, tuttavia non è un framework e
non gestisce la concorrenza tra processi. Si preoccupa solo di I/O event-driven.
Per la sua installazione su ubuntu, bisogna assicurarsi di avere un compilatore C++, previsto dalla shell bash di default di Ubuntu.
Installiamo i pacchetti essenziali:
sudo apt-get install build-essential checkinstall
sudo apt-get install libssl-dev
Adesso abbiamo bisogno del Node Version Manager, e lo scarichiamo dal
sito. E' uno script bash per aggiornare Node.js e gestirne più versioni. Alla conclusione di questa operazione, ci verrà chiesto di aprire e chiudere il terminale.
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh
| bash
Scarichiamo, compiliamo e installiamo node.js, indicando al nvm (Node Version Manager) quale versione usare.
nvm install 5.0
nvm use 5.0
Ora possiamo installare il driver con nmp, che è il package manager di JavaScript.
$ npm install rethinkdb
35
Adesso possiamo avviare il server di rethinkdb, aprendo il terminale e digitando a linea di comando:
rethinkdb --bind all &
Immediatamente ci verranno mostrati i parametri di connessione, il port
number di default 8080, e lo stato del server che è pronto a ricevere richieste.
36
4 Progettazione sica del database
Di seguito verranno riportati i comandi per la creazione delle tabelle costituenti il database.
Si tratta di un database che gestisce le prenotazioni di una compagnia aerea. Le informazioni memorizzate riguardano i Voli, le Persone che eettuano il
viaggio, gli Aerei e le Prenotazioni.
Creazione database e tabelle
r.dbCreate(CompagniaAerea);
r.db(CompagniaAerea).tableCreate(Persona);
r.db(CompagniaAerea).tableCreate(Volo);
r.db(CompagniaAerea).tableCreate(Aereo);
Inserimento elementi Persona
r.db(CompagniaAerea).table(Persona).insert( { id:1, nome: John,
cognome: Smith, contatti : { "cell": "238976", "home": "08563"
} });
r.db(CompagniaAerea).table(Persona).insert( { id:2, nome: Mark,
cognome: Shelly, contatti : [ phone: { "cell": "23146", "home":
"12345" }, email:{ "work": "[email protected] } ]});
r.db(CompagniaAerea).table(Persona).insert( { id:3, nome: Will,
cognome: Ryder, contatti : [ phone: { "cell": "957463", "home":
"35674" }, email:{ "work": "[email protected] }, address: "will road"
]});
Inserimenti elementi Volo
r.db(CompagniaAerea).table(Volo).insert( { id:1, partenza: Rome,
arrivo: Milan, data: r.time(2016, 4, 12) });
r.db(CompagniaAerea).table(Volo).insert( { id:2, partenza: London,
arrivo: New York, data: r.time(2016, 6, 10), prezzo: 100 });
r.db(CompagniaAerea).table(Volo).insert( { id:3, partenza: Paris,
arrivo: Madrid, data: r.time(2016, 7, 10), prezzo: 50 });
37
r.db(CompagniaAerea).table(Volo).insert( { id:4, partenza: Paris,
arrivo: Budapest, data: r.time(2016, 8 10), prezzo: 25 });
Inserimento elementi Aereo
r.db(CompagniaAerea).table(Aereo).insert( { id:1, nome :
tipo: "jet" });
r.db(CompagniaAerea).table(Aereo).insert( { id:
capienza: "40" });
"airtravel",
2, nome:
r.db(CompagniaAerea).table(Aereo).insert( { id:3, nome:
tipo: "linea", capienza: "20" });
"flywings"
"easywind",
Inserimento elementi Prenotazione
r.db(CompagniaAerea).table(Prenotazione).insert( { id: "A",
idVolo:1, idPersona: 2 });
r.db(CompagniaAerea).table(Prenotazione).insert( { id: "B",
idVolo:2, idPersona: 3 });
r.db(CompagniaAerea).table(Prenotazione).insert( { id: "C"
idVolo:4, idPersona: 1 });
r.db(CompagniaAerea).table(Prenotazione).insert( { id: "D",
idVolo:4, idPersona: 2 });
38
5 Conclusioni
In questa tesi si è voluto studiare il contesto dei database non relazionali, in
particolare di RethinkDB.
E' stato visto come quest'ultimo si stia diondendo su larga scala, essendo
tra i 50 database con più utenze al mondo.
Nonostante le sue idee di progettazione partino dal principio di non poter
rispettare uno dei pilastri per l'ecienza di un sistema distribuito, ossia la consistenza, dimostra di possedere funzionalità molto performanti in altri casi di
interesse.
Col passare degli anni le basi dati aumentano sempre più il proprio volume,
per questo c'è stato bisogno di un buon compromesso, trovato proprio dagli
sviluppatori di RethinkDB.
Il loro lavoro di progettazione è partito completamente da zero, poichè gli
obiettivi posti non erano conformi a sistemi già presenti, e adattarli avrebbe
portato solo enormi costi.
Questo ha contribuito a creare un sistema unico nel suo genere, che pur
appartenendo alla famiglia dei database non relazionali, è dierente da tutti gli
altri. Infatti, supporta alcuni meccanismi deniti tipici dei database relazionali,
come ad esempio le relazioni tra le informazioni.
Nella prima parte si è voluto attuare un confronto fra i modelli relazionali e
non relazionali, al ne di capire al meglio e focalizzare i punti chiave della tesi.
Nella seconda parte si è aperta una panoramica generale sui database NoSQL, classicandoli per categoria, descrivendoli per vantaggi e svanataggi e
riportando anche alcuni esempi a titolo informativo.
La terza parte, invece, è stata dedicata interamente a RethinkDB, partendo
da una sua descrizione ad alto livello, per poi ricadere sui modelli dei dati e
sulle API di ReQL.
In ultimo, è stata inserita l'implementazione di un database ideato, come
dimostrazione delle funzionalità descritte.
39
Riferimenti bibliograci
[1] https://www.rethinkdb.com/
[2] http://www.json.org/
[3] https://www.javascript.com/
[4] Jeremy Zawodny, http://www.linux-mag.com/id/7488/ , Linux Magazine,
August 24, 2009
[5] https://nodejs.org/en/
[6] https://it.wikipedia.org/wiki/NoSQL
40