Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Java NoSQL: primi passi con MongoDB

Java e MongoDB: è possibile utilizzare il document store in maniera molto semplice anche in java. Iniziamo a capire come, con quattro piccoli esempi semplici ed efficaci.
Java e MongoDB: è possibile utilizzare il document store in maniera molto semplice anche in java. Iniziamo a capire come, con quattro piccoli esempi semplici ed efficaci.
Link copiato negli appunti

MongoDB è un database NoSQL di tipo document: questo vuol dire che è particolarmente orientato ad un approccio alla modellazione di tipo domain driven e ad un utilizzo "ad oggetti" delle entità che decidiamo di gestire con esso, come vedremo. Non a caso è stato via via adottato in maniera crescente soprattutto in contesti, come quello Ruby On Rails e NodeJS, dove l'attenzione maggiore è rivolta ad una modellazione "agile", alla ricerca della possibilità di rimodulare continuamente la definizione degli schema di database, e alla produttività.

In questo articolo proseguiamo l'esplorazione del mondo NoSQL con java iniziata con gli articoli introduttivi su orientDB e neo4j (anche questo database condivide con gli altri l'interfaccia tinkerpop proposta negli esempi precedenti).

Dal punto di vista java MongoDB offre ottime potenzialità di integrazione, grazie a delle api che ricordano molto il pattern DAO, e quindi non dovrebbe presentare particolari problemi, e risultare peraltro particolarmente facile da utilizzare per chi ha già esperienza con JPA, o tool per il mapping ad oggetti, quali Hybernate o Ibatis.

Installazione e configurazione di mongoDB

Per l'installazione e la configurazione di base rimandiamo all'ottima guida ufficiale:

http://docs.mongodb.org/manual/installation/

Il database è scritto in c++ e dovremo quindi installare la versione nativa adatta al nostro sistema operativo: per quanto concerne java, per i nostri esempi utilizzeremo maven:

<dependency>
	<groupId>org.mongodb</groupId>
	<artifactId>mongo-java-driver</artifactId>
	<version>??</version>
</dependency>

In linea generale, una volta installato il server, si dispone come di consueto di una console accessibile da riga di comando, in questo caso particolarmente semplice da utilizzare e sulla quale può essere interessante spendere qualche parola.

La console

La console è molto semplice da utilizzare: se inoltre avete conoscenze di javascript sarete ulteriormente avvantaggiati, giacché è il linguaggio utilizzato per le operazioni a riga di comando. Ad esempio per ottenere una lista dei database presenti sulla nostra installazione digiteremo:

mongo> show dbs

Infine per aggiungere un nuovo documento di nome “mongo-doc” ad una collection esistente:

mongo> db.things.insert( { name: 'mongo-doc' })

Ovviamente è nostro interesse a questo punto capire come vengano salvati i dati internamente, e quale sia la sintassi corrispondente in java.

Inseriamo il primo record!

Tanto per dare una idea l'esempio precedente diventerebbe in java qualcosa del genere:

MongoClient client = new MongoClient("localhost", 27017);
DB db = client.getDB("TestDB");
// inserimento del primo documento
DBObject jsonDocument = (DBObject) JSON.parse("{ name : 'mongo-doc' }");
db.getCollection("things").insert(jsonDocument);

Nella prima riga effettuiamo la connessione al database locale sulla porta predefinita. Successivamente scegliamo di utilizzare il database TestDB, che non esiste ancora: questo non è un errore ma una precisa scelta, poiché MongoDB creerà per noi il database se non esiste già.
Allo stesso modo facciamo riferimento ad una collection things, senza doverci preoccupare di crearla prima se non esiste.

É interessante osservare come per l'inserimento dei documenti sia possibile utilizzare un oggetto di tipo DBObject. Per creare quest'oggetto avremmo potuto utilizzare le api del wrapper java:

// inserimento del primo documento
BasicDBObject document = new BasicDBObject();
document.put("name", "mongo-doc");
db.getCollection("things").insert(document);

ma nell'esempio proposto il driver java fornisce delle utilità per il parsing da json: in questo modo possiamo ragionare in maniera il più possibile simile alla console, e semplificarci un po' la vita.

E l'autenticazione?

Come certamente avrete notato, nell'esempio proposto non abbiamo tenuto in considerazione alcuna autenticazione. Di default MongoDB assume che il driver sia eseguito su un ambiente protetto per un trusted user, quindi l'autenticazione è semplicemente disabilitata di default. In ogni caso è naturalmente possibile utilizzarla all'occorrrenza, tramite il modulo auth:

MongoClient client = new MongoClient("localhost", 27017);
DB db = client.getDB("TestDB");
boolean auth = db.authenticate("root", "root".toCharArray());

Allo stesso modo il traffico di rete sulle istanze di MongoDB è non criptato, quindi qualora si voglia esporre direttamente l'accesso ad una istanza, è bene farlo tramite opportune connessioni SSL.

Caratteristiche di MongoDB

Sharding

MongoDB consente la possbilità dello sharding (lo "sbriciolamento" dei dati attraverso le istanze di un cluster), che avviene in maniera trasparente ed orizzontale, se abilitato: per un utilizzo ottimale è consigliabile avviare direttamente il database in sharding fin da principio. É bene ricordare che le shard key interne non sono modificabili (una specie di chiave primaria riconducibile all'istanza dove è copiato il dato).

Una configurazione tipica consiste di 3 unità di sharding, e in generale è bene usarne un numero dispari, così che la presenza di almeno una istanza in update consenta al sistema di ditribuire continuamente le repliche.

Quando usare lo sharding?

In generale se la RAM in-memory risulta essere (o è probabile che diventi) superiore a quella del sistema, e se la quantità prevista di operazioni di update rischia di far degenerare le performance per una singola macchina.

Il collegamento con più server in sharding da java è molto semplice:

ServerAddress[] servers = {
	new ServerAddress("localhost", 27017),
	new ServerAddress("host-02", 27017),
	new ServerAddress("host-03", 27017)
};
MongoClient client = new MongoClient(Arrays.asList(servers));

Tipo dei dati e BSON

Un tipico documento di MongoDB ha una struttura JSON del tipo:

{
	_id: ObjectId("5099803df3f4948bd2f98391"),
	name: { first: "Alan", last: "Turing" },
	birth: new Date('Jun 23, 1912'),
	death: new Date('Jun 07, 1954'),
	contribs: [ "Turing machine", "Turing test", "Turingery" ],
	views : NumberLong(1250000)
}

In realtà il documento viene salvato internamente in un formato binario json-like denominato BSON. Dall'esempio è facile intuire come anche utilizzando solo json per la creazione (o l'update) dei documenti si possa specificare il tipo dei campi, che prevede la possibilità di specificare i soliti tipi numerici, tipi data, così come di salvare dei dati strutturati per ogni campo (ad esempio per name o contribs).

Come è facile intuire il campo _id rappresenta un id interno utile per l'identificazione univoca del singolo documento (una specie di chiave primaria, insomma) e in generale non va inserito a mano, ma verrà generato dal db stesso.

Collection, Document, Field

Ogni database contiene delle collection di documenti, dove ciascuna collection gioca un ruolo simile a quello a cui siamo abituati per le tabelle su un db relazionale: possiamo immaginare infatti ogni istanza di Document come uno dei record su una tabella, corrispondente alla collection relativa. MongoDB utilizza per lo più un approccio schemaless, anche se si può definire uno schema, così da avere maggiore controllo e migliorare le performance: per il momento questo aspetto esula dai nostri scopi introduttivi (ci piace anzi concentrarci sulla flessibilità) e lo rimandiamo pertanto a futuri approfondimenti e alla guida ufficiale.

Scordiamoci però le join: nel contesto del database document bisogna ragionare sostanzialmente ad oggetti, quindi spezzettare le eventuali join in altrettante piccole query. La scelta è insomma tra l'utilizzo massiccio di riferimenti, o di nested document, e va valutata di caso in caso: diciamo che la seconda è probabilmente utilizzabile per nested document non troppo complessi.

Inserimento e update di documenti

MongoDB utilizza JSON anche per l'inserimento di valori, e persino come formato di query. Qualsiasi operazione sul database va eseguite tramite un opportuno oggetto che implementa l'interfaccia DBObject, che è un wrapper del relativo json.

Ad esempio potremmo scrivere:

BasicDBObject doc = new BasicDBObject();
	doc.put("nome", "Luigi");
	doc.put("eta", 22);
collection.insert(docLuigiRossi);

oppure:

BasicDBObject doc = (BasicDBObject) JSON.parse("{nome: 'Luigi', eta: 22}");
collection.insert(doc);

Osserviamo come nel primo caso l'oggetto concreto utilizzato (BasicDBObject) si comporti a tutti gli effetti come una Map: questa cosa probabilmente non stupirà chi abbia già un po' di pratica di utilizzo di altri wrapper/parser per JSON in java.

Osservando il secondo esempio vediamo in effetti quanto sia semplice il parsing diretto da stringa JSON ad oggetto, ed è la soluzione che riteniamo preferibile.

Ricerche

A questo punto se vogliamo effettuare delle ricerche ci basta utilizzare nuovamente degli “oggetti” JSON. Per esempio per cercare tutti i documenti per i quali il campo nome è pari a “Luigi”:

DBCursor cursor = collection.find((BasicDBObject) JSON.parse("{nome: 'Luigi'}"));
while (cursor.hasNext()) {
	System.out.println(cursor.next());
}

Anche qui il nostro suggerimento è di ragionare direttamente con la sintassi json (la stessa che utilizzeremmo per le prove a console, o sui servizi restful via http), e creare l'oggetto wrapper con l'utilità di parsing.

In questo modo possiamo utilizzare l'operatore $gt per le ricerche di persone di età maggiore dei 30 anni:

cursor = collection.find((BasicDBObject) JSON.parse("{eta: {$gt: 30}}"));

Un'altra caratteristica molto utile di MongoDB è la possibilità di effettuare ricerche basate su espressioni regolari, tramite l'operatore $regex:

cursor = collection.find((BasicDBObject) JSON.parse("{cognome: { $regex: 'rossi.*', $options: 'i' }}"));

Nel semplice esempio proposto riusciremo a trovare sia i cognomi “Rossi”, che “Rossini” (tramite $options possiamo utilizzare i parametri tipici delle regular expression: in questo caso ignore case).

Per i dettagli rimandiamo all'esempio completo, contenuto nel progetto scaricabile.

Transazioni

Le transazioni sono sempre atomiche, anche se è possibile aggirare questa limitazione (o meglio: simulare transazioni su più elementi, che verranno eseguite comunque singolarmente) prevedendo nel json di aggiornamento delle condizioni che coinvolgano un intero gruppo di documenti:

{eta: {$gt: 30}, nome: 'anonimo'}

Uso della memoria e GridFS

MongoDB utilizza per la persistenza un sistema noto come “memory mapped files”.

Insieme a questo va aggiunto il formato nativo dei salvataggi, BSON (cioè praticamente una specie di JSON ma binario), che può salvare documenti grandi al massimo 16 mega. Se c'è bisogno di utilizzare documenti più grandi, è bene rivedere il proprio database schema, o iniziare ad utilizzare il sistema apposito GridFS, che consente di suddividere un singolo documento in chunks.

Vediamo subito un piccolo esempio di utilizzo di GridFS, così da mettere a fuoco fin da subito la semplicità di utilizzo:

Mongo mongo = new Mongo("localhost", 27017);
DB db = mongo.getDB("imagedb");
File imageFile = new File("src/main/resources/img/Duke.gif");
// creiamo un namespace "photo" dove aggiungere l'immagine
GridFS images = new GridFS(db, "photo");
// creiamo il file
GridFSInputFile gfsFile = images.createFile(imageFile);
gfsFile.setFilename("Duke");
gfsFile.setContentType("image/gif");
gfsFile.save();
// visualizziamo la lista dei file... (per verifica)
DBCursor cursor = images.getFileList();
while (cursor.hasNext()) {
	System.out.println(cursor.next());
}
// come recuperiamo la nostra immagine?
GridFSDBFile image = images.findOne("Duke");
System.out.println("TIME UPLOADED? "+image.get("uploadDate"));
// rimuoviamo l'immagine
images.remove(images.findOne("Duke"));

Nell'esempio accediamo ad una immagine dal filesystem locale, le assegniamo alcune proprietà, e la salviamo. Il funzionamento generale è praticamente identico a quello già accennato per oggetti di uso comune, la differenza maggiore è nell'utilizzo di un namespace (nel nostro caso “photo”) al posto di una collection.

Come si vede dunque la scelta se utilizzare o meno GridFS è puramente legata a questioni di opportunità legate al design dello schema di database, e alle performance. A tal proposito è bene rammentare che è sempre preferibile il salvataggio di oggetti binari su filesystem, invece che all'interno di un database, rendendoli più facilmente disponibili per sistemi di indicizzazione efficienti quali solr o elasticsearch, e lasciando spazio ad opzioni di de-normalizzazione utili al miglioramento generale delle performance.

Da notare che la dimensione massima del db si traduce in pratica in un massimo numero di elementi nel namespace (cioè numero di collezioni + numero di indici) pari a 24000 per ogni database.

laziness e write failure

MongoDB usa una tecnica di scrittura ed aggiornamento molto veloce: questo è possibile in parte anche perché non sono previste di default le notifiche di eventuali errori, per controllare i quali è bene utilizzare getlastError.

I memory mapped files sono salvati su disco ogni 60 secondi: nei periodi intermedi si può ridurre o eleminare la possibilità di perdita di dati per via di guasti tramite l'adozione di record di tipo journalized, che registrano cioè i cambiamenti ogni 100ms, al costo di un degrado di performance in genere intorno al 5%.

Quali operatori possiamo usare nelle query?

Ecco una piccola lista degli operatori principali:

utilizzo operatori
confronto $ne, $lt, $lte, $gt, $gte
inclusione $in, $nin, $all
logici $and, $or, $nor, $not
esistenza, tipo $exists, $type
ricerca full-text $regex
query geospaziali $near, $within, $box, $polygon, $center, $uniqueDocs

ma è possibile utilizzarne anche di molto più sofisticati (dedicati agli array, o con l'utilizzo di veri callback in javascript), per approfondire i quali rimandiamo alla documentazione:
http://docs.mongodb.org/manual/reference/operators/

Un ultimo esempio: Cities

A questo punto proponiamo un piccolo esempio un po' più complesso: l'idea è di utilizzare i dati offerti per i test sul sito ufficiale, in formato json:

http://media.mongodb.org/zips.json

I dati proposti sono dei dati di test che rappresentano alcune informazioni relative a città degli stati uniti. Un record tipico ha una forma di questo tipo:

{"city": "FREEDOM", "loc": [-111.029178, 43.017167], "pop": 212, "state": "WY", "_id": "83120"}

Particolarmente interessante è il campo loc, che ci consentirà anche ricerche geospaziali, come accennato sopra.

Connessione al database e creazione della collection “cities”, per i dati

ServerAddress[] servers = { new ServerAddress("localhost", 27017) };
MongoClient mongoClient = new MongoClient(Arrays.asList(servers));
DB db = mongoClient.getDB("TestDB");

Creazione/reset della collection dell'esempio:

// se la collection esiste già, la cancelliamo e la ricreiamo
db.getCollection("cities").dropIndexes();
db.getCollection("cities").drop();

creazione degli indici

Possiamo creare degli indici, utili alle ricerche full-text e a quelle geospaziali:

DBCollection collection = db.getCollection("cities");
collection.createIndex(new BasicDBObject("city", 1));
collection.createIndex(new BasicDBObject("loc", "2d"));

import dei dati di esempio

Ogni riga del file disponibile con i dati di esempio contiene un singolo record json da inserire, quindi basterà accedere al file remoto (nel progetto abbiamo allegato per semplicità una copia locale), ed inserirne le righe via via che le leggiamo:

URL url = new URL(“http://media.mongodb.org/zips.json”);
Scanner sc = new Scanner(url.openStream(), "UTF-8");
while(sc.hasNextLine()){
	String json = sc.nextLine();
	BasicDBObject doc = (BasicDBObject) JSON.parse(json);
	collection.insert(doc);
}

una query un po' complicata

Concludiamo i nostri esempi con una query un po' più complicata, sui dati appena importati.
Vogliamo identificare le città con più di 3000 abitanti, che hanno un nome che termina in “-ville”, e sono nell'area geografica delimitata dal box tra [-80,0] e [20,35]

String query = "{city: { $regex: '.*ville.*', $options: 'i' }, pop: {$gt: 3000}, loc: { $within: { $box: [[-80,0], [20,35]] } }}";
DBCursor cursor = collection
	.find((BasicDBObject) JSON.parse(query))
	.sort((BasicDBObject) JSON.parse("{state:-1}"));
// NOTA: ordinamento per stato, discendente
// STAMPA:
while (cursor.hasNext()) {
	System.out.println(cursor.next());
}

Anche qui rimandiamo al progetto allegato per il sorgente completo.

Approfondimenti

Import/export del db con mongodump

Prima di procedere oltre con i nostri esperimenti è bene avere una idea di come effettuare l'export e l'input dei dati, tramite i comandi mongodump e mongorestore, presenti nella directory di installazione:

>> mongodump -d database_name -o /some/directory
>> mongorestore -d database_name /some/directory

Interfacce grafiche per il db

MongoDB consiste in una installazione molto ridotta, e non offre dei tool grafici per l'amministrazione o navigazione dei contenuti. É naturalmente possibile utilizzare quelli prodotti da terze parti, tra i quali senz'altro RockMongo (una webapp scritta in php, per chi dispone di una installazione apache utilizzabile), o ad esempio Umongo (una applicazione desktop scritta in java).
Esiste inoltre un utile plugin per Eclipse: MonjaDB, installabile via la url http://www.jumperz.net/update/

Corrispondenze tra SQL ed operatori MongoDB

sintassi SQL operatori MongoDB
WHERE $match
GROUP BY $group
HAVING $match
SELECT $project
ORDER BY $sort
LIMIT $limit
SUM() $sum
COUNT() $sum
JOIN si può usare in parte l'operatore $unwind, oppure dei document embedded, anche se è in generale preferibile de-normalizzare ed effettuare delle query tra oggetti.

Ti consigliamo anche