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:
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 strutturatype <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; 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 chetotalCount
non corrisponde al numero diitems
di cui la pagina è formata, tranne quando la pagina è grande abbastanza da recuperare tutti i record possibili;hasPrev
ehasNext
: flag booleani che indicano se ci sono record precedenti o successivi rispetto alla pagina corrente;prevCursor
enextCursor
: strutture di tipoCursor
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
eprev
); - navigare tra le pagine scorrendo l’insieme di record recuperabili (con i campi
offset
,cursor
efromCursor
); - filtrare i dati da ritornare (con i campi
filter
efilter_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
}
Navigare tra le pagine #
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
.