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

NoSQL in Java: un esempio con le api tinkerpop e neo4j

Iniziamo ad utilizzare le api tinkerpop con neo4j: come esempio implementiamo la mappa di un ipotetico gioco.
Iniziamo ad utilizzare le api tinkerpop con neo4j: come esempio implementiamo la mappa di un ipotetico gioco.
Link copiato negli appunti

In questo articolo vedremo come utilizzare Neo4j, uno dei graph database più popolari, con le API TinkerPop, uno standard molto importante che sta giocando un ruolo fondamentale nel mondo dei Graph DB (a cui abbiamo fatto cenno nel primo articolo di questa serie su java e graphDB, dedicato ad Orient DB).

Neo4j

I motivi dello straordinario successo di questo prodotto sono dovuti a diversi fattori:

  • è realizzato in Java, può quindi essere eseguito senza problemi su una gran quantità di architetture e sistemi operativi;
  • ha ottime performance e regge una grandissima quantità di dati, requisiti fondamentali per il mercato enterprise;
  • è disponibile sia in versione Community, liberamente scaricabile ed utilizzabile, che Advanced ed Enterprise, con caratteristiche avanzate come Online Backup, High Availability, Clustering, e molte altre;
  • la documentazione è ben realizzata, liberamente consultabile e soprattutto aggiornata.

Bisogna ammettere inoltre che una bella fetta di notorietà è dovuta alle efficaci strategie di comunicazione adottate dalla compagnia: social network, sponsorizzazione di eventi e presentazioni, integrazione con le piattaform di Cloud Computing (ad esempio Heroku), contributi nei gruppi di discussione. Tutto ciò ha alimentato moltissimo il passaparola fra gli sviluppatori, rendendo Neo4j un trending topic nell'ambiente NoSQL. Una grande lezione di marketing!

TinkerPop

TinkerPop è un progetto open-source molto ambizioso che punta alla realizzazione di uno standard per la gestione di graph database sulla piattaforma Java, sfruttando anche linguaggi "alternativi" che però si integrano perfettamente nella Java Virtual Machine (JVM), come Scala o Groovy.

I componenti di questa architettura sono molti, ma per gli scopi di questo articolo ce ne interesseranno due in particolare, ovvero Blueprints e Gremlin

Blueprints è insieme di interfacce e classi di base per la realizzazione di Property Graph in Java; i produttori di graph database che intendono avvalersi della compatibilità con TinkerPop devono quindi implementare il codice necessario a interfacciarsi con questa libreria, che rappresenta quindi l'elemento fondante di TinkerPop.

Gremlin è invece un linguaggio specifico progettato per interrogare, analizzare e manipolare i grafi all'interno dello stack TinkerPop.

Un esempio: creiamo una mappa per un videogame

Vedremo ora come creare un grafo da utilizzare per un ipotetico videogioco di tipo "adventure", utilizzando dapprima le API native di Neo4j e poi con Blueprints. I giochi di questo tipo, per chi non li conoscesse, hanno alcune caratteristiche comuni, che ci offrono una ottima base per i nostri esempi:

  • il "mondo" è costituito da molte stanze, o "scene";
  • ci si muove da una stanza all'altra tramite passaggi di vario genere: corridoi, strade, teletrasporto, eccetera;
  • anche i personaggi all'interno delle stanze si possono spostare;
  • gli oggetti possono essere raccolti, spostati, utilizzati, distrutti, combinati, e così via.

Un'altra caratteristica di questi giochi è l'estrema varietà di tutti gli elementi che abbiamo descritto; se fosse tutto rigido e preimpostato infatti sarebbero di una noia mortale! Questo requisito rende i graph database la tecnologia più adatta per lo scopo:

  • i dati sono rappresentati in maniera del tutto naturale;
  • l'organizzazione delle stanze e dei relativi collegamenti, il percorso del giocatore, la presenza di personaggi o oggetti: tutti questi oggetti sono palesemente grafi o parti di esso;
  • la manipolazione della struttura e dei contenuti è molto più efficiente nei graph database rispetto all'utilizzo di tabelle e relazioni.

Quest'ultimo punto in particolare è in realtà uno degli aspetti più importanti di queste tecnologie, anche se spesso è spiegato male, se non ignorato completamente!

Index-free adjacency e le ricerche sui nodi

Quasi tutti i Graph DB (Neo4j compreso) sfruttano la cosiddetta index-free adjacency: al posto di avere un indice monolitico di tutte le relazioni del grafo (come avviene invece nei database relazionali), ogni vertice ha un "mini-indice" con i puntatori diretti ai suoi vicini.

In questo modo l'attraversamento del grafo diventa un'operazione molto veloce, dato che le dimensioni di questi mini-indici è teoricamente molto contenuta rispetto al totale dei vertici.

Di contro "trovare" un particolare vertice o arco all'interno di un grafo in base all'ID (chiave surrogata) è un'operazione costosa almeno quanto la corrispettiva operazione nel mondo relazionale; per questo motivo si potrebbe affermare che i graph database includono il meglio dei due mondi.

La mappa del gioco con neo4j

Passiamo ora al nostro primo esempio, grazie al quale illustreremo come realizzare la mappa del nostro adventure, che sarà composta da tre stanze: Sala da ballo, Cucina, Cantina. Queste stanze sono collegate come illustrato in figura 1:

Figura 1. La mappa del gioco
(clic per ingrandire)


La mappa del gioco

Fra la sala da ballo e la cucina c'è inoltre un passaggio segreto, indicato da una linea tratteggiata.

Creazione della mappa in neo4j

Ecco il frammento di codice che crea questa struttura, utilizzando le API native di Neo4j:

private static enum RelTypes implements RelationshipType {
    PASSAGGIO
}
GraphDatabaseService graphDb = new GraphDatabaseFactory().newEmbeddedDatabase(storePath);
// crea stanze (vertici)
Node room_1 = graphDb.createNode();
	room_1.setProperty("nome", "Sala da ballo");
	room_1.setProperty("descrizione", "Descrizione della Sala da ballo");
Node room_2 = graphDb.createNode();
	room_2.setProperty("nome", "Cucina");
	room_2.setProperty("descrizione", "...");
Node room_3 = graphDb.createNode();
	room_3.setProperty("nome", "Cantina");
	room_3.setProperty("descrizione", "...");
// crea collegamenti (archi) fra le stanze
// NOTA: gli archi di TinkerPop del tipo diretto
Relationship passaggio_1_2 = room_1.createRelationshipTo(room_2, RelTypes.PASSAGGIO);
	passaggio_1_2.setProperty("nome", "Corridoio verso le cucine");
Relationship passaggio_2_1 = room_2.createRelationshipTo(room_1,	RelTypes.PASSAGGIO);
	passaggio_2_1.setProperty("nome", "Corridoio verso la sala da ballo");
Relationship passaggio_1_3 = room_1.createRelationshipTo(room_3, RelTypes.PASSAGGIO);
	passaggio_1_3.setProperty("nome", "Scale che scendono in cantina");
Relationship passaggio_3_1 = room_3.createRelationshipTo(room_1, RelTypes.PASSAGGIO);
	passaggio_3_1.setProperty("nome", "Scale che salgono in sala da ballo");
// Ci possono essere collegamenti multipli fra vertici
// A esempio: il "passaggio segreto"
Relationship passaggio_1_3_secret = room_1.createRelationshipTo(room_3, RelTypes.PASSAGGIO);
	passaggio_1_3_secret.setProperty("nome", "Passaggio segreto da sala a cantina");
	passaggio_1_3_secret.setProperty("nascosto", true);

Analizziamone brevemente gli elementi più importanti:

  • all'inizio viene definito, tramite una enumeration, un tipo di relazione (questa enumeration solitamente è definita a livello di classe);
  • la prima riga di codice vero e proprio istanzia il database;
  • i nodi vengono collegati fra di loro tramite il metodo createRelationshipTo();
  • sia i vertici che gli archi possono avere delle property, ovvero attributi di vario tipo utili per memorizzare valori come stringhe, numeri, date, eccetera.

Nel nostro esempio abbiamo scelto di utilizzare solamente due property, ovvero nome e descrizione, anche se ovviamente in un gioco vero e proprio (e più in generale naturalmente in una applicazione "reale") ci potrebbero essere decine o centinaia di property per vertice/arco! Ad esempio potremmo memorizzare il path di un'immagine o un'animazione di sfondo, un file audio, eccetera. Il modello è piuttosto flessibile, e l'unico vero limite è la fantasia!

Una volta creato il grafo possiamo utilizzarne gli oggetti come più ci fa comodo. Sia le API Neo4j che Blueprints hanno dei metodi a livello di nodo/vertice per attraversare tutti i collegamenti; sfruttando questi metodi quindi abbiamo implementato una rudimentale navigazione, eccola in versione Neo4j:

private Node moveSimple(Node currentRoom, int choice) {
    Iterator<Relationship> passaggiIter = currentRoom.getRelationships(RelTypes.PASSAGGIO, Direction.OUTGOING).iterator();
    Relationship passaggio = null;
    int i = 0;
    while(passaggiIter.hasNext()) {
        passaggio = passaggiIter.next();
        if (i == choice) {
            break;
        }
        // else
        i++;
    }
    return passaggio.getEndNode();
}

Utilizzando questo metodo all'interno di un ciclo while infinito si può realizzare una semplice navigazione testuale; in allegato all'articolo potete trovare il dettaglio del codice d'esempio, compilabile con Apache Maven.(TextAdventure.java). Ecco un output tipico:

Ti trovi in: Sala da ballo
Dove ti sposti?
1) Corridoio verso le cucine
2) Scale che scendono in cantina
3) [nascosto] Passaggio segreto da sala a cantina
quit) Esce dal gioco

> Scelta: 3

Ti trovi in: Cantina
Dove ti sposti?
1) Scale che salgono in sala da ballo
quit) Esce dal gioco

> Scelta: 1

Ti trovi in: Sala da ballo
Dove ti sposti?

[... e così via ...]

La mappa del gioco con Tinkerpop Blueprints

Ovviamente è possibile realizzare la stessa identica struttura utilizzando le API Blueprints, garantendo una maggiore portabilità fra diversi vendor di database (ad esempio si può utilizzare per lo storage OrientDb invece di neo4j, e decidere di migrare da una soluzione all'altra in qualsiasi momento!). È quindi possibile, utilizzando lo stesso codice, passare da un'implementazione all'altra! Ecco quindi la versione Blueprints del codice per la creazione della mappa:

creazione della mappa in blueprints

Graph graph = new Neo4jGraph("/tmp/myGraph");
// crea stanze (vertici)
Vertex room_1 = graph.addVertex(null);
	room_1.setProperty("nome", "Sala da ballo");
	room_1.setProperty("descrizione", "Descrizione della Sala da ballo");
Vertex room_2 = graph.addVertex(null);
	room_2.setProperty("nome", "Cucina");
	room_2.setProperty("descrizione", "...");
Vertex room_3 = graph.addVertex(null);
	room_3.setProperty("nome", "Cantina");
	room_3.setProperty("descrizione", "...");
// crea collegamenti (archi) fra le stanze
// NOTA: gli archi di TinkerPop del tipo diretto
Edge passaggio_1_2 = graph.addEdge(null, room_1, room_2, "passaggio");
	passaggio_1_2.setProperty("nome", "Corridoio verso le cucine");
Edge passaggio_2_1 = graph.addEdge(null, room_2, room_1, "passaggio");
	passaggio_2_1.setProperty("nome", "Corridoio verso la sala da ballo");
Edge passaggio_1_3 = graph.addEdge(null, room_1, room_3, "passaggio");
	passaggio_1_3.setProperty("nome", "Scale che scendono in cantina");
Edge passaggio_3_1 = graph.addEdge(null, room_3, room_1, "passaggio");
	passaggio_3_1.setProperty("nome", "Scale che salgono in sala da ballo");
// Ci possono essere collegamenti multipli fra vertici
// Esempio di "passaggio segreto"
Edge passaggio_1_3_secret = graph.addEdge(null, room_1, room_3, "passaggio");
	passaggio_1_3_secret.setProperty("nome", "Passaggio segreto da sala a cantina");
	passaggio_1_3_secret.setProperty("nascosto", true);

  • nel caso di Blueprints l'implementazione del database è fornita da com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jGraph, che fa da ponte fra le API Blueprints e il database Neo4j sottostante;
  • i nodi di Neo4j (Node) corrispondono ai vertici (Vertex) di TinkerPop;
  • in Blueprints è possibile assegnare un ID direttamente durante la creazione degli oggetti, ad esempio nel metodo addVertex(Object id), ma lasciando a null il parametro id si delega l'operazione di generazione al database;
  • discorso simile per gli archi (classe Edge) con il metodo addEdge(...), che però ha bisogno di un parametro aggiuntivo label di tipo String (oltre che dei vertici di partenza e arrivo);
  • anche in Blueprints vertici e archi sono dotati di property.

Questa invece è la versione Blueprints del metodo moveSimple():

private Vertex moveSimple(Vertex currentRoom, int choice) {
    Iterator<Edge> passaggiIter = currentRoom.getOutEdges("passaggio").iterator();
    Edge passaggio = null;
    int i = 0;
    while(passaggiIter.hasNext()) {
        passaggio = passaggiIter.next();
        if (i == choice) {
            break;
        }
        // else
        i++;
    }
    return passaggio.getInVertex();
}

Gremlin

Questo linguaggio, come già accennato, è stato creato per navigare all'interno dei grafi in TinkerPop, oltre che ad analizzarli e modificarli.

È stato realizzato utilizzando Groovy (Ma ne esiste una versione "non ufficiale" scritta usando scala), che permette di creare facilmente i cosiddetti Domain-specific language o DSL (come Gremlin appunto), ma è disponibile anche in versione Java, anche se è un po' più scomodo da utilizzare per via della verbosità di questo linguaggio, soprattutto per via dell'assenza di costrutti come le closure.
L'implementazione prevede comunque una fluent api che facilita un po' il nostro compito.

Utilizzando la versione Java di Gremlin abbiamo riscritto il metodo che si occupa dello spostamento da una stanza all'altra:

private Vertex moveGremlin(Vertex currentRoom, int choice) {
    GremlinPipeline<Vertex, Edge> pipe = new GremlinPipeline<Vertex, Edge>();
    Edge choosenEdge = pipe.start(currentRoom).outE("passaggio").toList().get(choice);
    currentRoom = choosenEdge.getInVertex();
    return currentRoom;
}

In realtà Gremlin è un linguaggio molto potente ed espressivo, che permette di fare molto di più che un semplice elenco di nodi! Può gestire e analizzare cammini, filtrare vertici e archi, combinare filtri e algoritmi di ricerca e backtracking... insomma c'è davvero molto da studiare!


Ti consigliamo anche