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