EfficientGlideRecord (Client-side)

EfficientGlideRecord is a client-side API class from which you can perform asynchronous client-side GlideRecord-style queries while maximizing performance (eliminating the negative performance impact of using the client-side GlideRecord object) and without having to create a separate GlideAjax Script Include!

Every senior ServiceNow developer knows that client-side GlideRecord queries are slow and inefficient, and that it's far preferable to use a GlideAjax call. However, GlideAjax can be a REAL pain to implement. I've got an entire article about using GlideAjax from both a client and server perspective.
Even I have to look up my own article from time to time to remind myself of the correct patterns when I need to use it, and I groan every time I think about having to create yet another Script Include just to handle this one little use-case in this unique application scope or something.

A couple days ago, I was whingeing on the ServiceNow Developers Discord about the poor performance (and inaccurate documentation) of the client-side GlideRecord API, as well as the fact that it doesn’t support dot-walking.
I was wishing there was something better that didn’t require me to make a whole separate Script Include just to query a single record from the database and get the value of a few fields and one dot-walked field value on that record in my Client Script.

Unfortunately, that’s just the way it is. Client-side GlideRecord is massively inefficient, far too slow, and returns way too much unnecessary data to be used commonly in production code. GlideAjax is simply the best and most efficient method for looking up data from client-side scripts.

At least, it wasuntil now!

After searching for a better solution for another couple of hours, I finally decided:

I'll make a more efficient client-side GlideRecord!

So, I did. And now I’m sharing that solution with you!

This consists of only a few files: A client-callable Script Include that does the back-end work for us, and a Global UI Script that acts as the client-side GlideRecord alternative (which I very creatively named EfficientGlideRecord). There is also a "portal" version of the same UI Script.

Aside from the fact that you'll specify only the fields you want to retrieve from the database for maximum performance (see examples in the API documentation), this is otherwise a near-perfect drop-in replacement for the client-side GlideRecord class; meaning that in the vast majority of cases, you'll be able to take your existing code, change the word "GlideRecord" to "EfficientGlideRecord", call .addField() for each field you want to retrieve, and that's it - you're done!

You might be wondering: "Okay, that's not too much work. I could do a code search for client-side code calling GlideRecord and get a performance and user-experience boost by replacing it with EfficientGlideRecord and adding any fields referenced in the callback function... but just how much performance improvement are we talking about here? Is it actually worth it?"

Oh my sweet summer child... even I was baffled when I did my performance testing, at just how inefficient the client-side GlideRecord is, and by just how much performance could be improved with EfficientGlideRecord.

As you can see in the Performance section (or in the image below), with the fastest internet, performance was improved by 80% (from nearly three full seconds, down to about half a second). For larger queries by users with a slower 1-10Mbps internet connection, performance was improved by as much as 93% - from ~71,700 milliseconds, down to ~5,100ms.

Read on to learn more, see usage examples, and download this free tool as an Update Set!

Index


Enjoying this article? Don’t forget to subscribe to SN Pro Tips!

We never spam you, never sell your information to marketing firms, and never send you things that aren’t relevant to ServiceNow.
We typically only send out 1-4 newsletters per year, but trust me - you don't want to miss them!


EfficientGlideRecord

This new client-side API, EfficientGlideRecord, is a nearly-drop-in-replacement for the client-side GlideRecord API, at least when it comes to queries. I may implement insert/update/delete functionality in a later version.

The client-side API documentation is below. The actual code can be found in the Download and Code section of this page.


addField(fieldName, getDisplayValue)

Add a field to retrieve from the target record(s).
Any fields not specified by calling this method will not be available on the resulting EfficientGlideRecord object in the callback function after calling .query() (except for sys_id, which is always returned). In this case, a warning will be shown in the console, and .getValue('field_name') will return a blank string.
If a second argument (getDisplayValue) is not specified and set to true, then the field's display value will not be available on the resulting EfficientGlideRecord object in the callback function. In this case, .getDisplayValue('field_name') will return a blank string.

Note: You can retrieve the value of “dot-walked” fields by calling .addField() and passing in a string with the dot-walked field name.
For example, if you're querying the incident table and want the assignee's email address, you can use .addField('assigned_to.email') to add the field. In your callback function, while iterating through the returned records, you can then use .getValue('assigned_to.email') to retrieve that field's value!

Parameters

Name Type Required Description
fieldName String True Add a field to retrieve from the target record(s).
Any fields not specified by calling this method will not be available on the resulting EfficientGlideRecord object in the callback function after calling .query(). In this case, a warning will be shown in the console, and .getValue('field_name') will return a blank string.
If a second argument (getDisplayValue) is not specified and set to true, then the field's display value will not be available on the resulting EfficientGlideRecord object in the callback function. In this case, .getDisplayValue('field_name') will return a blank string.
getDisplayValue Boolean False Set this to true to retrieve the display value for the specified field as well. In addition to .getValue(), you'll also then be able to call .getDisplayValue() on the EfficientGlideRecord object in your callback function after performing a query.
If set to false (or if unspecified), the display value is not retrieved from the server; so make sure that if you need to use the display value in your callback function, you set this argument to true.

Example

var egrIncident = new EfficientGlideRecord('incident');
egrIncident.addField('number')
    .addField('assignment_group', true)
    .addField('assigned_to', true);

egrIncident.get('some_incident_sys_id', function(egrInc) {
    g_form.addInfoMessage(
        egrInc.getValue('number') + '\'s assignment group is ' +
        egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
        egrInc.getValue('assignment_group') + ')\n' +
        'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
        egrInc.getValue('assigned_to') + ')'
    );
});

addQuery(fieldName, operator, fieldValue)

Add a query to the EfficientGlideRecord object.
By specifying a field name, operator, and value, you can perform all sorts of queries.
If only two arguments are specified, then it's assumed that the first is the field name and the second is the field value. The operator will automatically be set to "=".

Parameters

Name Type Required Description
fieldName String True The name of the field to perform the query against.
operator String False The operator to use for the query.
Valid operators: =, !=, >, >=, <, <=, STARTSWITH, ENDSWITH, CONTAINS, DOES NOT CONTAIN, IN, NOT IN, INSTANCEOF
Note: If only two arguments are specified (fieldValue is not defined), then the second argument will be treated as the value, and the operator will automatically be set to "=".
fieldValue String True The value to compare, using the specified operator, against the specified field.

Example

new EfficientGlideRecord('incident')
    .setLimit(10)
    .addQuery('assignment_group', '!=', 'some_group_sys_id')
    .addQuery('assigned_to', 'some_assignee_sys_id')
    .addNotNullQuery('assignment_group')
    .addField('number')
    .addField('short_description')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        while (egrIncident.next()) {
            var logMsg = '';
            if (egrIncident.canRead('short_description')) {
                logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
            }
            if (egrIncident.canRead('number')) {
                logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
            }
            if (egrIncident.canRead('assignment_group')) {
                logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
                    egrIncident.getDisplayValue('assignment_group') + ')';
            }

            console.log(logMsg);
        }
    });

addEncodedQuery(encodedQueryString)

Add an encoded query string to your query. Records matching this encoded query will be available in your callback function after calling .query().

Parameters

Name Type Required Description
encodedQueryString String True The encoded query string to use in your query.

Example

var egrIncident = new EfficientGlideRecord('incident');
egrIncident.addField('number');
egrIncident.addField('assignment_group', true);
egrIncident.addField('assigned_to', true);
egrIncident.addEncodedQuery('some_encoded_query_string');

egrIncident.get('some_incident_sys_id', function(egrInc) {
    g_form.addInfoMessage(
        egrInc.getValue('number') + '\'s assignment group is ' +
        egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
        egrInc.getValue('assignment_group') + ')\n' +
        'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
        egrInc.getValue('assigned_to') + ')'
    );
});

addNullQuery(fieldName)

Shorthand for .addQuery(fieldName, '=', 'NULL')

Parameters

Name Type Required Description
fieldName String True The name of the field to use in your query, getting only records where this field is empty.

Example

//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .addField('assignment_group', true) //Get display value as well
    .query(function (egrIncident) {
        var incidentsWithoutShortDesc = [];
        while (egrIncident.next()) {
            incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
        }
        if (incidentsWithoutShortDesc.length > 0) {
            g_form.addErrorMessage(
                'The following child Incidents have no short description:<br />* ' + 
                incidentsWithoutShortDesc.join('<br />* ')
            );
        }
    });

addNotNullQuery(fieldName)

Shorthand for this.addQuery(fieldName, '!=', 'NULL');.

Parameters

Name Type Required Description
fieldName String True The name of the field to use in your query, getting only records where this field is not empty.

Example

new EfficientGlideRecord('incident')
    .setLimit(10)
    .addQuery('assignment_group', '!=', 'some_group_sys_id')
    .addQuery('assigned_to', 'some_assignee_sys_id')
    .addNotNullQuery('assignment_group')
    .addField('number')
    .addField('short_description')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        while (egrIncident.next()) {
            var logMsg = '';
            if (egrIncident.canRead('short_description')) {
                logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
            }
            if (egrIncident.canRead('number')) {
                logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
            }
            if (egrIncident.canRead('assignment_group')) {
                logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
                    egrIncident.getDisplayValue('assignment_group') + ')';
            }

            console.log(logMsg);
        }
    });

orderBy(fieldName)

Orders the queried table by the specified column, in ascending order.

Parameters

Name Type Required Description
fieldName String True Orders the queried table by the specified column, in ascending order

Example

//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        var incidentsWithoutShortDesc = [];
        while (egrIncident.next()) {
            incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
        }
        if (incidentsWithoutShortDesc.length > 0) {
            g_form.addErrorMessage(
                'The following child Incidents have no short description:<br />* ' +
                incidentsWithoutShortDesc.join('<br />* ')
            );
        }
    });

orderByDesc(fieldName)

Orders the queried table by the specified column, in descending order.

Parameters

Name Type Required Description
fieldName String True Orders the queried table by the specified column, in descending order

Example

//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .addField('assignment_group', true) //Get display value as well
    .orderByDesc('number')
    .query(function (egrIncident) {
        var incidentsWithoutShortDesc = [];
        while (egrIncident.next()) {
            incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
        }
        if (incidentsWithoutShortDesc.length > 0) {
            g_form.addErrorMessage(
                'The following child Incidents have no short description:<br />* ' +
                incidentsWithoutShortDesc.join('<br />* ')
            );
        }
    });

setLimit(recordLimit)

Limits the number of records queried from the database and returned in the response.

Parameters

Name Type Required Description
limit Number True The limit to use in the database query. No more than this number of records will be returned.

Example

//Does at least one child Incident exist without a short description?
new EfficientGlideRecord('incident')
    .setLimit(1)
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .query(function (egrIncident) {
        if (egrIncident.hasNext()) {
            g_form.addErrorMessage(
                'At least one child Incident exists without a short description.'
            );
        }
    });

get(recordSysID, callbackFn)

Gets a single record, efficiently, from the database by sys_id.

Parameters

Name Type Required Description
sysID String True The sys_id of the record to retrieve. Must be the sys_id of a valid record which the user has permissions to see, in the table specified in the constructor when instantiating this object.
callbackFn Function True The callback function to be called when the query is complete.
When the query is complete, this callback function will be called with one argument: the EfficientGlideRecord object containing the records resultant from your query. After querying (in your callback function), you can call methods such as .next() and .getValue() to iterate through the returned records and get field values.

Example

var egrIncident = new EfficientGlideRecord('incident');
egrIncident.setLimit(10);
egrIncident.addField('number');
egrIncident.addField('assignment_group', true);
egrIncident.addField('assigned_to', true);


egrIncident.get('some_incident_sys_id', function(egrInc) {
    g_form.addInfoMessage(
        egrInc.getValue('number') + '\'s assignment group is ' +
        egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
        egrInc.getValue('assignment_group') + ')\n' +
        'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
        egrInc.getValue('assigned_to') + ')'
    );
});

query(callbackFn)

Perform the async query constructed by calling methods in this class, and get the field(s) from the resultant record(s) that were requested by calling .addField(fieldName, getDisplayValue).

Parameters

Name Type Required Description
callbackFn Function True The callback function to be called when the query is complete.
When the query is complete, this callback function will be called with one argument: the EfficientGlideRecord object containing the records resultant from your query. After querying (in your callback function), you can call methods such as .next() and .getValue() to iterate through the returned records, and get field values.

Example

new EfficientGlideRecord('incident')
    .setLimit(10)
    .addQuery('assignment_group', '!=', 'some_group_sys_id')
    .addQuery('assigned_to', 'some_assignee_sys_id')
    .addNotNullQuery('assignment_group')
    .addField('number')
    .addField('short_description')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        while (egrIncident.next()) {
            console.log(
                'Short description value: ' + egrIncident.getValue('short_description') + '\n' +
                'Number: ' + egrIncident.getValue('number') + '\n' +
                'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
                egrIncident.getDisplayValue('assignment_group') + ')'
            );
        }
    });

hasNext()

Check if there is a "next" record to iterate into using .next() (without actually positioning the current record to the next one).
Can only be called from the callback function passed into .query()/.get() after the query has completed.

Example

//Does at least one child Incident exist without a short description?
new EfficientGlideRecord('incident')
    .setLimit(1)
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .query(function (egrIncident) {
        if (egrIncident.hasNext()) {
            g_form.addErrorMessage(
                'At least one child Incident exists without a short description.'
            );
        }
    });

next()

Iterate into the next record, if one exists.
Usage is the same as GlideRecord's .next() method.
Can only be run from the callback function after a query using .query() or .get().

Example

new EfficientGlideRecord('incident')
    .setLimit(10)
    .addQuery('assignment_group', '!=', 'some_group_sys_id')
    .addQuery('assigned_to', 'some_assignee_sys_id')
    .addNotNullQuery('assignment_group')
    .addField('number')
    .addField('short_description')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        while (egrIncident.next()) {
            console.log(
                'Short description value: ' + egrIncident.getValue('short_description') + '\n' +
                'Number: ' + egrIncident.getValue('number') + '\n' +
                'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
                egrIncident.getDisplayValue('assignment_group') + ')'
            );
        }
    });

canRead(fieldName)

Returns true if the specified field exists and can be read (even if it's blank).
Will return false in the following cases:
-The specified field on the current record cannot be read
-The specified field does not exist in the response object (which may happen if you don't add the field to your request using .addField()).
-The specified field does not exist in the database

Parameters

Name Type Required Description
fieldName String True The name of the field to check whether the user can read or not.

Example

new EfficientGlideRecord('incident')
    .setLimit(10)
    .addQuery('assignment_group', '!=', 'some_group_sys_id')
    .addQuery('assigned_to', 'some_assignee_sys_id')
    .addNotNullQuery('assignment_group')
    .addField('number')
    .addField('short_description')
    .addField('assignment_group', true) //Get display value as well
    .orderBy('number')
    .query(function (egrIncident) {
        while (egrIncident.next()) {
            var logMsg = '';
            if (egrIncident.canRead('short_description')) {
                logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
            }
            if (egrIncident.canRead('number')) {
                logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
            }
            if (egrIncident.canRead('assignment_group')) {
                logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
                    egrIncident.getDisplayValue('assignment_group') + ')';
            }

            console.log(logMsg);
        }
    });

getValue(fieldName)

Retrieve the database value for the specified field, if the user has permissions to read that field's value.

Parameters

Name Type Required Description
fieldName String True The name of the field to retrieve the database value for.

Example

//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .addField('assignment_group', true) //Get display value as well
    .query(function (egrIncident) {
        var incidentsWithoutShortDesc = [];
        while (egrIncident.next()) {
            incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
        }
        if (incidentsWithoutShortDesc.length > 0) {
            g_form.addErrorMessage(
                'The following child Incidents have no short description:<br />* ' +
                incidentsWithoutShortDesc.join('<br />* ')
            );
        }
    });

getDisplayValue(fieldName)

Retrieve the display value for the specified field, if the user has permission to view that field's value.
Can only be called from the callback function after the query is complete.

Parameters

Name Type Required Description
fieldName String True The name of the field to retrieve the display value for.

Example

//Get assignment groups for child Incidents for some reason
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addField('assignment_group', true) //Get display value as well
    .query(function (egrIncident) {
        var assignmentGroupNames = [];
        while (egrIncident.next()) {
            assignmentGroupNames.push(egrIncident.getDisplayValue('assignment_group'));
        }
        //todo: Do something with the assignment group names
    });

getRowCount()

Retrieves the number of records returned from the query.
If used in conjunction with .setLimit(), then the maximum value returned from this method will be the limit number (since no more records than the specified limit can be returned from the server).

Example

//Show the number of child Incidents missing Short Descriptions.
new EfficientGlideRecord('incident')
    .addQuery('parent', g_form.getUniqueValue())
    .addNullQuery('short_description')
    .addField('number')
    .query(function (egrIncident) {
        if (egrIncident.hasNext()) {
            g_form.addErrorMessage(
                egrIncident.getRowCount() + ' child Incidents are missing a short description.'
            );
        }
    });

Performance Comparison

In my testing, EfficientGlideRecord was, on average, ~83% faster than ServiceNow’s client-side GlideRecord API. As you can see from the table below, the performance difference is massive; especially for users with internet speeds <10Mbps.

These performance tests did not involve interacting with the data much after the query was complete, but I would expect further performance improvements there as well. This is due to the fact that the client-side EfficientGlideRecord object is much smaller and more efficiently-structured; therefore, retrieving values from it should result in even more performance savings.


Download Update Set (v1.0.4)

You can simply import the above Update Set, or you can copy-and-paste the code from the official Github repo.

NOTE: The minified EfficientGlideRecord class (as EfficientGlideRecordPortal) has been added as a JS Include to the default “Stock” Service Portal theme by this Update Set. However, if you use a different theme on your Service Portal, please be sure to create a new JS Include, and point it to the UI Script that’s specifically marked as working with the Service Portal, as you can see in the screenshot below:

Changelog

  • v1.0.4
    • Updated query validation so that queries can now be performed even when no fields are specified.
      • In this case (when no fields are specified using .adddField()), only the sys_id will be returned as an accessible field.
    • The sys_id field will now always be returned, whether specified using .addField() or not.
    • Dot-walking is now supported when calling .addField(), .getValue(), and .getDisplayValue()!
      • To use this functionality, simply call .addField('some_reference_field.some_other_field', getDisplayValue).
      • To get the dot-walked field value after the query, use .getValue('some_reference_field.some_other_field').
      • To get the display value, use .getDisplayValue('some_reference_field.some_other_field').
  • v1.0.3
    • Split this functionality into two separate UI Scripts to work around ServiceNow's weird and broken handling of JS Includes when two UI Scripts have the same name. Portal behavior should now be much more stable. For real this time.
  • v1.0.2
    • Fixed an issue with scope binding specifically in Service Portal.
    • Added a JS Include to the default "Stock" Service Portal theme to include the minified version of EfficientGlideRecord. If you use a different theme for your Service Portal, please be sure to add a JS Include for the portal version of the EfficientGlideRecord UI Script to your theme.
  • v1.0.1
    • Added .getRowCount() method to get the number of rows returned from the server (limited by .setLimit() if specified).
    • Improved documentation and examples (in this article as well as in the JSDoc in the code).
    • Updated get(recordSysID) so you no longer have to call next() in the callback function (it should've been this way from the start; my bad).
  • v1.0.0
    • Initial public release

Upcoming features

  • Ability to insert/update/delete using EfficientGlideRecord
  • A highly efficient client-side .getRowCount() (Done!) (Thanks to Artur for the suggestion!).
  • Probably some bug-fixes. I've tested this thoroughly and am not aware of any bugs, but you jerks are probably going to identify some tiny little bug in my code that will make the inside of my brain itch until I fix it.
    • Just kidding, I absolutely adore people who report any bugs in my code. Please do tell me if you notice any actual or potential bugs.
  • Performance comparison so you can see exactly how much faster EfficientGlideRecord is versus GlideRecord. (done!)
  • If you have additional feature-requests, please let me know by filing a feature request in the official Git repo!

Code

Below, you can see the exact code in the Gist, which you can copy and paste into your environment if you prefer that over importing the XML for the Script Include using the above instructions.

You can also see the latest version of the app in the official repository on Github, where you can also file bug reports and feature requests.

Gist code (expand)