3.3 Caching
The
Dynamics AX application runtime supports the enabling of single-record
and set-based record caching. You can enable set-based caching in
metadata by switching a property on a
table definition or writing explicit X++ code, which instantiates a
cache. Regardless of how you set up caching, you don’t need to know
which caching method is used because the application runtime handles
the cache transparently. To optimize the use of the cache, however, you
must understand how each caching mechanism works.
3.3.1 Record Caches
You can set up three types of record caching on a table by setting the CacheLookup property on the table definition:
Found
FoundAndEmpty
NotInTTS
One additional value (besides None) is EntireTable, which is a set-based caching option we describe later in this section.
The
three record-caching possibilities are fundamentally the same. The
differences lie in what is cached and when cached values are flushed.
For example, the FoundFoundAndEmpty caches are preserved across transaction boundaries, but a table that uses the NotInTTS cache doesn’t use the cache when first accessed inside a transaction scope—it uses it in consecutive select statements, unless a forupdate keyword is applied to the select
statement. The following X++ code example describes when the cache is
used inside and outside a transaction scope, when a table uses the NotInTTS caching mechanism, and when the AccountNum
field is the primary key. The code comments indicate when the cache is
or isn’t used. In the example, it appears that the first two select statements after the ttsbegin
command don’t use the cache. The first doesn’t use the cache because
it’s the first statement inside the transaction scope; the second
doesn’t use the cache because the forupdate keyword is applied to the statement. The use of the forupdate keyword
forces the application runtime to look up the record in the database
because the previously cached record wasn’t selected with the forupdate keyword applied.
static void NotInTTSCache(Args _args) { CustTable custTable; ; select custTable // Look up in cache. If record where custTable.AccountNum == '1101'; // does not exist, look up // in database. ttsbegin; // Start transaction.
select custTable // Cache is invalid. Look up in where custTable.AccountNum == '1101'; // database and place in cache.
select forupdate custTable // Look up in database because where custTable.AccountNum == '1101'; // forupdate keyword is applied.
select custTable // Cache will be used. where custTable.AccountNum == '1101'; // No lookup in database.
select forupdate custTable // Cache will be used because where custTable.AccountNum == '1101'; // forupdate keyword was used // previously.
ttscommit; // End transaction.
select custTable // Cache will be used. where custTable.AccountNum == '1101'; }
|
If the table had been set up with Found or FoundAndEmpty caching in the preceding example, the cache would have been used when executing the first selectselect forupdate statement was executed. statement inside the transaction, but not when the first
Note
By default, all Dynamics AX system tables are set up using a Found cache. This cannot be changed. |
For all three caching mechanisms, the cache is used only if the select statement contains equal-to (==) predicates in the where
clause that exactly match all the fields in the primary index of the
table or any one of the unique indexes defined for the table. The PrimaryIndex
property on the table must therefore be set correctly on one of the
unique indexes used when accessing the cache from application logic.
For all other unique indexes, without any additional settings in
metadata, the kernel automatically uses the cache, if it is already
present. Support for unique-index-based caching is a new feature in
Dynamics AX 2009.
The following X++ code
examples show when the Dynamics AX application runtime will try to use
the cache and when it won’t. The cache is used only in the first select
statement; the remaining three statements don’t match the fields in the
primary index, so they will all perform lookups in the database.
static void UtilizeCache(Args _args) { CustTable custTable; ; select custTable // Will use cache because only where custTable.AccountNum == '1101'; // the primary key is used as // predicate.
select custTable; // Cannot use cache because no // "where" clause exists.
select custTable // Cannot use cache because where custTable.AccountNum > '1101'; // equal to (==) is not used.
select custTable // Will not use cache because where custTable.AccountNum == '1101' // where clause contains more && custTable.CustGroup == '20'; // predicates than the primary // key. }
|
Note
The RecId index, which is always unique on a table, can be set as the PrimaryIndex in the table’s properties. You can therefore set up caching using the RecId field. |
The following X++ code examples show how unique-index caching works in the Dynamics AX application runtime. InventDim in the base application has InventDimId as the primary key and a combination of keys (inventBatchId, wmsLocationId, wmsPalletId, inventSerialId, inventLocationId, configId, inventSizeId, inventColorId, and inventSiteId) as the unique index on the table.
static void UtilizeUniqueIndexCache(Args _args) { InventDim InventDim; ; select inventDim // Will use cache because only where inventDim.inventDimId == '00000001_082'; // the primary key is used as // predicate.
select inventDim // Will use cache where inventDim.inventBatchId == '' // because the column list in && inventDim.wmsLocationId == '' // the "where" clause && inventDim.wmsPalletId == '' // match that of a unique && inventDim.inventSerialId == '' // index for table inventdim && inventDim.inventLocationId == '400' // and the key values point to && inventDim.ConfigId == '01' // same record as the primary && inventDim.inventSizeId == '' // key fetch (inventDimId == && inventDim.inventColorId == '' // '00000001_082'). && inventDim.inventSiteId == '4'; //
select inventDim // Cannot use cache because where inventDim.inventLocationId== '400' // where clause does not && inventDim.ConfigId == '01' // match the unique key list && inventDim.inventSiteId == '4'; // or primary key. }
|
The
Dynamics AX application runtime ensures that all fields on a record are
selected before they are cached. The application runtime therefore
always changes a field list to include all fields on the table before
submitting the SELECT statement to the database when it can’t find the record in the cache. The following X++ code illustrates this behavior.
static void expandingFieldList(Args _args) { CustTable custTable; ; select creditRating // The field list will be expanded to all fields. from custTable where custTable.AccountNum == '1101'; }
|
If the preceding select statement doesn’t find a record in the cache, it expands the field to contain all fields, not just the creditRating
field. This ensures that the fetched record from the database contains
values for all fields before it is inserted into the cache. Even though
performance when fetching all fields is inferior compared to
performance when fetching a few fields, this approach is acceptable
because in subsequent use of the cache, the performance gain outweighs
the performance loss from populating it.
Tip
You can disregard the use of the cache by calling the disableCache method on the record buffer with a Boolean true
parameter. This method forces the application runtime to look up the
record in the database, and it also prevents the application runtime
from expanding the field list. |
The
Dynamics AX application runtime creates and uses caches on both the
client tier and the server tier. The client-side cache is local to the
rich client, and the server-side cache is shared among all connections
to the server, including connections coming from rich clients, Web
clients, .NET Business Connector, and any another connection.
The
cache used depends on which tier the lookup is made from. If the lookup
is made on the server tier, the server-side cache is used. If the
lookup is executed from the client tier, the client first looks in the
client-side cache; if it doesn’t find anything, it makes a lookup in
the server-side cache. If there is still no record, a lookup is made in
the database. When the database returns the record to the server and on
to the client, the record is inserted into both the server-side cache
and the client-side cache.
The caches are
implemented using AVL trees (which are balanced binary trees), but the
trees aren’t allowed to grow indefinitely. The client-side cache can
contain a maximum of 100 records for a given table in a given company,
and the shared server-side cache can contain a maximum of 2000 records.
When a new record is inserted into the cache and the maximum is
reached, the application runtime removes approximately 5 to 7 percent
of the oldest records by scanning the entire tree.
Note
You can’t change the maximum number of records to be cached in metadata or from the X++ code. |
Scenarios
that repeat lookups on the same records and expect to find the records
in the cache can suffer performance degradation if the cache is
continuously full—not only because records won’t be found in the cache
because they were removed based on the aging scheme, forcing a lookup
in the database, but also because of the constant scanning of the tree
to remove the oldest records. The following X++ code shows an example
in which all SalesTable records are looped twice, and each loop looks up the associated CustTable record. If this X++ code were executed on the server and the number of CustTable record lookups was more than 2000, the oldest records would be removed from the cache, and the cache wouldn’t contain all CustTable records when the first loop ended. When the code loops through the SalesTable records again, the records might not be in the cache, and the selection of the CustTable
record would continue to go to the database to look up the record. The
scenario would therefore perform much better with fewer than 2000
records in the database.
static void AgingScheme(Args _args) { SalesTable salesTable; CustTable custTable; ; while select SalesTable order by custAccount { select custTable // Fill up cache. where custTable.AccountNum == salesTable.CustAccount; // More code here. }
while select SalesTable order by custAccount { select custTable // Record might not be in cache. where custTable.AccountNum == salesTable.CustAccount; // More code here. }
}
|
Important
If
you test code on small databases, you can’t track repeat lookups only
by tracing the number of statements parsed to the database. When you
execute such code in a production environment, you can encounter severe
performance issues because this scenario doesn’t scale very well. |
Before
the Dynamics AX application runtime searches for, inserts, updates, or
deletes records in the cache, it places a mutually exclusive lock that
isn’t released until the operation is complete. This lock means that
two processes running on the same server can’t perform insert, update,
or delete operations in the cache at the same time; only one process
can hold the lock at any given time, and the remaining processes are
blocked. Blocking occurs only when the application runtime accesses the
server-side cache. So although the caching possibilities the
application runtime supports are useful features, you shouldn’t abuse
them. If you can reuse a record buffer that is already fetched, you
should do so. The following X++ code shows the same record fetched
twice. The second fetch uses the cache even though it could have used
the first fetched record buffer. When you execute the following X++
code on the server tier, the process might get blocked when the
application runtime searches the cache.
static void ReuseRecordBuffer(Args _args)
{ CustTable custTable; ; select custTable where custTable.AccountNum == '1101';
// Some more code, which does not change the custTable record.
select custTable // The cache will be used, but where custTable.AccountNum == '1101'; // blocking might occur. // Reuse the record buffer // instead. }
|