Chapter 2: Working With Data: Objectives
Chapter 2: Working With Data: Objectives
Introduction
A Microsoft Dynamics AX application processes large amounts of data. Most
functions involve sending data between the client and Application Object Server
(AOS) and between the AOS and database server. It is important to use the
correct approach to database access when developing in Microsoft Dynamics
AX. Almost every performance bottleneck is associated with database traffic.
2-1
Development III in Microsoft Dynamics® AX 2012
While Select
This section describes the different qualifiers and options that can be used in the
select statement, to achieve optimal database access performance.
[repeatableRead] [validTimeState]
[ * | <fieldlist> from] <tablebuffer>
[ index [hint] <indexname> ]
[ group by {<field>} ]
[ order by {<field> [asc][desc]} ]
[ where <expression> ]
[ outer | exists | notexists ] join [reverse]
[ * | <fieldlist> from] <tablebuffer>
[ index <indexname> ]
[sum] [avg] [minof] [maxof] [count]
[ group by {<field>} ]
[ order by {<field> [asc][desc]} ]
[ where <expression> ]
]
General Optimization
To optimize general performance, the following tools and keywords may be
used.
Fieldlist
One way to optimize communication with the database is to specify which fields
are returned. For example, for a table with 40 fields, reading the information
from only four fields will reduce the amount of data sent from the database server
by up to 90 percent.
2-2
Chapter 2: Working with Data
NOTE: Use this optimization with care. If the record returned from the database
is subsequently passed as a parameter to other methods, that method may have
been written on the assumption that all fields are set. Only use field lists when
controlling access to the information locally.
Aggregation
To obtain a sum of records, consider instructing the database to calculate the sum
and only return the result, as an alternative to reading all the records and making
the aggregation yourself. To receive a sum specified for one or more fields in the
table, combine the aggregation with a group by clause. The following
aggregation clauses are available.
qty = inventTrans.amountMST;
countInventTrans = ledgerTrans.recId;
2-3
Development III in Microsoft Dynamics® AX 2012
Join
To read records in a main table and then process related records in a transaction
table for each main table record, one solution is to make one "while select"
statement which loops over the records in the main table and another nested
"while select" statement which loops over the transaction records related to the
current record in the main table. The following is an example of a nested "while
select" statement
To process 500 records in the main table, this approach would have 501 SQL
statements executed on the database.
Alternatively, making a single "while select" statement with a join clause reduces
the number of SQL statements to just 1.
The following example illustrates using a join clause (and fieldlists for extra
performance).
ForceLiterals
ForceLiterals instructs the kernel to reveal the actual values used in the "where"
clauses to the database server at the time of optimization. This is the default
behavior in all "join" statements involving more than one table from the
following table groups:
• Miscellaneous
• Main
• Transaction
• Worksheet Header
• Worksheet Line
2-4
Chapter 2: Working with Data
The advantage of using this keyword is that the server now gets all information
needed to calculate the optimal access plan for a statement. The disadvantage is
that the access plan cannot be reused with other search values and that the
optimization may use more CPU resources on the database server. High
frequency queries should not use literals.
}
}
ForcePlaceholders
ForcePlaceholders instructs the kernel not to reveal the actual values used in
where clauses to the database server at the time of optimization. This is the
default in all non-join statements. The advantage of using this keyword is that the
kernel can reuse the access plan for other similar statements with other search
values. The disadvantage is that the access plan is computed without considering
that data distribution might not be even, or that the access plan is an "on average"
access plan.
}
}
2-5
Development III in Microsoft Dynamics® AX 2012
FirstFast
FirstFast instructs the SQL-database to prioritize fetching the first few rows fast
over fetching the complete result set. This also means that the SQL-database
might select an index fitting the order by clause over an index fitting the "where"
clause. The FirstFast hint is automatically issued from all forms, but is rarely
used directly from X++.
Firstonly
NOTE: It is best practice to use this in the "find" methods on the tables.
return custTable;
}
This keyword can be used either in the form index <indexName> or index hint
<indexName>. If only index is used, Microsoft Dynamics AX generates an
order by clause that corresponds to the index components.
2-6
Chapter 2: Working with Data
An incorrectly used index hint can affect performance, so index hints should
only be applied to SQL statements that do not have dynamic where or order by
clauses and where the effect of the hint can be verified.
ForceSelectOrder
This keyword forces the database server to access the tables in a join in the given
order. If two tables are joined the first table in the statement is always accessed
first. This keyword is frequently combined with the forceNestedLoop keyword.
One situation where it can be interesting to force a select order is when you use
index hint on a join. The following construction is an example of the
ForceSelectOrder.
}
}
Give the database a hint on using the index ItemIdx on the table InventTrans.
This works well if the database searches this table first. But if the database, with
the help of the generated statistics, starts with the table InventDim and then finds
records in InventTrans for each occurrence of InventDimId, the use of the
index ItemIdx may not be an appropriate approach. To hint indexes in a join,
consider specifying forceSelectOrder, as shown in the following example.
2-7
Development III in Microsoft Dynamics® AX 2012
where inventDim.inventDimId ==
inventTrans.inventDimId
&& inventDim.inventBatchId == 'Y'
{
}
}
ForceNestedLoops
This keyword forces the database server to use a nested-loop algorithm to process
a given SQL statement that contains a join. This means that a record from the
first table is fetched before trying to fetch any records from the second table.
Generally other join algorithms like hash-joins, merge-joins, and others are also
considered. This keyword is frequently combined with the forceSelectOrder
keyword.
Review the previous example with the tables InventTrans and InventDim. You
could risk that the database finds all InventTrans records by the index ItemIdx
and all the InventDim records by the BatchId. (If you hint the index DimIdIdx
this will be used for this search.) The two collections of records are hashed
together. For the database to find the inventTrans and then the inventDim for
each inventTrans, specify forceNestedLoops, as shown in the following
example.
}
}
2-8
Chapter 2: Working with Data
Cross Company
By default, select statements in X++ adhere to the DataAreaId structure of the
application. For example, if there are two company accounts, 001 and 002, a
select statement run in company 002 will only return records stamped with
dataAreaId equal to 002. The crossCompany keyword allows the statement to
fetch records regardless of the dataAreaId. The following example shows a
statement that will count all customer records from all company accounts.
You also have the option of adding a container variable of company identifiers
immediately after the crosscompany keyword (separated by a colon). The
container restricts the selected rows to those with a dataAreaId that match a value
in the container. The following example illustrates the use of the crossCompany
keyword.
CustTable custTable;
container conCompanies = [ '001', '002', 'dat' ];
;
while select crosscompany : conCompanies * from custTable
order by dataAreaId
{
//do something
}
Query
A query is an object-oriented interface to the SQL database. A query is composed
of objects from different classes.
Various objects are available to manipulate a query object from the AOT.
Use the queryRun object to execute the query and fetch data.
The query object is the definition master. It has its own properties and has one or
more related data sources.
The queryBuildFieldList object defines which fields to fetch from the database.
The default is a dynamic field list that is equal to a "select * from …". Each data
source has only one queryBuildFieldList object which contains information
about all selected fields. You can also specify aggregate functions like sum,
count, and avg with the field list object.
2-9
Development III in Microsoft Dynamics® AX 2012
The queryFilter object is used to filter the result set of an outer join. It filters the
data at a later stage than the queryBuildRange object and filters the parent table
based on the child table results.
The queryBuildDynaLink objects can only exist on the outer data source of a
query. The objects contain information about a relation to an external record.
When the query is run, this information is converted to additional entries in the
"where" section of the SQL statement. The function is used by forms when two
data sources are synchronized. The subordinate data source contains DynaLink(s)
to the master data source. The function is used even if the two data sources are
placed in two different forms, but are synchronized.
The queryBuildLink objects can only exist on inner data sources. The objects
specify the relation between the two tables in the join.
Query q;
QueryRun qr;
QueryBuildDatasource qbds1, qbds2;
QueryBuildRange qbr;
;
q = new Query();
qbds1 = q.addDataSource(tablenum(InventTable));
qbds2 = qbds1.addDataSource(tablenum(InventTrans));
qbds2.relations(TRUE); //this enforces a relationship
between this datasource and its parent. Relationships
defined in the Data Dictionary are used by default.
qbr = qbds1.addRange(fieldnum(InventTable, ItemId));
qbr.value(SysQuery::value("1706")); //SysQuery object
provides various static methods to assist in defining Query
criteria. The SysQuery::value() method should always be
used when defining a singular value for a range.
qr = new QueryRun(q);
while(qr.next())
{
//do something
}
You can build a query in the AOT using MorphX or as shown in the previous
topic, by dynamically creating the query by X++ code. Both approaches are used
in the standard application. One advantage of making the query dynamic is that it
is not public in the AOT and is protected against unintentional AOT changes.
Alternatively, an advantage of creating the query in the AOT is that it can be
reused in various places, saving lines of identical code, and making wide-
reaching query adjustment easier.
2-10
Chapter 2: Working with Data
Forms and reports use queries for fetching data. These queries are not public
below the Query-node in the AOT. Forms generate the queries dynamically from
the setting of the data source, whereas Reports have an integrated query-node. In
both cases, you can edit the query for modification.
There are alternative ways of specifying existing objects in a query. Best practice
is to use the intrinsic functions which use table and field ID as arguments
(tableNum and fieldNum).
2-11
Development III in Microsoft Dynamics® AX 2012
As part of a larger modification, you need to develop code which queries the
database for specific records. Use AOT queries, "while select" statements, and
query objects to develop the most efficient data fetching code.
Challenge Yourself!
Make the following query "Count the number of customers from all companies,
limited to a specific currency." For this example, make USD the limiting value.
1. Create a job which makes the above select using "while select". The
currency to use can be defined in the code.
2. Create a new query in the AOT. Prompt the user for the currency to
use when the query is run.
3. Create a job which executes the query defined in step 2.
4. Create a new job which builds the same query dynamically using the
query classes. Prompt the user for the currency to use when the query
is run.
5. Verify that the three implementations return the same data.
Step by Step
2-12
Chapter 2: Working with Data
FIGURE 2.1
3. The following shows the job that executes query from step 2.
if (queryRun.prompt())
{
while (queryRun.next())
{
recordsFound++;
}
}
info(strFmt("Customers found: %1", recordsFound));
}
4. The following shows the job that dynamically builds the query
queryFilter =
query.addQueryFilter(queryBuildDataSource,
2-13
Development III in Microsoft Dynamics® AX 2012
identifierStr(Currency));
queryRun = new QueryRun(query);
if (queryRun.prompt())
{
while (queryRun.next())
{
recordsFound++;
}
}
2-14
Chapter 2: Working with Data
Your development manager has asked that you re-write all your queries using
Query objects instead of "while select" statements.
Make a job that dynamically builds a query, using query objects, with the same
structure as the following statement.
inventTrans inventTrans;
inventDim inventDim;
Enable the user to enter the item id when the query is run.
Step by Step
2-15
Development III in Microsoft Dynamics® AX 2012
inventDim inventDim;
queryBuildDataSource =
query.addDataSource(tableNum(InventTrans));
queryBuildDataSource.addSelectionField(fieldNum(InventTrans
,Qty),SelectionField::Sum);
queryBuildRange =
queryBuildDataSource.addRange(fieldNum(InventTrans,
ItemId));
queryBuildDataSource =
queryBuildDataSource.addDataSource(tableNum(InventDim));
queryBuildDataSource.addGroupByField(fieldNum(InventDim,
InventBatchId));
queryBuildDataSource.relations(true);
if (queryRun.prompt())
{
while (queryRun.next())
{
inventTrans =
queryRun.get(tableNum(InventTrans));
inventDim = queryRun.get(tableNum(inventDim));
}
}
}
2-16
Chapter 2: Working with Data
Caching
The previous sections have discussed how to optimize communication with the
database by structuring SQL statements that make the database access data in the
correct way. But the most optimal strategy is to have no communication with the
database or to minimize the number of communications with the database server.
This section covers data caching done by Microsoft Dynamics AX AOS and/or
client. Microsoft Dynamics AX supports the following kinds of caching:
• Read-ahead caching
• Single-record caching
• Entire table caching
• Record view caching
• Display method caching
• Global object cache
• Date effective join cache
• Unique index cache extension
• Cache lookup for table groups
The remainder of this section explains the first five above mentioned data
catching.
Read-ahead Caching
The SQL interface in Microsoft Dynamics AX provides read-ahead caching and
uses a buffer to pre-fetch rows. The read-ahead adapts to the queries executed.
As an example, a query to show rows in a form pre-fetches in chunks that
correspond to the number of rows currently visible in the form, whereas a multi-
row select with the FirstOnly keyword will not pre-fetch.
Single-Record Caching
Microsoft Dynamics AX offers record caching. Rows selected through a cache-
key are cached and successive look-ups specifying the cache-key are then served
from the record cache. The record-cache holds the rows that match frequently
issued single-row queries.
The cache-key is the Primary Key on the cached table. If a Primary Key does not
exist, the first unique index is used.
2-17
Development III in Microsoft Dynamics® AX 2012
Because only single-row queries go into the cache, the cache is small with a
default value of 100 rows on the client and 2000 rows on the AOS.
CacheLookup Result
None No data is cached or retrieved from cache for this table.
No caching should be used for tables that are heavily
updated or where it is unacceptable to read outdated
data.
NotInTTS All successful caching key selects are cached.
When in TTS (after ttsbegin), the record is read once
from the database and subsequently from cache. The
record is select-locked when read in TTS which ensures
that the record cached is not updated as long as the TTS
is active.
A typical example of its use is the CustTable in the
standard application. It is acceptable to read outdated
data from cache outside TTS, but when data is used for
validation or creating references it is ensured that it is
real-time data.
Found All successful caching key selects are cached. All
caching key selects are returned from cache if the record
exists there. A select forupdate in TTS forces reading
from the database and replaces the record in the cache.
This is typically used for static tables like ZipCodes
where the normal case is that the record exists.
FoundAndEmpty All selects on caching keys are cached, even selects not
returning data.
All caching key selects are returned from caching if the
record exists there or the record is marked as non-
existing in the cache.
An example of this kind of caching is found in the
InventTxt table in the standard application. For this
table it is a normal case that no record exists. Therefore,
in this case it is of great value to register the non-
existing keys.
• The caching only works when selecting exactly one record, with a
distinct where on the primary key.
• The cache will not support fetch of multiple records in a query or a
"while select".
• The cache does not support data retrieved by joining more than one
table.
2-18
Chapter 2: Working with Data
The cache on the client is not updated with changes done from other clients. This
means that one client can cache and use invalid information.
The single record caching is selected by the CacheLookup property setting on the
table.
You can use the AOT find tool to list some examples of this setting.
An individual cache is maintained for each data area in Microsoft Dynamics AX.
The AOS holds the cache. This is shared between all clients connected to the
AOS. The client supplements this AOS caching with a FoundAndEmpty single
record caching if possible.
The entire table caching is not used when making a join with other tables which
is not entire table cached.
2-19
Development III in Microsoft Dynamics® AX 2012
The entire table caching is selected by the CacheLookup property setting on the
table. The individual users can disable the caching clicking Tools > Options >
Preload. This form also provides an overview of which tables are configured by
using entire table caching.
2-20
Chapter 2: Working with Data
SalesLine salesLine;
RecordViewCache salesLineCache;
;
select noFetch salesLine
where salesLine.SalesId =="100";
The cache is used when a select is from the same table, not a join and at least
matches the where clause that the cache was instantiated with.
If the table is not set up for Optimistic concurrency then using the forupdate
option on the select within TTS results in database locks on all the rows that are
put into the cache. A subsequent select forupdate within the same TTS is
selected from the cache and not from the database as the row is already locked.
Updates and deletes that match the instantiating where clause simultaneously
maintain the database and the cache. Updates and deletes are not in the above
category do not affect the result-set.
The reread method retrieves data from the database and updates the copy data.
The cache can be instantiated using a select without a where clause. This
effectively creates a copy of the table in memory and functions like a personal
table copy.
The cache is useful for caching tables that are not of static nature, or that contain
so many records that the other caching methods would be impractical.
Do not use field-lists unless you are certain that the omitted fields will never be
used. There is no warning or error when selecting fields from the cache that were
not included in the select that instantiated the result set.
Inserts normally do not affect the cache even when the record would match the
where clause defining the result-set.
When you use this form of caching, consider the memory consumption, as there
is no internal limitation to the memory allocated for a single table. Different
application behavior, with regard to exploiting caching when it is running on
server versus client, could be an option.
2-21
Development III in Microsoft Dynamics® AX 2012
One example of using record view caching is the class InventMovement. This
holds information about inventory transactions related to one SalesLine,
PurchLine and more. The cache is defined as a RecordViewCache object in
\Classes\InventMovement\classDeclaration.
void viewCacheInventTransOrigin()
{
if (viewCacheInventTrans)
return;
viewCacheInventTrans = null;
viewCacheInventTrans =
InventTrans::viewCacheInventTransOrigin(this.InventTransOri
ginId(),true);
Only methods that are explicitly added to the cache are affected by the new
caching mechanism. To sign up a method for caching, the method
cacheAddMethod on the form data source should be called after super() in the
init method of the data source.
2-22
Chapter 2: Working with Data
The call to cacheAddMethod also determines how frequently the cached display
method value is updated. The value is filled in upon fetching data from the back-
end, and it is refreshed when reread is called on the data source. Additionally, by
default, the display method values are also updated when the record is written to
the database. But that can be changed using the _updateOnWrite parameter in the
cacheAddMethod.
Only display methods that are of display type can be cached. Edit methods
cannot be cached. Only display methods placed on the table, can be cached.
purchLine_ds.cacheAddMethod(tablemethodstr(PurchLine,
receivedInTotal));
purchLine_ds.cacheAddMethod(tablemethodstr(PurchLine,
invoicedInTotal));
purchLine_ds.cacheAddMethod(tablemethodstr(PurchLine,
itemName));
purchLine_ds.cacheAddMethod(tablemethodstr(PurchLine,
displayBudgetCheckResult));
purchLine_ds.cacheAddMethod(tablemethodstr(PurchLine,
calcPendingQtyPurchDisplay));
Display methods not based on cached data, are candidates for display method
caching.
Locking
So far you have seen that the database server can access data faster when the
number of requests to the database is minimized, by using efficient queries and
caching.
Another issue is if multiple users want to lock the same record at the same time.
Only one user can lock the record, and the rest wait in a queue until the record is
released. This has a substantial effect on the time used to perform a function.
Locks are typically done in transactions where more than one record is locked at
a time. The following example illustrates two different processes which lock
different items:
Process 1 Process 2
Item C - Locked -
Item A - Locked -
Item B - Locked Item G - Locked
Item G - Waiting for lock from Item A - Waiting for lock from
Process 2 Process 1
2-23
Development III in Microsoft Dynamics® AX 2012
This situation is called a dead lock; one of the transactions must be aborted to
resolve the issue.
Locking is the biggest issue with scalability, which means that many users are
working with the same functions and data.
NOTE: Only a few tables in the standard application do not use Optimistic
Concurrency Control.
Locking is held for the time it takes to finish the process. If the process has poor
performance because of missing optimization of AOS and database
communication, the locking is a bigger issue and needs to be addressed first.
2-24
Chapter 2: Working with Data
Some users have been reporting performance issues and deadlock messages,
when using some of your modifications. You have been given the task of
optimizing your code, to reduce any locking.
Challenge Yourself!
Modify the following job to reduce locking:
InventTable inventTable;
VendTable vendTable;
;
ttsbegin;
Step by Step
Solution 1: A new InventTable table variable is declared. Updates are done
against this variable, so that you do not need to lock the whole InventTable
forupdate.
ttsbegin;
// solution #1
2-25
Development III in Microsoft Dynamics® AX 2012
Solution 2: If and find on VendTable are replaced with exist join on VendTable.
ttsbegin;
// solution #2
while select forupdate inventTable
exists join vendTable
where vendTable.AccountNum ==
inventTable.PrimaryVendorId &&
vendTable.Blocked == CustVendorBlocked::All
{
inventTable.PrimaryVendorId ="";
inventTable.update();
}
ttscommit;
}
2-26
Chapter 2: Working with Data
Temporary Tables
A temporary table is defined in the AOT as a normal table, but with the
TableType property set to either InMemory or TempDB. Data is not persisted in
a temporary table, and only remains while the table is in scope. If TableType is
set to InMemory, the data is stored in memory or in a temporary file if memory is
full. TempDb means it is stored in a table in SQL, and can be joined to regular
tables at the database tier.
A typical example is a class which initializes a temporary table and inserts some
records, which should be shown in a form. The class has a variable _tmpTable
which holds the data and the form has a data source tmpTable_DS which should
show the same data. This is solved by using the method
tmptable_DS.setTmpData(_tmpTable). This disregards the individual file
allocated to tmptable_DS. Now, both _tmpTable and tmptable_DS will access
the same pool of data, as if they were normal table variables. The allocated file
now shared between _tmpTable and tmptable_DS is deleted once both variables
have gone out of scope.
tmpCustTable.setTmp();
2-27
Development III in Microsoft Dynamics® AX 2012
Challenge Yourself!
Make a job which works with two individual instances of temporary data (for
example, TmpSum).
Step by Step
If you declare two temporary table variables A and B, inserts, updates, and
deletes on table A do not have impact on B. This only happens if the method
variableB.setTempData(variableA) is set before updating.
2-28
Chapter 2: Working with Data
countRecords(a,"3","a");
countRecords(b,"3","b");
2-29
Development III in Microsoft Dynamics® AX 2012
2-30
Chapter 2: Working with Data
InitFrom
Often, a record has default values for fields initialized with data copied from
another table. For example, when a sales order is created and a customer account
is specified, fields including Invoice Account, Delivery Address, Tax Group, and
Currency are copied from the CustTable record to the SalesTable. This is done
in the SalesTable.InitFromCustTable() method which contains a list of field
assignments between the two tables. This is a common technique of initializing
fields in one table from another table, and is always the first place to analyze.
For example, to add the new field "Fragile" to the BOM table that defaults from
another new field "Fragile" in the InventTable, consider adding the following
line to BOM.InitFromInventTable():
this.fragile = inventTable.fragile;
Parm Tables
When sales orders, purchase orders, or production orders are updated to a new
status, Microsoft Dynamics AX uses Parm tables to store what will be updated.
This allows the user to view and modify what will be updated without affecting
the original order. It also enables the update to be processed by the batch system.
The user can view and edit the Parm tables in the SalesEditLines or
PurchEditLines forms. In the previous figure, the SalesParmLine table appears
on the Lines tab, and the SalesParmTable appears on the SalesOrders tab.
2-31
Development III in Microsoft Dynamics® AX 2012
To use the Parm tables, a ParmId is created. This is a sequential number taken
from the System ID number sequence set up on the Company Info table. The
ParmId ensures the tables are linked correctly using a unique identifier for each
update - one order can be updated multiple times.
The table records are then created using data from the appropriate tables. For
example, PurchParmTable is initialized using data from PurchTable, and
PurchParmLine is initialized using data from PurchLine.
Date Effectiveness
Date effectiveness allows the application to associate valid from and to dates
with the application artifacts. For example, an agreement can be valid between a
range of dates. Similarly, interest rates are assigned based on start date and end
date association.
2-32
Chapter 2: Working with Data
10. Verify the ValidFrom and ValidTo date fields are created.
11. Right-click the Indexes node and select New Index.
12. Rename the index to ItemDateIdx.
13. Drag the date fields and the ItemId field to the new index node.
14. Right-click the new index and select Properties.
15. Set AllowDuplicates to No.
16. Set AlternateKey and ValidTimeStateKey to "Yes".
17. Set ValidTimeStateMode to NoGap.
18. Save the table.
19. If the compiler reports an error, right-click the table and select
Compile. If it still reports an error, click reset on the compiler form
and compile the table again.
2-33
Development III in Microsoft Dynamics® AX 2012
1. Open the AOT, expand the Data Dictionary > Views node.
2. Find the InventTableExpanded node
3. In the Fields node, find the ProductName field, and open the
properties sheet.
4. Note the property ViewMethod is set to productName. This refers
to a static method on the view.
5. Expand the Methods node on the InventTableExpanded view.
6. Open the productName method. The code in the method is as
follows:
str translatedNameField =
SysComputedColumn::returnField(viewName,
identifierstr(EcoResProductTranslations),
fieldstr(EcoResProductTranslations, ProductName));
2-34
Chapter 2: Working with Data
str productDisplayNumberField =
SysComputedColumn::returnField(viewName,
identifierstr(EcoResProduct), fieldstr(EcoResProduct,
DisplayProductNumber));
return SysComputedColumn::if(
SysComputedColumn::isNullExpression(translatedNameField),
productDisplayNumberField,
translatedNameField);
7. The three variables that are declared are used to hold the physical
names of the view and the fields that are to be used.
8. The SysComputedColumn::if() method then converts three
expressions in to the T-SQL expression. By placing the cursor on the
if() method and pressing F12, you can see the method.
9. To view the final T-SQL code that is generated, you need to turn on
SQL-Tracing. Go to Tools > Options > SQL.
10. Check SQL Trace, and then check Multiple SQL Statements >
infolog.
11. Close the user options form.
12. In the AOT, right-click on view InventTableExpanded and select
Synchronize.
2-35
Development III in Microsoft Dynamics® AX 2012
14. Note that one of the fields has the following : ,(CAST ((CASE
WHEN T5.PRODUCTNAME IS NULL THEN
T2.DISPLAYPRODUCTNUMBER ELSE T5.PRODUCTNAME
END) AS NVARCHAR(60))) AS PRODUCTNAME,
Data Integration
External data comes in many different formats. This section describes
mechanisms to read and write data in four different, but commonly used, formats.
With all external interaction in X++, code access permission must be asserted.
IO classes
The standard classes that extend the IO class are useful for importing and
exporting data to and from a simple text file format. Most commonly, these are
TXT or CSV files.
2-36
Chapter 2: Working with Data
The CommaIO and Comma7IO classes are used to import and export comma
separated data.
The TextIO and AsciiIO classes are used to import and export text-based data.
if (commaIO)
{
while (commaIO.status() == IO_Status::OK)
{
readCon = commaIO.read();
The commaIO object is looped over, using a "while" statement, which continues
until the IO_status is no longer OK, which indicates the end of the file.
The CommaIO.read() method returns data from a single record (or row) from the
file, in the form of a container, where each container element is a single field
value. In this example, there are at least three fields per row.
2-37
Development III in Microsoft Dynamics® AX 2012
FileIO
FileIO is commonly used to export data to a TXT file. There are three different
methods to write text to an FileIO object:
The following is sample code which writes a simple string to a text file:
if (fileIO)
{
outputText = "text that will go into the text file.";
Creating XML
Extensible Markup Language (XML) is a text-based standard for representing
data and is commonly used for data exchange and business-to-business
communication.
The structure of XML files is similar to HTML in that it uses tags to define
sections of the file. Each tag defines either single or groups of data elements, and
can also have attributes (for example, a date or reference ID) attached to the tag.
<note>
<appl>Microsoft Dynamics AX</appl>
<version>2012</version>
<body>This is a simple XML file</body>
</note>
2-38
Chapter 2: Working with Data
Since it is a simple text file, it is possible to create an XML file using Microsoft
Dynamics AX AsciiIO class, and to ensure that all the tags are correctly
formatted and positioned. Be careful though as the tags are case-sensitive.
The following example creates an XML file containing employee names and
addresses:
permission= new
FileIoPermission('c:\\Empl_Address_List.xml','w');
permission.assert();
xmlDoc = XMLDocument::newBlank();
2-39
Development III in Microsoft Dynamics® AX 2012
Reading XML
XML files can be read in an easy way or a hard way. The hard way is to read the
file line by line and determine the correct tag each time.
The easy way is to let the microsoft.xmldom COM object do the work. The
following example reads an XML file containing customer account number and
transaction details.
permission= new
FileIoPermission("C:\\CustTrans.xml",'r');
permission.assert();
xmlError = doc.parseError();
rootNode = doc.documentElement();
2-40
Chapter 2: Working with Data
info(transNode.selectSingleNode("TransText").text()
+ ' ' +
transNode.selectSingleNode("TransAmount").text());
}
}
ODBC
Sometimes, data that you need to access from Microsoft Dynamics AX may be
stored in an alternative database. This is common when fetching data from an
external application, either on a regular basis, or during data conversion.
Once a DSN is available in Windows, code can be written to leverage its access
to the external database:
if (odbcConnection)
{
sql = "SELECT * FROM MYTABLE WHERE FIELD =" +
criteria + " ORDER BY FIELD1,FIELD2 ASC"
//assert permission for sql string
perm = new SqlStatementExecutePermission(sql);
2-41
Development III in Microsoft Dynamics® AX 2012
perm.assert();
//Prepare statement
statement = odbcConnection.createStatement();
resultSet = statement.executeQuery(sql);
//Running statement
while (resultSet.next())
{
//It is not possible to get field 3 and then 1.
Always get fields in numerical order: 1,2,3,4,5,6
print resultSet.getString(1);
print resultSet.getString(3);
}
The previous code uses a variable called "criteria", and includes it in the SQL
string that is executed against the example external database. It then prints the
string values from columns 1 and 3 in the result set. Columns in the result set can
only be called in numerical order. If you need to print column 3 value first, first
fetch the column 1 value and store it in a local variable, then fetch and print the
column 3 value, then print the local variable.
More methods are available on the ResultSet object, to fetch other types from the
record. These include, getReal(), getDate(), getInt().
Microsoft Excel
Microsoft Dynamics AX includes built-in administration tools for importing and
exporting table data using the Microsoft® Excel® spreadsheet format. However,
sometimes it may be necessary to perform similar Microsoft Excel data import
and export functions from a custom function. Often, this can be done using
Comma Separated Value (CSV) files and the CommaIO class, but sometimes
using XLS or XLSX formatted files is a requirement. To make this possible,
there are various classes to perform Microsoft Excel integration. These all start
with the prefix SysExcel and can be found in the Class branch of the AOT.
2-42
Chapter 2: Working with Data
The following is an example of some X++ code that integrates with an XLS file,
and pulls a value from a specific cell.
SysExcelApplication ExcelApp;
Str cellValue;
;
2-43
Development III in Microsoft Dynamics® AX 2012
Your company needs to import seasonal sales prices every quarter. The sales
prices are maintained in a spreadsheet that need to be imported in to AX once
they are complete. You have been asked to write a program to import the sales
prices.
Challenge Yourself!
To complete this lab, you need to create an external data file. Create a new file
with the following structure:
Save the file as a CSV file named SalesPrice.csv in the root C drive directory.
Write a job in X++ that will import the data from the SalesPrice.csv file that you
just created. The job will need to set some values for other fields - the price
should be for All customer accounts, use the current companies default currency,
be applicable for all quantities and for any inventory dimension.
Step by Step
2-44
Chapter 2: Working with Data
priceDiscTable.relation =
PriceType::PriceSales;
priceDiscTable.AccountCode =
TableGroupAll::All;
priceDiscTable.Currency =
Ledger::accountingCurrency();
priceDiscTable.ItemRelation = column1;
priceDiscTable.FromDate =
str2date(column2,123);
priceDiscTable.Amount = str2num(column3);
priceDiscTable.InventDimId =
InventDim::findOrCreateBlank().inventDimId;
priceDiscTable.QuantityAmountFrom = 1;
pricedisctable.insert();
}
}
}
2-45
Development III in Microsoft Dynamics® AX 2012
Summary
Using the most efficient approaches to database communication will result in an
application that runs with the best performance possible. Using the tools
described in this chapter, modifications made to the standard application can
operate at the peak performance expected by the users.
2-46
Chapter 2: Working with Data
2. You have the following X++ code, with two QueryBuildDatasource objects.
Modify the second line and add a third line, so that these two datasources are
joined:
qbds1 = query.addDatasource(tableNum(CustTable));
qbds2 = query.addDatasource(tableNum(CustTrans));
2-47
Development III in Microsoft Dynamics® AX 2012
3. Which of the following are true for avoiding locking: (Select all that apply)
( ) Keep the transactions as short in time as possible.
( ) Try to lock central resources.
( ) Try to lock central resources in the same order.
( ) Try to implement dialogs with the user inside transactions.
2-48
Chapter 2: Working with Data
8. Identify the problem with the following block of ODBC code, where the
resultSet has already been successfully fetched from the external database:
while (resultSet.next())
{
print resultSet.getString(2);
print resultSet.getString(5);
print resultSet.getReal(4);
print resultSet.getInt(7);
}
2-49
Development III in Microsoft Dynamics® AX 2012
1.
2.
3.
2-50
Chapter 2: Working with Data
Solutions
Test Your Knowledge
1. Match the select statement clauses, with their correct purpose:
2. You have the following X++ code, with two QueryBuildDatasource objects.
Modify the second line and add a third line, so that these two datasources are
joined:
qbds1 = query.addDatasource(tableNum(CustTable));
qbds2 = query.addDatasource(tableNum(CustTrans));
MODEL ANSWER:
qbds2 = qbds1.addDatasource(tableNum(CustTrans));
qbds2.relations(TRUE);
3. Which of the following are true for avoiding locking: (Select all that apply)
(√) Keep the transactions as short in time as possible.
( ) Try to lock central resources.
(√) Try to lock central resources in the same order.
( ) Try to implement dialogs with the user inside transactions.
2-51
Development III in Microsoft Dynamics® AX 2012
MODEL ANSWER:
The data in the temporary table are stored in a file on the AOS because the
first record is inserted from a method running on the AOS.
MODEL ANSWER:
SalesTable.InitFromCustTable()
MODEL ANSWER:
Microsoft Dynamics AX uses Parm tables to store data that will be updated
on sales orders, purchase orders. This allows the user to view and modify
what will be updated without affecting the original order. It also enables the
update to be processed by the batch system.
8. Identify the problem with the following block of ODBC code, where the
resultSet has already been successfully fetched from the external database:
while (resultSet.next())
{
print resultSet.getString(2);
print resultSet.getString(5);
print resultSet.getReal(4);
print resultSet.getInt(7);
}
MODEL ANSWER:
All columns in the result set need to be called in numerical order. Column 4
cannot be called after column 5.
2-52