Nella lezione precedente abbiamo visto come interrogare il database sfruttando il suo modello logico. In questa lezione vediamo come possiamo creare, modificare o eliminare nodi e relazioni.
Inserimento di nodi e relazioni
La clausola CREATE
è deputata alla creazione di nuovi nodi, relazioni o intere porzioni di grafo. L'istruzione più semplice per creare un nodo è:
CREATE ()
L'istruzione, ovviamente, può essere usata per creare nodi più complessi oppure porzioni di grafo. Con la virgola vengono separate le parti di grafo da comporre. Nell'esempio che segue (tratto da GraphGist) si crea un grafo che contiene la composizione di un noto cocktail. Il database viene usato poi per effettuare ricerche su ingredienti, similitudini, raccomandazioni, etc.
CREATE (campari:Ingredient {name: "campari"}), (vermouth:Ingredient {name: "vermouth"}),
(soda:Ingredient {name: "soda water"}), (orange:Garnish {name: "orange"}),
(americano:Cocktail {name: "Americano"}),
(americano)-[:CONTAINS {quantity: 1, unit: "ounces"}]->(campari),
(americano)-[:CONTAINS {quantity: 1, unit: "ounces"}]->(redVermouth),
(americano)-[:CONTAINS {quantity: 1, unit: "ounces"}]->(soda),
(americano)-[:GARNISHED_WITH]->(orange)
La prima parola all'interno della parentesi (tonda o quadra) è il nome della variabile da assengare al nodo o relazione creato/a.
La variable può essere usata solo nel contesto della stessa istruzione, ovvero una variabile non può essere usata nell'istruzione successiva, né eventualmente in una parte successiva ad una istruzione WITH
, a meno che non sia ridichiarata nella stessa, come abbiamo visto nella lezione precedente.
Inoltre, vediamo come abbiamo creato non solo singoli nodi, bensì percorsi fatti di nodi e relazioni. I percorsi creati possono essere ancora più complessi, concatendando le parti di grafo opportunamente.
CREATE
può essere utilizzata con altre istruzioni, ad esempio per creare nuove relazioni sulla base di regole di inferenza. Ecco un semplice esempio:
MATCH (a)-[:FATHER*2]->(b)
CREATE (a)-[:GRANDFATHER]->(b)
RETURN a, b
Abbiamo creato una relazione di di tipo GRANDFATHER tra due nodi tra cui sussiste una catena di due relazioni di tipo FATHER. Se eseguiamo questa istruzione più volte, le relazioni verranno create altrettante volte.
Per evitare che sussistano relazioni multiple tra due nodi, si può usare l'istruzione CREATE UNIQUE
, che si occupa:
- di verificare se esiste già il pattern da creare;
- se non esiste, di crearlo;
- se esiste ed è unico, semplicemente di valorizzare le eventuali variabili utilizzate;
- altrimenti (se esiste multiplo), di generare un errore.
L'utilità di questa istruzione si ha ogni volta che si vuole imporre, a livello applicativo, un vincolo "logico" di unicità sulla creazione delle relazioni. Attenzione: il vincolo è confinato all'interno della singola istruzione, vale a dire che nulla mi vieta di eseguire un'istruzione CREATE
semplice dopo aver eseguito una CREATE UNIQUE
. Quindi il vincolo non viene né persistito né tantomeno garantito dal database nelle istruzioni successive. Vedremo in seguito quali vincoli sono garantiti dal database.
Se non ci interessa l'unicità della relazione, ma vogliamo semplicemente creare una porzione di grafo se essa non esiste (eventualmente recuperandola, nel caso in cui esista già), l'istruzione da usare è MERGE
.
Questa è probabilmente la più potente per la manipolazione dei grafi, se usata congiuntamente a MATCH
e WITH
.
Essa, infatti, se trova più volte il pattern specificato non restituisce un errore, bensì effettua il matching altrettante volte.
L'esempio che segue (ancora tratto da GraphGist) crea un nodo se non ne esiste uno con lo stesso pattern; se invece il nodo esiste, semplicemente viene restituito.
MERGE (sugar:Ingredient {name: "Sugar"}) RETURN sugar
Inoltre, possono essere aggiunte le clausule, entrambe opzionali, per indicare cosa fare in caso di creazione o in caso di corrispondenza trovata:
MERGE (u:User {email: "user@html.it"})
ON CREATE SET u.signedIn = timestamp()
ON MATCH SET u.login = timestamp()
In questo esempio, un nodo di tipo User viene creato se non esiste, ed in tal caso viene inizializzato il campo signedIn; altrimenti, se il nodo esiste nel database, verrà solamente impostato il campo login.
Anche MERGE
può essere usato per creare porzioni di grafo. Ad esempio, realizzando un log più intelligente e relazionato, se volessimo registrare che un certo utente ha visitato una città nel 2017, possibilmente senza eccessiva ridondanza, potremmo procedere così:
MERGE (u:User {email: "user@html.it"})
MERGE (u)-[:VISITED { year: 2017 }]->(c:Town{name:"Ancona", country:"Italy"})
Vincoli
Se invece il dominio della nostra applicazione richiede vincoli più stringenti, Neo4j permette di definire vincoli di univocità a livello di label, specificando quali campi sono univoci. Un esempio tipico, è l'univocità delle e-mail per gli utenti:
CREATE CONSTRAINT ON (u:User) ASSERT u.email IS UNIQUE
Se provassimo ad inserire più utenti con la stessa e-mail, otterremmo un errore, proprio come accade per le chiavi univoche dei database SQL:
CREATE (u:User {name: "user@html.it"}), u
Il vincolo ci permette anche di risolvere un problema dell'istruzione MERGE
, dovuto al partial matching. Se consideriamo l'esempio:
MERGE (u:User {email: "user@html.it", name: "HTML.it" })
vediamo come viene cercato, ed eventualmente creato qualora non venisse trovato, l'utente avente i campi e-mail e nome con i valori specificati. Se però esiste già un utente con la stessa e-mail ma diverso nome, ne verrebbe comunque creato un altro. Impostando il vincolo si evitano proprio questo genere di errori.
Dalla versione 3.1, utilizzando al momento necessariamente la licenza Enterprise di Neo4j, è possibile specificare un vincolo di esistenza di una proprietà su nodi con una certa etichetta. Ad esempio, se vogliamo che un utente abbia obbligatoriamente una e-mail:
CREATE CONSTRAINT ON (u:User) ASSERT EXISTS(u.email)
Chiaramente, combinando i due vincoli si ottiene quanto più simile ad una chiave primaria di un database SQL.
In Neo4j non avrebbe senso definire un vincolo di integrità referenziale (chiave esterna) dei database SQL, perché le relazioni sono esplicite nel modello (non ci sono riferimenti a dati contenuti in altre entità), per cui non è possibile eliminare un nodo fin quando esso abbia una relazione in essere con un altro nodo.
Modifiche alle proprietà
Abbiamo già intravisto la parola chiave per impostare proprietà: SET
. Vediamo un altro esempio, mutuato dal precedente. In questo caso facciamo una modifica semplice, senza creare l'utente:
MATCH (u:User {email: "user@html.it"})
SET u.login = timestamp()
L'istruzione SET
può essere usata anche per impostare le proprietà delle relazioni. Qui vale la regola che se la proprietà esiste, essa verrà sovrascrittà, o altrimenti creata.
Per eliminare una proprietà, si usa l'istruzione REMOVE
:
MATCH (u:User {email: "user@html.it"})
REMOVE u.login
Eliminazione di nodi e relazioni
La parola chiave DELETE
si riferisce all'eliminazione di nodi e relazioni. Come già detto più volte, per eliminare un nodo devono essere prima eliminate le relazioni che lo coinvolgono.
Un comando tipicamente usato quando si fanno prove sul database è quello per svuotare tutto:
MATCH (a) OPTIONAL MATCH (a)-[r]-()
DELETE a,r
Questa istruzione, infatti, seleziona tutti i nodi a ed eventuali relazioni r e li elimina.
Cicli
Può capitare, soprattuto quando si lavora con i path, di dover iterare su diversi elementi per realizzare modifiche, eliminazioni, o creazioni.
Ad esempio, con il seguente comando si aggiornano tutti i nodi trovati nella catena di relazioni di tipo FATHER da un certo nodo in giù, impostando una proprietà e rimuovendone un'altra (se esiste).
MATCH p = ({name: "Abhram"})-[:FATHER*]->(child) FOREACH (n IN nodes(p)| SET n.visited = true REMOVE n.toCheck)
Concludendo, Cypher fornisce strumenti molto semplici per modificare il grafo, ma che combinate danno grandi possibilità di manipolazione e di trasformazione che difficilmente sono possibili su altri tipi di modelli.
Ci limitiamo ad aggiungere la possibilità built-in di caricare dati in formato CSV. Vediamo un esempio che carica una lista di utenti da un file contenente anche le intestaizoni dei campi che vengono interpretate; gli utenti, quindi, vengono creati se non esistono:
LOAD CSV WITH HEADERS FROM "file://users.csv" AS line
MERGE (u:User { email: csvLine.email })
SET u.displayName = csvLine.name