skip to main content

Read Data

Read services allow you to get graphs of objects from a certain class.

Read services basically work with two structures: type <ClassName> (single object) and type <ClassName>Page (subset of objects). By combining the two structures, it is possible to request entire object graphs for output.

Reference model #

All examples on this page reference the model in the figure:

Employee

The Employee class, center, as modeled in Tutorial.

The Employee class has a role for each supported relation type, as well as native, query, math, and platform attributes declared on it.

get service #

The Query.<ClassName>___get service allows you to get a graph of objects from an object of a certain class.

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

The service requests the ID of the object to be retrieved as input and returns the corresponding structure <ClassName>. More information about the output of the service is available at the bottom.

For example, the service to retrieve an Employee object is as follows:

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

An example query with this service is available here.

getBy<UniqueConstraints> services #

For each uniqueness constraint defined on <ClassName> a Query.<ClassName>___getBy<UniqueConstraints> service is generated that works with that constraint. Each service allows an object graph to be obtained from an object of a certain class, identified by a unique constraint (or key) on it defined. Clearly, services of this type are not generated if no unique constraints are defined on the class in question.

A key may include one or more attributes; in the case of multiple attributes, they appear separated by underscores in the service name (getBy<Constraint1>_<Constraint2>_<Constraint3>).

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

Each service requests as input the values of the attributes that form the key and returns the structure <ClassName>.

For example, for the Employee class, the services are available:

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
}

which can be used as follows:

{
  Employee___getByLast_name_First_name_Date_of_birth(
    date_of_birth: "06/23/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
  }
}

getPage service #

The Query.<ClassName>___getPage service allows you to get a graph of objects from a subset of objects of a certain class.

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

The service is generated for all managed and main (non-part) classes, so it makes sense to retrieve a list of persistent instances; it is therefore not generated for Form, Enum and Singleton classes.

The service returns the structure <ClassName>Page as output. You can control the subset of objects returned by the page by enhancing the optional options argument. More information is available here: Customize-page request.

For example, the service to retrieve a subset of Employee objects is as follows:

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

An example query with this service is available here.

type <ClassName> #

Let’s recap what we said on the page The Graphql Schema: Model-Schema Mapping:

  • each class <ClassName> in the model is mapped to the type <ClassName> structure;
  • type <ClassName> is generated for all managed (enabled) main classes and for all enabled part classes that are reachable by at least one enabled main class;
  • type <ClassName> contains all attributes and enabled roles of the class;
  • attribute types are mapped as shown in Primitive data types.
  • The platform attribute __id is always present named _id, even if not explicitly enabled (in GraphQL, the double underscore __ is a prefix reserved for metadata);

For example, for the Employee class, you have the following Employee structure:

type Employee {
  # platform attributes
  _id: ID!

  # native attributes
  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

  # derived attributes
  full_name: String
  age: Int
  junior: Boolean
  team_name: String
  qualifications: String
  is_active: Boolean

  # outgoing roles
  # ...

  # assignable
  # ...
}

All outgoing roles (associations and compositions) from <ClassName> to a target class are mapped, with respect to the class pointed to by the relation, as follows:

  • to-one roles : <TargetClassName> (analogous to <ClassName>);
  • to-many roles: <TargetClassName>Page (analogous to <ClassNamePage>).

The name of the outgoing role on <ClassName> is the same as the name of the role on the model class (TargetRoleName>).

For Employee, the outgoing roles are mapped as follows:

type Employee {
  # attributes
  # ...

  # outgoing roles
  team: Team # association to 1
  supervisor: Employee # reflexive association to 1
  address: Address # composition to 1
  qualification_(options: QualificationPageOptions): QualificationPage # association to N
  assignments(options: Project_assignmentPageOptions): Project_assignmentPage # composition to N

  # assignments
  # ...
}

Finally, on <ClassName> there is information about the assignable objects for the outgoing roles of that class. The name of these fields is <TargetRoleName>___associables, while the type is <TargetClassName>Page for both many and one associations. The contents of these fields basically correspond with the set of objects you would see in the web client by opening the Livetable of associables for those roles; even in GraphQL, records that do not satisfy any Selection filter (or Selection path) do not appear in the list of associables.

For Employee, associables are mapped as follows:

type Employee {
  # attributes
  # ...
  # outgoing roles
  # ...

  # associates
  team___associables(options: TeamPageOptions) # association to 1
  supervisor___associables(options: EmployeePageOptions) # reflexive 1-way association
  qualification___associables(options: QualificationPageOptions) # association to N
}

As is true for the getPage service, for <ClassName> fields that return a page, you can control the subset of objects returned by properly setting the optional options argument, of type <TargetClassName>PageOptions. More information is available here: Customize-page request.

type <ClassName>Page. #

Both the <ClassName>___getPage service and type <ClassName> fields related to roles and associates return the type <ClassName>Page structure, containing a subset of objects of class <ClassName>.

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

The actual objects are included in the non-null field items, which returns a list of type <ClassName>. When you output this field, you must specify which fields you want to retrieve for each record in the list; in other words, the structure of each <ClassName> element in the list of items must be described in the query.

{
  items {
    scalarField1
    scalarField2
    # ...
  }
}
{
  // all objects have the same structure
  "items": [
    {
      "scalarField1": "abcdef",
      "scalarField2": 12
    },
    {
      "scalarField1": "xxyyzz",
      "scalarField2": 314
    }
    // ...
  ]
}
Example: using the items field.

In addition to items, the structure contains the following metadata:

  • totalCount: total number of records retrievable for that class from that path, net of any filters modeled on the class or role; the count also depends on any filters applied to the page. It should be noted that totalCount does not correspond to the number of items of which the page consists, except when the page is large enough to retrieve all possible records;
  • hasPrev and hasNext: boolean flags indicating whether there are previous or next records to the current page;
  • prevCursor and nextCursor: Cursor type structures that point to the previous page and current page, respectively, based on the current sort and offset.

This information can be reused used to navigate between pages.

Customizing the paged request #

As stated earlier, it is possible to control the subset of objects returned by the page; both the <ClassName>___getPage service, and type <ClassName> fields related to roles and associates, in fact accept the optional options argument, of type <ClassName>PageOptions.

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

Valuing the options of this input type makes it possible to:

  • order the data to be returned (with the orderBy field);
  • limit the size of the current page, i.e. the number of values to return (with the next and prev fields);
  • navigate between pages by scrolling through the set of retrievable records (with the fields offset, cursor and fromCursor);
  • filter the data to be returned (with the filter and filter_exp fields).

Default options #

When the options argument is not specified, the server sorts the records by increasing ID and selects the first 10 results.

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]}
  )
}

The server uses these default values for the next, offset and orderBy fields even when the request includes options but omits these fields. In practice, the server performs the merge of the default options with the custom options.

Example: equivalent queries

Sort results #

The set of objects returned by the page can be sorted by populating the orderBy field, which contains a list of constant values of type enum <ClassName>Sort.

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

Each constant represents a sorting criterion, based on an attribute of the class and a verse (ascending or descending); the criterion name respects the format <AttributeName>___ASC or <AttributeName>___DESC. These constants are generated for all managed attributes of the class.

enum ClassNameSort {
  attribute1Name___ASC
  attribute1Name___DESC

  attribute2Name___ASC
  attribute2Name___DESC
  # ...
}

For example, the following criteria are available for Employee:

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
  # ...
}

The page is sorted according to the chosen criteria:

  • if no criteria were chosen, the server automatically sorts by _id___ASC;
  • if only non-unique attributes have been chosen, the server automatically includes _id___ASC as a sub-criterion (if the order is equal, records are sorted by ID).
Example: sorting

Limit the number of results #

As mentioned earlier, the default pagination returns a maximum of 10 elements. You can reduce or increase the size of the page by populating a field between next and prev.

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

next allows you to select the next N records forward from the current position. Conversely, prev lets you select N records backward, again relative to the current position. The current position is dictated by the offset (or cursor), so it is useful to combine these fields to navigate between pages.

It may also be useful to use the hasNext and hasPrev flags returned by <ClassName>Page to dynamically correct the next or prev values.

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

The GraphQL server supports two pagination modes: offset-based and cursor-based. The former makes use of the offset field (in addition to next and prev), while the latter makes use of the cursor or fromCursor fields. In both, the goal is to move the current position of the pointer to the list of records retrievable by the paged request. In fact, as stated earlier, by default, the server sorts the records by ascending ID, “positions” itself on the first one (offset=0), and selects the next 10 records (next=10) from there.

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

In offset-based pagination, we specify a shift (offset) from “zero”, which is the first element of the list sorted according to the chosen criteria (by default, _id___ASC).

Examples: offset-based pagination.

In cursor-based pagination, we make use of cursors to scroll through results. A cursor is a structure that “points” to the current record uniquely in the context of pagination; it, therefore, depends on the sorting chosen (orderBy), the direction of the scroll, and the size of the page (both set by prev or next).

We take the nextCursor and prevCursor fields offered by the <ClassName>Page structure:

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

The Cursor type is a scalar, serialized as a string. When we make a paged request, the server creates temporary values and offers them in the nextCursor and prevCursor fields; in subsequent calls, we can reuse these strings by inserting them into the cursor field of <ClassName>PageOptions. This way, we can scroll the results forward or backward, depending on which value we used between nextCursor and prevCursor.

Example: using the cursor field.

You can make use of cursors pre-calculated by the server with prevCursor and nextCursor, as seen above, or you can specify your own. In fact, the fromCursor field allows you to specify a value of type <ClassName>Cursor. This structure describes a record from which you want to scroll through the list; starting with the record described by the cursor, you can request previous or subsequent records.

input ClassNameCursor {
  _id: ID
  # attributes of ClassName
}

The available fields are all the managed attributes of <ClassName>, which are all optional. We can reference an existing record from which to start scrolling by populating these fields. The check if this record really exists intentionally loose: if the record exists, the result is a page obtained starting from the included record; vice versa, if it doesn’t exist, the page will start from the first “next” record, according to the chosen sorting criteria and respecting the pagination direction.

Filter results #

The set of objects returned by the page can be filtered by populating the filter or filter_exp fields; the former consists of a way of defining filters based on the GraphQL schema, the latter allows you to use Livebase expressions, in the same way as math expressions or filters are defined in the model.

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

The filter field is of type <ClassNameFilter>; this structure provides filters on a class based on its managed attributes. Each filter thus generated follows the format <AttributeName>___<FilterOperation>, while the data type depends on the operation. Logical operators (AND, OR and NOT) are also available to apply to a set of filters.

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

The supported operations are:

  • ___eq: the value exactly matches with the given value of the same type as the attribute;
  • ___ne: the value is different from the given value of the same type as the attribute;
  • ___gt: the value is greater than the given value of the same type as the attribute;
  • ___gte: the value is greater than or equal to the given value of the same type as the attribute;
  • ___lt: the value is less than the given value of the same type as the attribute;
  • ___lte: the value is less than or equal to the given value of the same type as the attribute;
  • ___in: the value appears in the given list of values of the same type as the attribute;
  • ___null: checks if the value is null (true) or not null (false);
  • ___not___in: the value does not appear in the given list of values of the same type as the attribute;
  • ___not___null: checks if the value is non-null (true) or null (false).

Additional operations are available for attributes of type string:

  • ___starts_with: the string starts with the given prefix;
  • ___ends_with: the string ends with the given suffix;
  • ___contains: the string contains the given sub-string;
  • ___not___starts_with: the string does not begin with the given prefix;
  • ___not___ends_with: the string does not end with the given suffix;
  • ___not___contains: the string does not contain the given sub-string.

For example, the following filters are available for Employee:

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
  # ...
}

If multiple different filters are valued, they are implicitly evaluated in AND with each other.

Example: implicit AND
{
  Employee___getPage(
    options: {
      filter: {
        AND: {
            first_name___starts_with: "A"
            last_name___ends_with: "E"
        }
      }
    }
  )  {
    totalCount
    hasNext
    items {
      _id
      first_name
      last_name
      date_of_birth
    }
  }
}

{
  Employee___getPage(
    options: {
      filter_exp: "startsWith(first_name,\"A\") && endsWith(last_name,\"E\")"
    }
  )  {
    totalCount
    hasNext
    items {
      _id
      first_name
      last_name
      date_of_birth
    }
  }
}