Best practices

GraphQL best practices guide for naming queries, parameterizing with variables, fetching only needed data, ensuring input order, batch inserting, handling transactions, and executing long-running operations asynchronously.

This page contains a series of recommendations for creating GraphQL queries and mutations.

Name the queries

Although it’s possible to create anonymous queries (and mutations) it is recommended that you give each operation a relevant name. The main benefits is that it indicates the purpose of each operation in a clear manner.

Tip

Give each query a name.

Recommended ✅

query read_gla
{
  useCompany (no: 123456789)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
      }
    }
  }
}
Not recommended ❌

query
{
  useCompany (no: 123456789)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
      }
    }
  }
}

Parameterize the queries

You probably have to run the same query with different input values (such as filters or inputs for create and update operations). At the least, you need to provide either a company number or a customer number or run any query. You should avoid hardcoding such input values and use variables to provide them. The main advange is that this makes a query reusable.

Best practice

Use variables to parameterize queries.

Recommended ✅

query read_gla($cid : Int!)
{
  useCompany (no: $cid)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
      }
    }
  }
}

Variables:

{
  "cid" : 123456789
}
Not recommended ❌

query read_gla
{
  useCompany (no: 123456789)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
      }
    }
  }
}

Read only the data you need

A major advantage of GraphQL compared to other API standards (such as REST or SOAP) is that it solves the problem of data overfetching (fetching more data than what you need) and underfetching (fetching less data than what you need). This is because GraphQL is a data-oriented API. In GraphQL, you can fetch exactly what you need, no more, no less.

Even when you fetch data from the same table, sometimes you might need more fields, sometimes less. Reading only the data you need improves the performance by reducing unnecessary operation on the server and data transfer over the network.

Best practice

Always fetch only the data that you need for the current operation.

Example: you need to display the general ledger account numbers and names.

Recommended ✅

query read_gla($cid : Int!)
{
  useCompany (no: $cid)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
      }
    }
  }
}
Not recommended ❌

query read_gla($cid: Int)
{
  useCompany (no:$cid)
  {
    generalLedgerAccount
    {
      items
      {
        accountNo
        name
        name2
        shortName
        accountGroup
        accountSubType
        taxCode
        changedDate
        changedTime
        createdDate
        createdTime
        joinup_Currency {
          isoCode
          name
        }
      }
    }
  }
}

Input values have a specific order

When you insert a new record or update an existing one, the order of the given input fields is important. Various business logic operations occur in the backend based on the order of these fields.

Therefore, you must be aware of the order of fields for various tables and use it accordingly. Otherwise, the result of an insert or update operation will be (at least partially) incorrect.

Best practice

When you specify inputs, do the following:

  • fields must be specified in the correct order
  • if a field should have a specific value, provide it as an input (e.g. customerNo : 42)
  • if a field should remain unassigned, do not list it in the input
  • if a field should have a system given value, specify null for its value (e.g. voucherNo : null)
  • if a field should have a system given value but in a given interval and you want to specify the interval, use the _suggest_interval suffixed fields (e.g. customerNo_suggest_interval: {from: 10000, to: 20000}). If you do not want to specify the interval, use the regular input field and give the value null (e.g. customerNo : null).
  • do not explicitly request a suggested value for primary key fields (the system does that implicitly)

Do not fetch total count when you don’t need it

When you fetch a list of records, you can also fetch the total count of records that match the given filter. Although there are cases when you need this information, there are also cases when it’s not need. For instance, when you need to fetch all the records from a table, you don’t really need the total count. You do a repetitive fetch of the records (page by page) until there are no more records to fetch.

Fetching of the total count translates to a SELECT Count(*) FROM table. On large tables (with millions or tens of millions of records), this operation can be very slow, an can take up 20-30 seconds to execute.

Best practice

Do not fetch the total count when you don’t need it. Avoid fetching the total count when you fetch all the records from a table.

Do not pass entire input objects as variables

Variables are parsed and deserialized into objects on the server. This results in loosing the order of the fields as given in the input variables. However, as explained above, the order of the fields is important. The outcome is an incorrect result of the insert or update operation.

Best practice

When you specify inputs, do not pass entire input objects as variables.

Recommended ✅

mutation create_order($cid : Int!,
                      $cno: Int,
                      $ourref : String,
                      $yourref : String)
{
 useCompany(no : $cid)
 {
  order_create(values: [{
         customerNo : $cno,
     orderType : 2, 
     orderPreferences : 0,
     information5 : "1",
     information6 : null,
     transactionType : 1,
     ourReference : $ourref,
     yourReference : $yourref
      }])
  {
   affectedRows
   items
   {
    orderNo
   }
  }
  
 }
}

{
 "cid":12345678,
 "order":
 { 
  "customerNo":12345,
  "ourReference":"<john.doe@test.com>",
  "yourReference":"Ref Test"
 }
}
Not recommended ❌

mutation create_order($cid : Int!,
                      $order: Order_Input!)
{
 useCompany(no : $cid)
 {
  order_create(values: [$order])
  {
   affectedRows
   items
   {
    orderNo
   }
  }
  
 }
}

{
 "cid":12345678,
 "order":
 { 
  "customerNo":12345,
  "orderType":2, 
  "orderPreferences":0,
  "information5":"1",
  "information6":null,
  "transactionType":1,
  "ourReference":"<john.doe@test.com>",
  "yourReference":"Ref Test"
 }
}

Batch multiple inserts into a single request

If you need to insert multiple records into the same table, doing so one record at a time will incur performance penalties, the more significant the more records you have. This will in incorrect

Best practice

Insert multiple records into a table with in a single request.

Recommended ✅

mutation create_voucher($cid: Int, $batchId: Int!)
{
   useCompany(no: $cid)
   {  
      voucher_create(values: [
         {
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 1300
            creditAccountType: 2
            creditAccountNo: 50000
         },
         {
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 600
            debitAccountType: 3
            debitAccountNo: 4300
         },
         {
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 700
            debitAccountType: 3
            debitAccountNo: 1930
         }])
      {
         affectedRows
         items
         {
            voucherNo
         }
      }
   }
}
Not recommended ❌

mutation create_voucher($cid: Int, $batchId: Int!)
{
   useCompany(no: $cid)
   {  
      voucher_create(values: [{
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 1300
            creditAccountType: 2
            creditAccountNo: 50000
         }])
      {
         affectedRows
         items
         {
            voucherNo
         }
      }
   }
}

mutation create_voucher($cid: Int, $batchId: Int!)
{
   useCompany(no: $cid)
   {  
      voucher_create(values: [{
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 600
            debitAccountType: 3
            debitAccountNo: 4300
         }])
      {
         affectedRows
         items
         {
            voucherNo
         }
      }
   }
}

mutation create_voucher($cid: Int, $batchId: Int!)
{
   useCompany(no: $cid)
   {  
      voucher_create(values: [{
            batchNo: $batchId
            voucherNo : null
            voucherDate: null
            amountDomestic: 700
            debitAccountType: 3
            debitAccountNo: 1930
         }])
      {
         affectedRows
         items
         {
            voucherNo
         }
      }
   }
}

Implement transactions in your application

Business NXT GraphQL does not support transactional operations. If a request succeeds partially and has failed operations, the successful operations will be persisted. There is no mechanism to request the system to roll back the operations when something failed. This must be done in the client application.

Best practice

Ensure that operations that must succeed entirely to be persisted are handled transactionally in your application.

Let us consider the example of creating an order. This is done in two steps:

  1. The order is added to the Order table and its orderNo (at least) is retrieved.
  2. The order lines are added to the OrderLines table for the newly created order.

In your application, you must handle the following cases:

  1. Creating the order fails. In this case, the order lines should not be inserted. If the operation succeeds partially, you must verify the returned error and decide whether you can continue, or the order should be deleted from the table and the operation retried.
  2. Creating one or more order lines failed. Typically, you do not want partial/incomplete orders in the system. Therefore, in this case, you must delete the successfully added lines (from the OrderLines table) and the order (from the Order table).

Long running operations should be async

If a GraphQL request takes more than 30 seconds to execute, you will get an HTTP timeout, even though the operation will continue to execute on the server. To avoid this problem that implies loosing the result of the operation, Business NXT provides the ability to execute the operations asynchronously.

You should identify potentially long running operations and execute them async. You will receive an operation ID immediately, and you will use this to poll for the result.

Best practice

Execute long running operations asynchronously.

Recommended ✅

mutation update_batches_async($args: Dictionary)
{
  asyncQuery(query:"""
mutation update_batches($cid: Int)
{
  useCompany (no:$cid)
  {
    batch_processings
    {
      updateBatch
      {
        succeeded
        voucherJournalNo
      }
    }
  }
}
""", args: $args)
    {
       operationId
       status
    }
}

Variables:

{
   "args" : {
      "cid" : 123456789
   }
}
Not recommended ❌

mutation update_batches($cid: Int)
{
  useCompany (no:$cid)
  {
    batch_processings
    {
      updateBatch
      {
        succeeded
        voucherJournalNo
      }
    }
  }
}

Variables:

{
   "cid" : 123456789
}

Use batched requests judiciously

You can send multiple requests in a single HTTP request. This is called batched requests. It is possible to batch both queries and mutations together in any order. They are executed individually, one by one, in the order they where provided.

When batching multiple request, the results will only be returned after all the requests have been executed. A large number of requests could make the overall operation exceed the HTTP timeout interval. In this case, the operation will timeout and the result will be lost.

You should use batched requests judiciously, making sure their overall execution time will not trigger an operation timeout!

Best practice

Prefer to use an insert with multiple records over batching multiple inserts, or, similarly, an update with multiple records over batching multiple updates.

Recommended ✅

mutation multi_update($cid : Int!,
                      $ono : Int!)
{
  useCompany(no : $cid)
  {
    orderLine_update(
      filters: [
        {_and:[
            {orderNo : {_eq : $ono}}
            {lineNo : {_eq : 1}}]},
        {_and:[
            {orderNo : {_eq : $ono}}
            {lineNo : {_eq : 2}}]}
      ]
      values: [
        {
          priceInCurrency : 199.99,
          invoiceNow : 1.0
        },
        {
          priceInCurrency : 59.99,
          invoiceNow : 1.0
        },
      ])
    {
      affectedRows
      items
      {
        orderNo
        lineNo
        quantity
        priceInCurrency
        invoiceNow
      }
    }
  }
}
Not recommended ❌

mutation one_update($cid : Int!,
                    $ono : Int!)
{
  useCompany(no : $cid)
  {
    orderLine_update(
      filter:
        {_and:[
            {orderNo : {_eq : $ono}}
            {lineNo : {_eq : 1}}]},
      value:
        {
          priceInCurrency : 199.99,
          invoiceNow : 1.0
        })
    {
      affectedRows
      items
      {
        orderNo
        lineNo
        quantity
        priceInCurrency
        invoiceNow
      }
    }
  }
}

mutation another_update($cid : Int!,
                        $ono : Int!)
{
  useCompany(no : $cid)
  {
    orderLine_update(
      filter:
        {_and:[
            {orderNo : {_eq : $ono}}
            {lineNo : {_eq : 2}}]},
      value:
        {
          priceInCurrency : 59.99,
          invoiceNow : 1.0
        })
    {
      affectedRows
      items
      {
        orderNo
        lineNo
        quantity
        priceInCurrency
        invoiceNow
      }
    }
  }
}
Last modified September 24, 2024