SpazioCodice

Nella quarta parte della nostra serie su GraphQL + OWL, progettiamo il lato "scrittura" del sistema: in GraphQL, le Mutations

GraphQL + OWL: Mutazioni

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.

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:

  1. Il campo mutation è unico (ad esempio, identifierScheme, come nell’esempio sopra).
  2. Ci possono essere n argomenti opzionali: uri e properties, 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 (uri e namespace properties), significa che l’utente vuole aggiornare l’istanza associata al uri fornito utilizzando le proprietà di input.

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.

Share this post

Rispondi

Scopri di più da SpazioCodice

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continua a leggere