Best practices
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.
query read_gla
{
useCompany (no: 123456789)
{
generalLedgerAccount
{
items
{
accountNo
name
}
}
}
}
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.
query read_gla($cid : Int!)
{
useCompany (no: $cid)
{
generalLedgerAccount
{
items
{
accountNo
name
}
}
}
}
Variables:
{
"cid" : 123456789
}
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.
query read_gla($cid : Int!)
{
useCompany (no: $cid)
{
generalLedgerAccount
{
items
{
accountNo
name
}
}
}
}
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 valuenull
(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.
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"
}
}
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.
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
}
}
}
}
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:
- The order is added to the
Order
table and itsorderNo
(at least) is retrieved. - The order lines are added to the
OrderLines
table for the newly created order.
In your application, you must handle the following cases:
- 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.
- 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 theOrder
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.
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
}
}
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.
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
}
}
}
}
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
}
}
}
}