vai al contenuto principale

Leggere dati

I servizi di lettura consentono di ottenere grafi di oggetti a partire da una certa classe.

I servizi di lettura lavorano fondamentalmente con due strutture: type <ClassName> (oggetto singolo) e type <ClassName>Page (sottoinsieme di oggetti). Combinando le due strutture è possibile richiedere in output interi grafi di oggetti.

Modello di riferimento #

Tutti gli esempi in questa pagina fanno riferimento al modello in figura:

Employee

La classe Employee, al centro, come modellata nel Tutorial.

La classe Employee ha un ruolo per ogni tipologia di relazione supportata, nonché attributi nativi, query, math e platform dichiarati su essa.

Servizio get #

Il servizio Query.<ClassName>___get consente di ottenere un grafo di oggetti a partire da un oggetto di una certa classe.

type Query {
  ClassName___get(_id: ID!): ClassName
}

Il servizio richiede in input l’ID dell’oggetto da recuperare, e restituisce la corrispondente struttura <ClassName>. Maggiori informazioni sull’output del servizio sono disponibili in fondo.

Ad esempio, il servizio per recuperare un oggetto Employee è il seguente:

type Query {
  Employee___get(_id: ID!): Employee
}

Un esempio di query con questo servizio è disponibile qui.

Servizi getBy<UniqueConstraints> #

Per ogni vincolo di unicità definito su <ClassName> è generato un servizio Query.<ClassName>___getBy<UniqueConstraints> che lavora con quel vincolo. Ciascun servizio consente di ottenere un grafo di oggetti a partire da un oggetto di una certa classe, identificato da un vincolo di unicità (o chiave) su essa definito. Chiaramente, servizi di questo tipo non vengono generati se sulla classe in questione non sono definiti vincoli di unicità.

Una chiave può comprendere uno o più attributi; in caso di attributi multipli, essi compaiono separati da underscore nel nome del servizio (getBy<Constraint1>_<Constraint2>_<Constraint3>).

type Query {
  ClassName___getByConstraint1_Constraint2(
    constraint1: Constraint1Type!
    constraint2: Constraint2Type!
  ): ClassName
}

Ciascun servizio richiede in input i valori degli attributi che formano la chiave e restituisce la struttura <ClassName>.

Ad esempio, per la classe Employee sono disponibili i servizi:

type Query {
  Employee___getByLast_name_First_name_Date_of_birth(
    last_name: String!
    first_name: String!
    date_of_birth: Date!
  ): Employee

  Employee___getByUsername(username: String!): Employee
}

che possono essere utilizzati come segue:

{
  Employee___getByLast_name_First_name_Date_of_birth(
    date_of_birth: "23/06/1968"
    first_name: "Dolorita"
    last_name: "Wanell"
  ) {
    username
    is_active
    email_address
  }

  Employee___getByUsername(username: "vanna.bamforth") {
    full_name
    date_of_birth
    is_active
    email_address
  }
}

Servizio getPage #

Il servizio Query.<ClassName>___getPage consente di ottenere un grafo di oggetti a partire da un sottoinsieme di oggetti di una certa classe.

type Query {
  ClassName___getPage(options: ClassNamePageOptions): ClassNamePage
}

Il servizio è generato per tutte le classi managed e main (non-part), per cui ha senso recuperare una lista di istanze persistite; non viene pertanto generato per classi Form, Enum e Singleton.

Il servizio restituisce in output la struttura <ClassName>Page. È possibile controllare il sottoinsieme di oggetti restituito dalla pagina valorizzando l’argomento facoltativo options. Maggiori informazioni sono disponibili qui: Personalizzare la richiesta paginata.

Ad esempio, il servizio per recuperare un sottoinsieme di oggetti Employee è il seguente:

type Query {
  Employee___getPage(options: EmployeePageOptions): EmployeePage
}

Un esempio di query con questo servizio è disponibile qui.

type <ClassName> #

Ricapitoliamo quanto detto nella pagina Lo Schema Graphql: Mappatura modello - schema:

  • ogni classe <ClassName> nel modello viene mappata nella struttura type <ClassName>;
  • type <ClassName> è generato per tutte le classi main managed (abilitate) e per tutte le classi part abilitate e raggiungibili da almeno una classe main abilitata;
  • type <ClassName> contiene tutti gli attributi e ruoli abilitati della classe;
  • i tipi degli attributi sono mappati come mostrato in Tipi di dato primitivi.
  • il platform attribute __id è sempre presente con nome _id, anche se non esplicitamente abilitato (in GraphQL, il doppio underscore __ è un prefisso riservato a metadati);

Ad esempio, per la classe Employee, si ha la seguente struttura Employee:

type Employee {
  # platform attributes
  _id: ID!

  # attributi nativi
  first_name: String
  last_name: String
  date_of_birth(format: String = "default"): Date
  phone_number: String
  email_address: String
  date_joined(format: String = "default"): Date
  hourly_cost(format: String = "default"): Real
  username: String

  # attributi derivati
  full_name: String
  age: Int
  junior: Boolean
  team_name: String
  qualifications: String
  is_active: Boolean

  # ruoli uscenti
  # ...

  # associabili
  # ...
}

Tutti i ruoli uscenti (associazioni e composizioni) da <ClassName> verso una classe target vengono mappati, in riferimento alla classe puntata dalla relazione, come segue:

  • ruoli a uno : <TargetClassName> (analogo a <ClassName>);
  • ruoli a molti: <TargetClassName>Page (analogo a <ClassNamePage>).

Il nome del ruolo uscente su <ClassName> è lo stesso del nome del ruolo sulla classe del modello (TargetRoleName>).

Per Employee, i ruoli uscenti vengono mappati come segue:

type Employee {
  # attributi
  # ...

  # ruoli uscenti
  team: Team # associazione a 1
  supervisor: Employee # associazione riflessiva a 1
  address: Address # composizione a 1
  qualification_(options: QualificationPageOptions): QualificationPage # associazione a N
  assignments(options: Project_assignmentPageOptions): Project_assignmentPage # composizione a N

  # associabili
  # ...
}

Infine, su <ClassName> sono disponibili informazioni sugli oggetti associabili per i ruoli uscenti di tale classe. Il nome di questi campi è <TargetRoleName>___associables, mentre il tipo è <TargetClassName>Page sia per associazioni a molti, sia a uno. Il contenuto di questi campi corrisponde sostanzialmente con l’insieme di oggetti che si vedrebbe nel client web aprendo la Livetable degli associabili per quei ruoli; anche in GraphQL, record che non soddisfano eventuali Selection filter (o Selection path) non compaiono nella lista degli associabili.

Per Employee, gli associabili vengono mappati come segue:

type Employee {
  # attributi
  # ...

  # ruoli uscenti
  # ...

  # associabili
  team___associables(options: TeamPageOptions) # associazione a 1
  supervisor___associables(options: EmployeePageOptions) # associazione riflessiva a 1
  qualification___associables(options: QualificationPageOptions) # associazione a N
}

Come vale per il servizio getPage, per i campi di <ClassName> che ritornano una pagina, è possibile controllare il sottoinsieme di oggetti restituito valorizzando l’argomento facoltativo options, di tipo <TargetClassName>PageOptions. Maggiori informazioni sono disponibili qui: Personalizzare la richiesta paginata.

type <ClassName>Page #

Sia il servizio <ClassName>___getPage, sia campi di type <ClassName> relativi a ruoli e associabili restituiscono la struttura type <ClassName>Page, contenente un sottoinsieme di oggetti della classe <ClassName>.

type ClassNamePage {
  items: [ClassName!]!
  totalCount: Int
  hasNext: Boolean
  hasPrev: Boolean
  nextCursor: Cursor
  prevCursor: Cursor
}

Gli oggetti veri e propri sono inclusi nel campo non-nullo items, che ritorna una lista di type <ClassName>. Quando si richiede in output questo campo è necessario specificare quali campi si vogliono recuperare per ciascun record della lista; in altre parole, nella query va descritta la struttura di ciascun elemento <ClassName> nella lista di items.

{
  items {
    scalarField1
    scalarField2
    # ...
  }
}
{
  // tutti gli oggetti hanno la stessa struttura
  "items": [
    {
      "scalarField1": "abcdef",
      "scalarField2": 12
    },
    {
      "scalarField1": "xxyyzz",
      "scalarField2": 314
    }
    // ...
  ]
}
Esempio: utilizzo del campo items

Oltre a items, la struttura contiene i seguenti metadati:

  • totalCount: numero totale di record recuperabili per quella classe da quel percorso, al netto di eventuali filtri modellati sulla classe o sul ruolo; il conteggio dipende anche da eventuali filtri applicati alla pagina. È bene notare che totalCount non corrisponde al numero di items di cui la pagina è formata, tranne quando la pagina è grande abbastanza da recuperare tutti i record possibili;
  • hasPrev e hasNext: flag booleani che indicano se ci sono record precedenti o successivi rispetto alla pagina corrente;
  • prevCursor e nextCursor: strutture di tipo Cursor che puntano rispettivamente alla pagina precedente e alla pagina corrente, in base all’ordinamento e all’offset correnti.

Queste informazioni possono riutilizzate usate per navigare tra le pagine.

Personalizzare la richiesta paginata #

Come affermato in precedenza, è possibile controllare il sottoinsieme di oggetti restituito dalla pagina; sia il servizio <ClassName>___getPage, sia campi di type <ClassName> relativi a ruoli e associabili, accettano infatti l’argomento facoltativo options, di tipo <ClassName>PageOptions.

input ClassNamePageOptions {
  orderBy: [ClassNameSort!]
  next: Int
  prev: Int
  offset: Int
  cursor: Cursor
  fromCursor: ClassNameCursor
  filter: ClassNameFilter
  filter_exp: String
}

Valorizzando le opzioni di questo input type è possibile:

  • ordinare i dati da ritornare (con il campo orderBy);
  • limitare la grandezza della pagina corrente, ovvero il numero di valori da ritornare (con i campi next e prev);
  • navigare tra le pagine scorrendo l’insieme di record recuperabili (con i campi offset, cursor e fromCursor);
  • filtrare i dati da ritornare (con i campi filter e filter_exp).

Opzioni di default #

Quando l’argomento options non viene specificato, il server ordina i record per ID crescente e seleziona i primi 10 risultati.

type Query {
  ClassName___getPage(
    options: ClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
  ): ClassNamePage
}

type ClassName {
  # ...
  targetRoleN: TargetClassNamePage(
    options: TargetClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
  )
  targetRoleN___associables: TargetClassNamePage(
    options: TargetClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
  )
}

Il server usa questi valori di default per i campi next, offset e orderBy anche quando la richiesta include delle options ma omette questi campi. In pratica, il server effettua il merge delle opzioni di default con le opzioni custom.

Esempio: query equivalenti

Ordinare i risultati #

L’insieme di oggetti restituiti dalla pagina può essere ordinato popolando il campo orderBy, che contiene una lista di valori costanti di tipo enum <ClassName>Sort.

input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}

Ciascuna costante rappresenta un criterio di ordinamento, basato su un attributo della classe e un verso (ascendente o discendente); il nome del criterio rispetta il formato <AttributeName>___ASC o <AttributeName>___DESC. Queste costanti sono generate per tutti gli attributi managed della classe.

enum ClassNameSort {
  attribute1Name___ASC
  attribute1Name___DESC

  attribute2Name___ASC
  attribute2Name___DESC
  # ...
}

Ad esempio, per Employee sono disponibili i seguenti criteri:

enum EmployeeSort {
  _id___ASC
  _id___DESC
  age___ASC
  age___DESC
  date_of_birth___ASC
  date_of_birth___DESC
  email_address___ASC
  email_address___DESC
  first_name___ASC
  first_name___DESC
  # ...
}

L’ordinamento della pagina avviene in base ai criteri scelti:

  • se non sono stati scelti criteri, il server ordina automaticamente per _id___ASC;
  • se sono stati scelti solo attributi non-unique, il server include automaticamente _id___ASC come sotto-criterio (a parità di ordine, i record vengono ordinati per ID).
Esempio: ordinamento

Limitare il numero di risultati #

Come detto in precedenza, la paginazione di default ritorna un massimo di 10 elementi. È possibile ridurre o aumentare la dimensione della pagina popolando un campo tra next e prev.

input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}

next consente di selezionare i prossimi N record in avanti rispetto alla posizione corrente. Viceversa, prev consente di selezionare N record a ritroso, sempre rispetto alla posizione corrente. La posizione corrente è dettata dall’offset (o dal cursor), pertanto è utile combinare questi campi per navigare tra le pagine.

Può essere utile inoltre usare i flag hasNext e hasPrev restituiti da <ClassName>Page per correggere dinamicamente i valori next o prev.

type ClassNamePage {
hasNext: Boolean
hasPrev: Boolean
nextCursor: Cursor
prevCursor: Cursor
totalCount: Int
}

Il server GraphQL supporta due modalità di paginazione: offset-based e cursor-based. La prima fa uso del campo offset (oltre a next e prev), mentre la seconda fa uso dei campi cursor o fromCursor. In entrambe, lo scopo è spostare la posizione corrente del puntatore nella lista di record recuperabili dalla richiesta paginata. Come affermato in precedenza, infatti, di default il server ordina i record per ID ascendente, si “posiziona” sul primo (offset=0) e da lì seleziona i successivi 10 record (next=10).

input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}

Nella paginazione offset-based, specifichiamo uno spostamento (offset) rispetto allo “zero”, ovvero il primo elemento della lista ordinata secondo i criteri scelti (di default, _id___ASC).

Esempi: paginazione offset-based

Nella paginazione cursor-based, facciamo appunto uso di cursori per scorrere risultati . Un cursore è una struttura che “punta” al record corrente in modo univoco nel contesto della paginazione; esso dipende quindi dall’ordinamento scelto (orderBy), dalla direzione dello scorrimento e dalla dimensione della pagina (entrambe stabilite da prev o next).

Riprendiamo i campi nextCursor e prevCursor offerti dalla struttura <ClassName>Page:

type ClassNamePage {
hasNext: Boolean
hasPrev: Boolean
nextCursor: Cursor
prevCursor: Cursor
totalCount: Int
}

Il tipo Cursor è uno scalar, serializzato come stringa. Quando effettuiamo una richiesta paginata, il server crea dei valori provvisori e li offre nei campi nextCursor e prevCursor; in chiamate successive, possiamo riutilizzare queste stringhe inserendole nel campo cursor di <ClassName>PageOptions. In questo modo possiamo scorrere i risultati in avanti o indietro, a seconda di quale valore abbiamo usato tra nextCursor e prevCursor.

Esempio: utilizzo del campo cursor

È possibile far uso di cursori pre-calcolati dal server con prevCursor e nextCursor, come visto sopra, oppure specificarne uno proprio. Il campo fromCursor consente infatti di specificare un valore di tipo <ClassName>Cursor. Questa struttura descrive un record a partire dal quale si desidera scorrere la lista; a partire dal record descritto dal cursore, si possono richiedere i record precedenti o successivi.

input ClassNameCursor {
  _id: ID
  # attributi di ClassName
}

I campi disponibili sono tutti gli attributi managed di <ClassName>, e sono tutti opzionali. Popolando questi campi possiamo far riferimento a un record esistente, da cui far partire lo scorrimento. Il controllo se questo record esista veramente è volutamente lasco: se il record esiste, il risultato consiste in una pagina ottenuta a partire dal record compreso; viceversa, se non esiste, la pagina partirà dal primo record “successivo”, secondo il criterio di ordinamento scelto e rispettando il verso della paginazione.

Filtrare i risultati #

L’insieme di oggetti restituiti dalla pagina può essere filtrato popolando i campi filter o filter_exp; il primo consiste in una modalità di definizione di filtri basata sullo schema GraphQL, il secondo consente di usare le espressioni Livebase, allo stesso modo di come vengono definite espressioni math o filtri nel modello.

input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}

Il campo filter è di tipo <ClassNameFilter>; questa struttura fornisce dei filtri su una classe sulla base dei suoi attributi managed. Ciascun filtro così generato segue il formato <AttributeName>___<FilterOperation>, mentre il tipo di dato dipende dall’operazione. Sono inoltre disponibili operatori logici (AND, OR e NOT) da applicare a un insieme di filtri.

input ClassNameFilter {
  AND: [ClassNameFilter!]
  OR: [ClassNameFilter!]
  NOT: ClassNameFilter
  Attribute1Name___FilterOperation1: Attribute1NameFilter1Type
  Attribute1Name___FilterOperation2: Attribute1NameFilter2Type
  # ...
}

Le operazioni supportate sono:

  • ___eq: il valore corrisponde esattamente con il valore dato dello stesso tipo dell’attributo;
  • ___ne: il valore è diverso dal valore dato dello stesso tipo dell’attributo;
  • ___gt: il valore è maggiore del valore dato dello stesso tipo dell’attributo;
  • ___gte: il valore è maggiore o uguale al valore dato dello stesso tipo dell’attributo;
  • ___lt: il valore è minore del valore dato dello stesso tipo dell’attributo;
  • ___lte: il valore è minore o uguale al valore dato dello stesso tipo dell’attributo;
  • ___in: il valore compare nella data lista di valori dello stesso tipo dell’attributo;
  • ___null: controlla se il valore è nullo (true) o non nullo (false);
  • ___not___in: il valore non compare nella data lista di valori dello stesso tipo dell’attributo;
  • ___not___null: controlla se il valore è non nullo (true) o nullo (false).

Per attributi di tipo string sono disponibili ulteriori operazioni:

  • ___starts_with: la stringa inizia con il prefisso dato;
  • ___ends_with: la stringa termina con il suffisso dato;
  • ___contains: la stringa contiene la sotto-stringa data;
  • ___not___starts_with: la stringa non inizia con il prefisso dato;
  • ___not___ends_with: la stringa non termina con il suffisso dato;
  • ___not___contains: la stringa non contiene la sotto-stringa data.

Ad esempio, per Employee sono disponibili i seguenti filtri:

input EmployeeFilter {
  AND: [EmployeeFilter!]
  NOT: EmployeeFilter
  OR: [EmployeeFilter!]
  full_name___eq: String
  full_name___ne: String
  full_name___gt: String
  full_name___gte: String
  full_name___lt: String
  full_name___lte: String
  full_name___in: [String!]
  full_name___null: Boolean
  full_name___not___in: [String!]
  full_name___not___null: Boolean
  full_name___starts_with: String
  full_name___ends_with: String
  full_name___contains: String
  full_name___not___starts_with: String
  full_name___not___ends_with: String
  full_name___not___contains: String
  # ...
}

Se vengono valorizzati più filtri diversi, questi sono implicitamente valutati in AND tra loro.

Esempio: AND implicito

Il campo filter_exp consente di scrivere una espressione Livebase, allo stesso modo di come vengono definite espressioni math o filtri nel modello. L’espressione deve essere booleana, altrimenti il server solleva una Issue di tipo MALFORMED_REQUEST.

Esempio: la query precedente riscritta con filter_exp