0% found this document useful (0 votes)
32 views52 pages

Chapter 2: Working With Data: Objectives

Uploaded by

Romeo Dibakoane
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
32 views52 pages

Chapter 2: Working With Data: Objectives

Uploaded by

Romeo Dibakoane
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 52

Chapter 2: Working with Data

CHAPTER 2: WORKING WITH DATA


Objectives
The objectives are:

• Program optimal database access using a "while select" statement.


• Program optimal database access using queries.
• Describe the caching mechanisms in Microsoft Dynamics® AX.
• Prevent and resolve database locking.
• Use temporary tables in classes, forms, and reports.
• List the reasons for using InitFrom<tablename> methods.
• Use ParmId and ParmTables.
• Discuss date effectiveness and describe how to build date effective
forms.
• Add a computed column to a view.
• Employ the various techniques available for integrating external data
with Microsoft Dynamics AX.

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.

The complete syntax for the select statement is as follows.

[while] select [reverse] [firstfast]

[firstonly] [firstOnly10] [firstOnly100] [firstOnly1000]


[forupdate] [nofetch] [crosscompany]
[forcelitterals | forceplaceholders] [forcenestedloop]
[forceselectorder]

[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> ]
]

<fieldlist> ::= <field> | <fieldlist> , <field>


<field> ::= fieldname | <function>(<field>)

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

The following illustrates using a field list.

while select amountMST from ledgerTrans


{
amountMST += ledgerTrans.amountMST;
}

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.

Aggregation clause Description


sum Returns the sum of the values in a field.
avg Returns the average of the values in a field.
maxof Returns the maximum of the values in a field.
minof Returns the minimum of the values in a field.
count Returns the number of records that satisfy the statement.

The following illustrates using aggregate functions:

select sum(qty) from inventTrans;

qty = inventTrans.amountMST;

select count(recId) from inventTrans;

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

while select InventTable


{
while select InventTrans
where InventTrans.itemId == inventTable.itemId
{
qty += inventTrans.qty;
}
}

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).

while select recId from inventTable


join qty from inventTrans
where inventTrans.itemId == inventTable.itemId
{
qty += inventTrans.qty;
}

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.

The following X++ statement is an example of how to use this keyword.

static void DemoForceLiterals()


{
InventTrans inventTrans;
;
while select forceliterals inventTrans
order by itemId
where inventTrans.DatePhysical >= mkdate(21,12,2012)
{

}
}

It is not possible to determine whether an index on itemId or an index on


DatePhysical should be used without considering the actual value of 21\12\2012.
Therefore, the keyword should be used as shown in the previous code sample.

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.

The following X++ statement is an example of when to use this keyword.

static void DemoForcePlaceholders()


{
SalesTable salesTable;
SalesLine salesLine;
;
while select forcePlaceholders salesLine
join salesTable
where salesTable.SalesId == salesLine.SalesId
&& salesTable.SalesId == '10'
{

}
}

2-5
Development III in Microsoft Dynamics® AX 2012

In the previous code example, the database server automatically chooses to


search the SalesTable using an index on salesId. The database server uses the fact
that the salesId column is a unique field and does not need the actual search value
to compute the optimal access plan.

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

When Microsoft Dynamics AX fetches data from the database, it transfers a


package of records in each fetch. This is called read-ahead caching and is
performed to minimize calls to the database. If it is known that only one record
will be fetched, you can disable the read-ahead caching with this qualifier.

NOTE: It is best practice to use this in the "find" methods on the tables.

The following example illustrates the use of FirstOnly.

static CustTable findCustTable(CustAccount _custAccount)


{
CustTable custTable;
;
select firstonly custTable
where custTable.AccountNum == _custAccount;

return custTable;
}

NOTE: It is important that "find" methods are designed to be executed on the


tier where the method is called. This is the default behavior on static table
methods. If the method is bound on the server it will always require a round-trip
to the AOS even if the data was cached on the client.

Access Plan Repair


The following keywords are categorized as access plan repair keywords and
should not be used unless required to fix specific performance problems.

Index or Index Hint Keywords

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.

Microsoft Dynamics AX automatically removes index hints referring to a


disabled index.

By default index hints are disabled on the AOS.

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.

static void DemoForceSelectOrder()


{
InventTrans inventTrans;
InventDim inventDim;
;
while select inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim
where inventDim.inventDimId ==
inventTrans.inventDimId
&& inventDim.inventBatchId == 'Y'
{

}
}

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.

static void DemoForceSelectOrder()


{
InventTrans inventTrans;
InventDim inventDim;
;
while select forceSelectOrder inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim

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.

static void DemoForceSelectOrder()


{
InventTrans inventTrans;
InventDim inventDim;
;
while select forceSelectOrder forceNestedLoop
inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim
where inventDim.inventDimId ==
inventTrans.inventDimId
&& inventDim.inventBatchId == 'Y'
{

}
}

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.

select crossCompany count(recId) from custTable;


countCustTable = custTable.recId;

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 queryBuildDataSource defines access to a single table in the query. If one


data source is added below another data source, they form a join between the two
tables.

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 queryBuildRange object contains a limitation of the query on a single field.

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.

The following is a sample of code using some of these QueryBuild objects, to


build a query and loop through it:

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

Lab 2.1 - Fetching Data


Scenario

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.

Perform the following actions:

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

1. The following shows query made in a job using "while select".

static void SelectCustomerJob(Args _args)


{
CustTable custTable;
Counter recordsFound;
;
while select crosscompany custTable
where custTable.Currency =="USD"
{
recordsFound++;
}
info(strFmt("Customers found: %1", recordsFound));
}

2-12
Chapter 2: Working with Data

2. The following shows a query built in AOT:

FIGURE 2.1

3. The following shows the job that executes query from step 2.

static void SelectCustomerRunQuery(Args _args)


{
QueryRun queryRun;
Counter recordsFound;
;
queryRun = new QueryRun(queryStr(CustTableByCurrency));

if (queryRun.prompt())
{
while (queryRun.next())
{
recordsFound++;
}
}
info(strFmt("Customers found: %1", recordsFound));
}

4. The following shows the job that dynamically builds the query

static void SelectCustomerQuery(Args _args)


{
Query query;
QueryBuildDataSource queryBuildDatasource;
QueryFilter queryFilter;
QueryRun queryRun;
Counter recordsFound;
;

query = new Query();


query.allowCrossCompany(TRUE);
queryBuildDataSource = query.addDataSource(tableNum(
CustTable));

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++;
}
}

info(strFmt("Customers found: %1", recordsFound));


}

2-14
Chapter 2: Working with Data

Lab 2.2 - Converting Queries


Scenario

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;

while select sum(qty) from inventTrans


where inventTrans.ItemId =="1706"
join inventDim
group by inventBatchId
where inventDim.InventDimId ==
inventTrans.InventDimId
{

info(strFmt("Batch %1, qty %2",


inventDim.inventBatchId, inventTrans.qty));
}

Enable the user to enter the item id when the query is run.

Perform the following actions:

1. Create a new job.


2. Use the Query, QueryBuildDataSource classes, and more, to create a
query dynamically.
3. Create another job that uses "while select" as shown.
4. Verify that the two implementations return the same data, using item
number 1706.

Step by Step

1. In the AOT, create a new job.


2. Copy the code above in to the job.
3. Press F5 to run the code
4. Note the result.
5. Create another new job.
6. Add the following code.

2-15
Development III in Microsoft Dynamics® AX 2012

static void InventTransBuildQuery(Args _args)


{
Query query;
QueryBuildDataSource queryBuildDataSource;
QueryBuildRange queryBuildRange;
QueryRun queryRun;
Qty total;
InventTrans inventTrans;

inventDim inventDim;

query = new Query();

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);

queryRun = new QueryRun(query);

if (queryRun.prompt())
{
while (queryRun.next())
{
inventTrans =
queryRun.get(tableNum(InventTrans));
inventDim = queryRun.get(tableNum(inventDim));

info(strFmt("Batch %1, qty %2",


inventDim.inventBatchId, inventTrans.qty));

}
}
}

7. Press F5 to run the job.


8. In the prompt, enter 1706 in the Item number field
9. Note the result.

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.

The concept of caching is to remember the information retrieved from the


database and use this memory when the same data is needed at a later time.
However, this strategy has one large drawback, if the remembered information is
no longer valid, this could compromise the consistency of the database, as the
updates will be made based on invalid data.

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.

Four single record cache settings are used with tables:

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.

Important features about single-record caching are as follows:

• 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.

Entire Table Caching


The entire table caching loads all the records in the table the first time one record
is fetched. There is no limitation on the number of records cached.

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.

FIGURE 2.2 PRELOAD TABLES

Record View Caching


Use record view caching to cache the records from a select statement (a result
set). Result set caching is made available through the RecordViewCache class.
The cache is instantiated using a select with the nofetch option. If the
instantiating select is a join, a temporary table or is instantiated without the
nofetch option, the cache is not instantiated and an error is reported. The select
clause of the select defines the result set and only "==" predicates are accepted.

2-20
Chapter 2: Working with Data

The following example illustrates the instantiating of a record view cache:

SalesLine salesLine;
RecordViewCache salesLineCache;
;
select noFetch salesLine
where salesLine.SalesId =="100";

salesLineCache = new recordViewCache(salesLine);

The cache is not shared between Microsoft Dynamics AX clients, and is


deactivated as soon as the cache object goes out of scope or is destroyed.

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.

Because of concurrency issues, the forupdate keyword on the instantiating X++


select should only be used when all the records in the result set will be updated.
Otherwise, it is a better strategy to select forupdate the records that are updated
or use Optimistic concurrency on the tables.

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.

The cache is initialized by the method


\Data\Dictionary\Tables\InventTrans\Methods\viewCacheInventTransOrigin

public static RecordViewCache


viewCacheInventTransOrigin(InventTransOriginId
_inventTransOriginId, boolean _forupdate = false)
{
InventTrans inventTrans;
;
inventTrans.selectForUpdate(_forupdate);

select nofetch inventTrans where


inventTrans.InventTransOrigin == _inventTransOriginId;

return new RecordViewCache(inventTrans);


}

According to the cross reference, this method is called from


\Classes\InventMovement\viewCacheInventTransId.

void viewCacheInventTransOrigin()
{
if (viewCacheInventTrans)
return;

viewCacheInventTrans = null;
viewCacheInventTrans =
InventTrans::viewCacheInventTransOrigin(this.InventTransOri
ginId(),true);

Display Method Caching


Caching display methods improves the performance of display functions if they
are calculated on the AOS and improves performance when records are
transferred from the server to the client.

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.

One example of the initialization is found in \Forms\PurchTable\Data


Sources\PurchLine\Methods\init.

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.

To reduce locking, consider using Optimistic Concurrency Control (OCC). When


the risk of overlapping transactions modifying the same data is small, optimistic
locking can increase concurrency and thus transaction throughput. Enable OCC
for a table by setting the property OccEnabled to Yes.

NOTE: Only a few tables in the standard application do not use Optimistic
Concurrency Control.

Strategies to use to avoid or reduce the locking problem are as follows:

• Keep the transactions as short in time as possible. Do not


compromise the integrity of the data.
• Try to avoid locking central resources.
• Try to lock central resources in the same order. This avoids the dead
lock situation discussed previously. This serializes the process,
which means that only one user can perform this at a time.
• Never implement dialog with the user inside a transaction.

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

Lab 2.3 - Reducing Locking


Scenario

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;

while select forupdate inventTable


{
if
(VendTable::find(inventTable.PrimaryVendorId).Blocked
== CustVendorBlocked::All)
{
inventTable.PrimaryVendorId ="";
inventTable.update();
}
}
ttscommit;

Perform the following actions:

1. Create a new job with the above contents.


2. Try to reduce the locking.
3. Try to redesign the code to obtain a better performance.

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.

static void Locking_Solution_1(Args _args)


{
InventTable inventTable;
InventTable inventTableUpdate;
VendTable vendTable;
;

ttsbegin;

// solution #1

2-25
Development III in Microsoft Dynamics® AX 2012

while select inventTable


{
if
(VendTable::find(InventTable.PrimaryVendorId).Blocked ==
CustVendorBlocked::All)
{
inventTableUpdate =
InventTable::find(inventTable.ItemId, true);
inventTableUpdate.PrimaryVendorId ="";
inventTableUpdate.update();
}
}
ttscommit;
}

Solution 2: If and find on VendTable are replaced with exist join on VendTable.

static void Locking_Solution_2(Args _args)


{
InventTable inventTable;
VendTable 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.

Instead of making a dedicated definition of a temporary table in the AOT,


consider making a temporary instance of a database table (in other words, a non-
temporary table which is part of the SQL database). Do this using the .setTmp()
method before accessing the variable. Be careful with this option, as you can
activate other methods which act as if it is real database data and cause
subsequent updates in other tables. See the following example, which copies all
customers from Australia to a temporary table.

static void CopyPersistedTableToTemp(Args _args)


{
CustTable custTable;
CustTable tmpCustTable;
;

tmpCustTable.setTmp();

while select custTable


where custTable.CountryRegionId == "AU"
{
tmpCustTable.data(custTable.data());
tmpCustTable.doInsert();
}
}

2-27
Development III in Microsoft Dynamics® AX 2012

Lab 2.4 - Temporary Tables


Scenario

As part of a larger modification, you need to implement some temporary tables,


to achieve the required outcome.

Challenge Yourself!
Make a job which works with two individual instances of temporary data (for
example, TmpSum).

Perform the following steps:

1. Make a job with two variables A and B of type TmpSum.


2. Insert some records in variable A. Does this have an effect on the
contents of variable B?
3. Copy all records from A to B. Verify that the contents of the two
tables are equal.
4. Delete one record from A. Does this have an effect on the contents of
variable B?
5. Call the B.setTempData(A) method. Repeat step 2 and 4.

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.

1. Open the AOT create a new job.


2. Copy the following code in to the job.
3. Read through the code carefully, and try to understand what each
line is doing and what the result will be.
4. Press F5 to run the job. Verify the results are what you expected.

static void Temporary(Args _args)


{
TmpSum a;
TmpSum b;
Counter counter;

void countRecords(Tmpsum _tmptable, str _step, str


_name)
{
select count(RecId) from _tmptable;
info(strFmt("Step %1 - %2 records in %3", _step,
_tmptable.RecId, _name));
}
;
ttsbegin;

2-28
Chapter 2: Working with Data

// Insert records in 'a'


for (counter = 1; counter<= 1000; counter++)
{
a.Key = num2Str(counter,1,0,0,0);
a.Balance01 = counter;
a.insert();
}

// Verify that inserts in 'a' do not affect 'b'


countRecords(a,"2","a");
countRecords(b,"2","b");

// Copy all records from 'a' to 'b'


b.skipDataMethods(true);
insert_recordSet b (Key, Balance01)
select Key, Balance01 from a;

countRecords(a,"3","a");
countRecords(b,"3","b");

// Delete one record from 'a'


select forupdate a;
a.delete();

/* Verify that the contents of the two tables are not


equal,
delete from ¢a¢ do not effect b */
countRecords(a, "4","a");
countRecords(b, "4","b");

// Call setTmpData method


b.setTmpData(a);

// Insert 10 new records in ¢a¢


for (counter = 1001; counter<= 1010; counter++)
{
a.Key = num2Str(counter,1,0,0,0);
a.Balance01 = counter;
a.insert();
}

/* Verify that the contents of the two tabels are


equal,
inserts in 'a' effect 'b' */
countRecords(a,"5a","a");
countRecords(b,"5b","b");

// Delete one record from ¢a¢


select forupdate a;
a.delete();

2-29
Development III in Microsoft Dynamics® AX 2012

/* Verify that the contents of the two tabels are


equal,
delete from 'a' effect 'b' */
countRecords(a,"5b","a");
countRecords(b,"5b","b");
ttscommit;
}

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.

FIGURE 2.3 SALESEDITLINES FORM

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.

Note the use of InitFrom methods on most of these tables.

In the SalesFormLetter.CreateParmLine() method the SalesParmLine record


is set from SalesLine table by use of the SalesParmLine.initFromSalesLine()
method.

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.

The date effectiveness feature is a central framework that is supported in all


framework components. It helps developers write less code, to develop more
consistent code, and to create forms that manage viewing and editing of current,
past, and future records.

Scenario: Create a Date Effective Table


Isaac, the Developer, wants to build a table to hold descriptions of an item that
are only valid for a specified period of time. There must always be one and only
one description so no gaps or overlaps are allowed

Procedure: Create a Date Effective Table


Perform the following steps to create a table that is enabled for date effectiveness
and does not enable date gaps.

1. Open the AOT.


2. Create a project.
3. Create a new Table.
4. Right-click the table and select Properties.
5. Set the name property to ItemDescEffective.
6. Drag Extended Data Type ItemId to the Fields node on the new
table.
7. Click Yes to create the Foreign Key relation.
8. Drag the Extended Data Type Name to the Fields node on the new
table.
9. Enter Date in the ValidTimeStateFieldType property.

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.

Procedure: Demonstrate Date Gap


Perform the following steps to demonstrate the date effectiveness and make sure
the table does not enable date gaps.

1. Open the AOT.


2. Select the table that is created in the "Create a Date Effective Table"
procedure.
3. Right-click the table and select Open.
4. Press Ctrl-N to create a new record.
5. Select an item, enter an ItemName and enter a ValidFrom date and
ValidTo date.
6. Press Ctrl-N to create another new record.
7. Enter a second record for the same item but with a ValidFrom date
and ValidTo date starting some days after the ValidTo date of the
first record.
8. Click Yes and accept the adjustment to the effective dates.
9. Verify the system changed the first record and adjusts the ValidTo
date to fill the gap between the two records.
10. Press Ctrl+F4 to close the table browser.

Procedure: Demonstrate Date Overlap


To demonstrate the date effectiveness and to make sure that the table does not
enable date overlaps, follow these steps:

1. Open the AOT.


2. Select the table created in the previous procedure.
3. Right-click the table and select Open to open the table.
4. Press CTRL+N to create a third record for the same item as in the
previous procedure but with a ValidFrom date and ValidTo date
overlapping the second records time frame.

2-33
Development III in Microsoft Dynamics® AX 2012

5. Click Yes and accept the adjustment to the effective dates.


6. Verify the system changes the second record and adjusts the
ValidTo date to resolve the date overlap.
7. Press Ctrl+F4 to close the table browser.

Selecting Date Effective Data


When selecting data in X++, a keyword validTimeState can be used to specify
dates so that the data returned is valid for those dates. There can be one or two
dates specified. If one is specified, all records that are valid on that date are
returned. If two are specified, all records that are valid within the date range are
returned. The two options are shown in the following example:

select validTimeState(asAtDate) from ExchangeRates;

select validTimeState(fromDate, toDate) from ExchangeRates;

Computed Columns in Views


A view is a selected number of fields that come from one or more related tables.
Microsoft Dynamics AX can also display a computed column based on fields in
those tables. A computed column generates T-SQL code at compile time and
adds it to the T-SQL code generated by the view, which increases performance.

Demonstration: Computed Column


During the following demonstration you examine a computed column in the
standard application.

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:

TableName viewName = identifierstr(InventTableExpanded);

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

13. An infolog box is displayed with a number of SQL commands. One


of them is creating the view and is as follows:

FIGURE 2.4 CREATE VIEW

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.

The BinaryIO class is used to import and export binary data.

The following code is an example of the CommaIO class:

FileName fileName = 'c:\\test.csv';


FileIoPermission permission;
CommaIO commaIO;
container readCon;
int i;
int level;
str column1;
str column2;
str column3;
;

permission= new FileIoPermission(filename,'r');


permission.assert();

commaIO = new CommaIo(filename, 'r');


commaIO.inFieldDelimiter(';'); // Delimiter...

if (commaIO)
{
while (commaIO.status() == IO_Status::OK)
{
readCon = commaIO.read();

column1 = conPeek(readCon, 1);


column2 = conPeek(readCon, 2);
column3 = conPeek(readCon, 3);

info(strFmt("%1 - %2 - %3", column1, column2,


column3));
}
}

In this example, a specific field delimiter character has been specified. By


default, the field delimiter character for CommaIO is the comma character, but in
this case the code specifies the semi-colon character as the field delimiter.

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:

• FileIO.write(str) - writes a string.


• FileIO.writeExp(container) - writes the contents of a container.
Elements are separated by character defined in
FileIO.outFieldDelimiter().
• FileIO.writeChar(int) - writes a unicode character represented by
an integer.

The following is sample code which writes a simple string to a text file:

FileName fileName = 'c:\\test.txt';


FileIoPermission permission;
FileIO fileIO;
str outputText;
#File
;

permission= new FileIoPermission(filename,#io_write);


permission.assert();

fileIO= new FileIO(filename, #io_write);

if (fileIO)
{
outputText = "text that will go into the text file.";

fileIO.write(outputText); //write the text to the


file.
fileIO.finalize(); //finish the 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.

The following is an example of a simple XML file:

<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.

Alternatively, Microsoft Dynamics AX includes a wrapper for the COM object


microsoft.xmldom, which creates the file using tag names and data.

The following example creates an XML file containing employee names and
addresses:

static void XMLWriteEmplAddr(Args _args)


{
FileIoPermission permission;
XMLDocument xmlDoc = XMLDocument::newBlank();
XMLNode rootNode;
XMLNode NodeEmpl, NodeName, NodeAddr;
XMLElement xmlElement;
XMLText xmlText;
HCMWorker HCMWorker;
;

permission= new
FileIoPermission('c:\\Empl_Address_List.xml','w');
permission.assert();

xmlDoc = XMLDocument::newBlank();

// Create first line containing version info


rootNode = xmlDoc.documentElement();
xmlElement = xmlDoc.createElement(‘EmployeeList’);
rootNode = xmlDoc.appendChild(xmlElement);

while select EmplTable


{
// Create a node for the Employee record
xmlElement = xmlDoc.createElement(‘Employee’);
NodeEmpl = rootNode.appendChild(xmlElement);

// Create a node for the name


xmlElement = xmlDoc.createElement(‘EmplName’);
NodeName = NodeEmpl.appendChild(xmlElement);
xmlText =
xmlDoc.createTextNode(EmplTable.Name());
NodeName.appendChild(xmlText);

// Create a node for the address


xmlElement = xmlDoc.createElement(‘EmplAddr’);
NodeAddr = NodeEmpl.appendChild(xmlElement);
xmlText =
xmlDoc.createTextNode(EmplTable.Address);
NodeAddr.appendChild(xmlText);
}

2-39
Development III in Microsoft Dynamics® AX 2012

// Save the file


xmldoc.save('c:\\Empl_Address_List.xml');
}

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.

static void XMLReadCustTrans(Args _args)


{
FileIoPermission permission;
XMLDocument doc;
XMLNode rootNode, AccNode, NameNode, transNode;
XMLNodeList custTransList;
XMLParseError xmlError;
int i;
;

permission= new
FileIoPermission("C:\\CustTrans.xml",'r');
permission.assert();

// Get the XML document


doc = new XMLDocument();
doc.load("C:\\CustTrans.xml");

xmlError = doc.parseError();

if (xmlError && xmlError.errorCode() != 0)


throw error(strFmt("Error: %1",xmlError.reason()));

rootNode = doc.documentElement();

// Get the customer account and name from the document


AccNode =
rootNode.selectSingleNode("//CustomerAccount");
NameNode = rootNode.selectSingleNode("//CustomerName");

setprefix(AccNode.text()+ ' ' + NameNode.text());

// Select all the customer transactions into a nodelist


custTransList =
rootNode.selectNodes("//TransactionDetail");

for (i = 0; i < custTransList.length(); i++)


{
transNode = custTransList.item(i);

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.

An Open DataBase Connection (ODBC) is a simple way for Microsoft Dynamics


AX to access an external database, and perform queries on it to fetch the required
data.

For this to work, an ODBC DataSource Name (DSN) needs to be created in


Windows. A DSN acts as a thin database client, usually including all
authentication information like username and password. The DSN needs to be
created on the tier where the X++ code will call it from (in other words, either the
client or the AOS). Best practice is to keep the DSN on the AOS, to help
scalability.

Once a DSN is available in Windows, code can be written to leverage its access
to the external database:

static void TestODBC()


{
LoginProperty loginProperty;
OdbcConnection odbcConnection;
Statement statement;
ResultSet resultSet;
str sql, criteria;
SqlStatementExecutePermission perm;
;

//Set information on the ODBC


loginProperty = new LoginProperty();
loginProperty.setDSN("dsn");
loginProperty.setDatabase("databaseName");

//Create connection to external DB


odbcConnection = new OdbcConnection(loginProperty);

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);
}

//Shutting down the connection


resultSet.close();
statement.close();
}
else
error("Failed to log on to the database");
}

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().

The string that is passed to the statement.executeQuery() method must be an


SQL query in the format that the external database can understand. For example,
if you are querying an Oracle database, the string must contain a valid Oracle
SQL query.

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;
;

//Creates the instance


ExcelApp = SysExcelApplication::construct();

//Opens the file


ExcelApp.workbooks().open('c:\test.xls');

//Shows the value of the selected cell.


cellValue = ExcelApp.activeSheet().cells().item(1,
1).value().toString(); //both columns and rows are
referenced numerically, eg. A=1, B=2 etc
info(strfmt("Value of cell A1 = %1", cellValue));

2-43
Development III in Microsoft Dynamics® AX 2012

Lab 2.5 - Integrating External Data


Scenario

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:

Item number, Date, Amount

For example your file could look like this:

FIGURE 2.5 SALES PRICE IMPORT FILE

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

1. Open the AOT and create a new Job.


2. Copy the following code to the job.

static void ImportSalesPrice(Args _args)


{
CommaIO commaIO;
container readCon;
str column1, column2, column3;
PriceDiscTable priceDiscTable;
#File
;

2-44
Chapter 2: Working with Data

commaIO = new CommaIO('c:\\salesprice.csv', #io_read);


if (commaIO)
{
while (commaIO.status() == IO_Status::OK)
{
readCon = commaIO.read();

column1 = conPeek(readCon, 1);


column2 = conPeek(readCon, 2);
column3 = conPeek(readCon, 3);

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();
}
}
}

3. Press F5 to run the job.


4. You can check that the prices have been imported by navigating to
Product information management > Released products. Find the
items that you have set the price for in the file, and then click Sell >
Sales price.

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

Test Your Knowledge


Test your knowledge with the following questions.

1. Match the select statement clauses, with their correct purpose:

_____ 1. Fieldlist a. This keyword forces the database to use a


_____ 2. FirstOnly specific index as the order by clause.
_____ 3. FirstFast b. This keyword forces the database to prioritize
fetching the first few rows fast over fetching the
_____ 4.
complete result set.
ForceNestedLoop
c. This keyword forces the database to return
_____ 5.
records regardless of dataAreaId, or for a set of
ForceSelectOrder
specific dataAreaId's.
_____ 6.
d. This keyword forces the database server to use a
CrossCompany
nested-loop algorithm to process a given SQL
_____ 7. Index statement that contains a join.
e. Used to specify fields which will be fetched from
the database.
f. This keyword forces the database server to
access the tables in a join in the given order.
g. This keyword forces the database to return only
the first found record.

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.

4. A temporary table is declared in an object running on the client. The first


record is inserted from a method running on the AOS. Where is the data in
the temporary table physically stored?

5. If you create a new field on CustTable and SalesTable, where is it most


appropriate to set the field on SalesTable from the field on CustTable?

2-48
Chapter 2: Working with Data

6. Why does Microsoft Dynamics AX use Parm tables?

7. Which line of code adds a new node into an XML file:


( ) Node = rootNode.AddChild(xmlDoc.createElement("Customer"));
( ) Node = rootNode.AppendChild(xmlDoc.createElement("Customer"));
( ) Node = AddChild(rootNode).(xmlDoc.createElement("Customer"));
( ) Node = rootNode.(xmlDoc.create("Customer"). appendChild());

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

Quick Interaction: Lessons Learned


Take a moment and write down three key points you have learned from this
chapter

1.

2.

3.

2-50
Chapter 2: Working with Data

Solutions
Test Your Knowledge
1. Match the select statement clauses, with their correct purpose:

e 1. Fieldlist a. This keyword forces the database to use a


g 2. FirstOnly specific index as the order by clause.
b 3. FirstFast b. This keyword forces the database to prioritize
fetching the first few rows fast over fetching the
d 4.
complete result set.
ForceNestedLoop
c. This keyword forces the database to return
f 5.
records regardless of dataAreaId, or for a set of
ForceSelectOrder
specific dataAreaId's.
c 6.
d. This keyword forces the database server to use a
CrossCompany
nested-loop algorithm to process a given SQL
a 7. Index statement that contains a join.
e. Used to specify fields which will be fetched from
the database.
f. This keyword forces the database server to access
the tables in a join in the given order.
g. This keyword forces the database to return only
the first found record.

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

4. A temporary table is declared in an object running on the client. The first


record is inserted from a method running on the AOS. Where is the data in
the temporary table physically stored?

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.

5. If you create a new field on CustTable and SalesTable, where is it most


appropriate to set the field on SalesTable from the field on CustTable?

MODEL ANSWER:

SalesTable.InitFromCustTable()

6. Why does Microsoft Dynamics AX use Parm tables?

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.

7. Which line of code adds a new node into an XML file:


( ) Node = rootNode.AddChild(xmlDoc.createElement("Customer"));
(•) Node =
rootNode.AppendChild(xmlDoc.createElement("Customer"));
( ) Node = AddChild(rootNode).(xmlDoc.createElement("Customer"));
( ) Node = rootNode.(xmlDoc.create("Customer"). appendChild());

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

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy