Implement the global cache and singleton

Completed

You can use caching to improve the performance of your code. The benefits increase for frequently used data or code. You can implement global caching on a specific client session or apply it to all clients on a server. In some scenarios, you can use global caching for extra parameters in a method signature because method signatures are unchangeable with extensions.

Important

You must handle caching with care because it impacts memory consumption. Global caching stores values in-memory, and as a developer, you’re responsible for updating and removing outdated data from the cache. Ignoring the cleanup of global caching can negatively impact performance.

Best practices for the implementation of caching depend on your use cases.

SysGlobalCache and singleton

SysGlobalCache and singleton refer to a single client session cache that isn’t accessible in other system threads. The system cleans this cache when the client session is complete, but it’s best practice to clean up the cache after you use it.

You can create SysGlobalCache of an instance of the SysGlobalCache class, but we recommend that you create it from the following system classes so that cleanup is easier:

  • From the Application class by using the appl system variable
  • From the ClassFactory class by using the classFactory system variable
  • From the Info class by using the Infolog system variable

All listed system classes have globalCache already defined and you can use it.

The SysGlobalCache class stores data on a specific key, and it has several commonly used methods:

  • Construct - Allows you to create a new instance of SysGlobalCache. By using one of the system classes, such as Application, ClassFactory, or Info, this method automatically creates an instance if it doesn’t already exist.
  • Get - Retrieves a value from a specific owner and key.
  • Set - Sets a value on a specific owner and key.
  • IsSet - Checks whether the cache is set for a specific owner and key.

The system stores values in SysGlobalCache by using an owner and a key.

The following example shows how to check if a value exists, get a value, and set a value by using SysGlobalCache.

internal final class RunnableClass1
{

    /// <summary>
    /// Class entry point. The system will call this method when a designated menu 
    /// is selected or when execution starts and this class is set as the startup class.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        const str   mySysGlobalCacheKey   = 'MySysGlobalCacheKey';
        str         mySysGlobalCacheOwner = classStr(RunnableClass1);

        SysGlobalCache cache = classFactory.globalCache();

        container       cacheValue;

        if (cache.isSet(mySysGlobalCacheOwner, mySysGlobalCacheKey))
        {
            cacheValue = cache.get(mySysGlobalCacheOwner, mySysGlobalCacheKey);
        }
        else
        {
            cacheValue = ['MyValue'];

            cache.set(mySysGlobalCacheOwner, mySysGlobalCacheKey, cacheValue);
        }

        cache.remove(mySysGlobalCacheOwner, mySysGlobalCacheKey);

    }

}

You can achieve the same result by using a Singleton class (for the user session), which is an instance of the class that the system creates inside the class. One common use of this instance in finance and operations apps is for the flight classes, as the following code illustrates.

/// <summary>
/// When <c>AccountingFinTagFlight</c> is enabled, the uptake of financial tags functionality in the
/// Source Document and Accounting Framework is turned on. This allows documents implementing SDAF
/// to opt into processing of financial tags and provide their values during accounting distribution.
/// </summary>
public final class AccountingFinTagFlight extends Flight
{
    private static AccountingFinTagFlight singleton = new AccountingFinTagFlight();

    [Hookable(false)]
    protected boolean isEnabledByDefault()
    {
        return false;
    }

    [Hookable(false)]
    public static AccountingFinTagFlight instance()
    {
        return singleton;
    }

}

In this example from the standard application, the system creates an instance of the AccountingFinTagFlight class, but only once for the user session.

The use case defines the approach, and the system uses SysGlobalCache and singleton in the standard application.

SysGlobalObjectCache

SysGlobalObjectCache is server-shared caching across all user sessions on a specific server.

The SysGlobalObjectCache cache is a singleton, and you can only use it once in the system. Getting it from the ClassFactory class or initializing a new SysGlobalObjectCache always refers to the first created instance of the SysGlobalObjectCache class.

You would define SysGlobalObjectCache on a scope rather than associating it with an owner, as in SysGlobalCache.

The purpose of using SysGlobalObjectCache is to store static values in a cache for all sessions so that the system doesn't need to reread or calculate them for each session.

The following example from finance and operations apps illustrates a new instance of SysGlobalObjectCache. However, if the instance is already created, the system refers to the existing SysGlobalObjectCache, so only one, or a singleton, exists in the system.

public boolean findExternalDescription()
{
    boolean                     found;
    container                   cacheKey;
    container                   cacheValueExistsCheck;
    boolean                     continueProcessing;
    SysGlobalObjectCache        sysGlobalObjectCacheExistCheck = new SysGlobalObjectCache();

    cacheKey = [custVendAccountId, this.parmCustVendItemGroupId(), this.moduleType(), curExt()];

    //Check for existence
    cacheValueExistsCheck = sysGlobalObjectCacheExistCheck.find(CustVendExternalItemDescription::cacheScopeExistsCheck(), cacheKey);

    //We have not checked this combination before
    if (cacheValueExistsCheck == conNull())
    {
        if (CustVendExternalItem::existsForCustVendRelation(custVendAccountId, this.parmCustVendItemGroupId(), this.moduleType()))
        {
            // at least 1 record available for the customer/vendor group or customer/vendor
            continueProcessing = true;
        }
        else
        {
            // no record available for the customer/vendor group or customer/vendor
            continueProcessing = false;
        }
        sysGlobalObjectCacheExistCheck.insert(CustVendExternalItemDescription::cacheScopeExistsCheck(), cacheKey, [continueProcessing]);
    }
    else
    {
        [continueProcessing] = cacheValueExistsCheck;
    }

    if (continueProcessing)
    {
        [found, externalItemId, externalItemFreeTxt, custVendExternalItem] =
                CustVendExternalItemDescription::findExternalItemDescription(this.moduleType(), itemId, inventDim,
                                                                             custVendAccountId,this.parmCustVendItemGroupId());
    }

    return found;
}

You can remove all entries in SysGlobalObjectCache for a server by running a class that uses the following URL: https://[your finance and operations apps url]/?mi=SysClassRunner&cls=SysFlushAOD.

The SysFlushAOD class clears all cache in SysGlobalObjectCache.

Context classes

With extensions, you can't change the method signature, which means that you can’t add new parameters to an existing method.

Note

As previously mentioned, you can change the method signature with over-layering by adding a new parameter for a method. However, you can extend the method signature by using a Disposable context. When you implement IDisposable on the context class, it forces you to have a dispose method to use for cleanup.

The following example creates a context class for SalesPool with one parameter named myParameter.

internal final class MySalesPoolContext
{
    static MySalesPoolContext salesPoolContext;
 
    public  boolean myParameter;
  
    public void new()
    {
        salesPoolContext = this;
    }
 
    public void dispose()
    {
        salesPoolContext = null;
 
    }
  
    static public MySalesPoolContext current()
    {
        return  salesPoolContext;
 
    }

}

Now you can add an extension to the find method for the SalesPool table. You must test the new parameter that you created in the context class without changing the signature of the find method, as follows:

[ExtensionOf(tableStr(SalesPool))]
final class SalesPool_MB500_Extension
{
    static SalesPool find(SalesPoolId  _salesPoolId,
                          boolean     _forUpdate)
    {
        SalesPool  salesPool = next find(_salesPoolId, _forUpdate);

        MySalesPoolContext  salesPoolContext = MySalesPoolContext::current();

        if (salesPoolContext && salesPoolContext.myParameter)
        {
            // some code...
        }

        return salesPool;
    }

}

For test purposes, the following example creates a new runnable class, and you can configure the context parameter variable to call the disposal methods to clean up after using the class.

internal final class MySalesPoolContextTest
{
    /// <summary>
    /// Class entry point. The system will call this method when a designated menu 
    /// is selected or when execution starts and this class is set as the startup class.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        SalesPool           salesPool;
        MySalesPoolContext  mySalesPoolContext = new MySalesPoolContext();

        mysalesPoolContext.myParameter = true;

        salesPool = SalesPool::find('10');

        if (mySalesPoolContext)
        {
            mySalesPoolContext.dispose();
        }
    }

}

Creating a context class as a singleton helps you transfer parameters without changing the method signature.