Introduction
Nella prima parte (GraphQL + OWL: Un’interfaccia GraphQL “ontologizzata”), abbiamo introdotto e discusso un design per uno schema GraphQL che fornisce un’interfaccia di query (ad esempio, tipi, campi) conforme a un insieme arbitrario di ontologie.
Nella seconda parte (GraphQL + OWL: Namespaces), abbiamo iniziato a scrivere il modulo software che implementa quanto sopra. In particolare, abbiamo discusso e illustrato come gestire i namespaces.
Nella terza parte (GraphQL + Tipi e Campi), abbiamo definito tipi e proprietà, che rappresentano i mattoni fondamentali di un’ontologia e di uno schema GraphQL.
Ora è il momento di progettare il lato “scrittura” del sistema: in GraphQL, le Mutazioni. Le Mutazioni, come suggerisce il nome, permettono la manipolazione dei dati.
Al termine di questo articolo, il sistema fornirà quanto segue:
- Un endpoint GraphQL
- L’API di introspezione
- Uno schema navigabile, come discusso nella prima parte (GraphQL + OWL: Un’interfaccia GraphQL “ontologizzata”). In particolare, dovremmo trovare i tipi, i campi, le query e le mutazioni corrispondenti definiti all’interno di ogni namespace.
Tipi e Tipi di Input
GraphQL fa una chiara distinzione tra l’interfaccia di lettura e quella di scrittura. In particolare:
- L’interfaccia di lettura è composta da campi di recupero, direttamente o indirettamente definiti sotto il tipo di livello superiore
Query, la cui firma include un tipo di ritorno e un elenco opzionale di argomenti di input. - L’interfaccia di scrittura è molto simile (cioè campi di mutazione con argomenti di input opzionali e un tipo di ritorno), ma tutto deve essere definito sotto un tipo di livello superiore chiamato
Mutation.
Vedi il seguente esempio:
type Query {
// retrieval fields goes here (even nested under types)
cartDetails(owner: String!): Cart
}
type Mutation {
// mutation fields go here, here are some examples
addCart(name:String!, capacity: Int): Cart
renameCart(id:Int!, name:String!): Cart
removeCart(id:Int!): Cart
}
Le sezioni Query e Mutation all’interno dello schema sopra sono piuttosto simili; in realtà, la differenza risiede nel comportamento:
- Le query (cioè tutto ciò che si trova all’interno del tipo
Query) vengono utilizzate per il recupero dei dati. - Le mutation vengono utilizzate per manipolare i dati. Inoltre, possono avere un tipo di ritorno, quindi includono anche una parte di recupero.
- Le query (cioè tutto ciò che si trova all’interno del tipo
Nella definizione del nostro schema, vogliamo mantenere questa somiglianza tra le due sezioni. GraphOWL dovrebbe consentire al richiedente di utilizzare la seguente semantica (che, come puoi vedere, è molto simile all’interazione con le query descritta nei precedenti articoli):
mutation {
dcterms {
identifierScheme(uri: "https://svde.org/938473894<7008898") {
// properties of IdentifierScheme instance we want to get back
}
}
}
La manipolazione di un tipo richiede la definizione di un insieme di mutation che includa le capacità di inserimento, aggiornamento e eliminazione. Invece di creare tre campi di mutation differenti, abbiamo optato per la seguente semantica:
- Il campo mutation è unico (ad esempio,
identifierScheme, come nell’esempio sopra). - Ci possono essere n argomenti opzionali:
urieproperties, uno per ciascun namespace. Almeno uno di essi deve essere presente, perché al momento della mutation:- Se è presente solo il campo
uri, significa che l’utente vuole eliminare l’istanza associata a quell’identificatore. - Se sono presenti solo le
namespace properties, significa che l’utente vuole creare una nuova istanza il cui stato è rappresentato dalle proprietà di input. - Se sono presenti entrambi (
urienamespace properties), significa che l’utente vuole aggiornare l’istanza associata alurifornito utilizzando le proprietà di input.
- Se è presente solo il campo
Lo schema apparirebbe così:
// The mutation set associated to a given namespace.
// Actually, it doesn't represent any mutation: it acts only as a namespace
// container for holding the mutations that belongs to a given namespace.
type DctermsMutationSet {
// Create, update or delete a given instance of DctermsIdentifierScheme,
// a set of resource identifier encoding schemes and/or formats.
identifierScheme (
// The instance URI
uri:String,
// The "dc" namespace properties
dc:DcIdentifierSchemePropertiesInput,
// The "dcterms" namespace properties
dcterms:DctermsIdentifierSchemePropertiesInput): DctermsIdentifierScheme
sourceScheme (
uri:String,
dc:DcSourceSchemePropertiesInput,
dcterms:DctermsSourceSchemePropertiesInput): DctermsSourceScheme
// ... other mutation fields (one for each type)
}
type Mutation {
dcterms: DctermsMutationSet
dc: DcMutationSet
// ... other namespaces
}
Ecco tre esempi di mutation che potrebbero essere utilizzati a runtime per manipolare il tipo IdentifierScheme:
// Delete an IdentifierScheme instance.
mutation {
dcterms {
identifierScheme(uri: "https://graphowl.org/9384738") {
// properties of the deleted instance we want to get back in response.
...
}
}
}
// Create a new IdentifierScheme instance.
mutation (
$dcProperties: DcIdentifierSchemePropertiesInput,
$dctermsProperties: DctermsIdentifierSchemePropertiesInput) {
dcterms {
identifierScheme(dc: $dcProperties, dcterms: $dctermsProperties) {
// the system assigned URI for this new instance.
uri
// other properties of the new instance.
...
}
}
}
// Update an existing IdentifierScheme instance.
mutation (
$uri: String,
$dcProperties: DcIdentifierSchemePropertiesInput,
$dctermsProperties: DctermsIdentifierSchemePropertiesInput) {
dcterms {
identifierScheme(uri: $uri, dc: $dcProperties, dcterms: $dctermsProperties) {
// properties of the deleted instance we want to get back in response.
...
}
}
}
L’idea dovrebbe essere intuitiva: a parte l’eliminazione, che accetta solo l’identificatore dell’istanza, quando si gestisce una nuova istanza o una già esistente, il richiedente dovrebbe specificare le proprietà del namespace che rappresentano lo stato (persistente) di tale istanza.
Come considerazione generale, il sistema sposta la leggibilità maggiormente sul lato delle query. In altre parole, lo schema diventa un po’ più complicato (molti tipi, tipi di input con nomi simili, campi, proprietà dei namespace, e così via). Allo stesso tempo, però, le query risultano più immediate e comprensibili.
Anche se ciò significa che l’API di introspezione offrirebbe una visione complessa del sistema (la complessità cresce linearmente con il numero di ontologie di input definite all’avvio), al momento dell’esecuzione delle query o delle mutation, cioè nella parte “operativa” del sistema, le cose diventano più semplici.
Prossimi passi
Ci sono ancora molte cose mancanti. Ecco alcuni punti:
- Relazioni semantiche tra classi nell’ontologia (ad esempio, inverseOf, symmetric)
- Cardinalità dei campi nelle mutation
- Vincoli
- Implementazione della logica di persistenza e recupero
- Archiviazione dei dati persistenti
Tutti questi aspetti contengono sfide interessanti che affronteremo nei prossimi capitoli.
Ci piacerebbe ricevere le vostre domande, dubbi e feedback su questo post del blog!
Non esitate a contattarci o a lasciare un messaggio nella sezione dei commenti qui sotto.