Code highlighting

Showing posts with label Table. Show all posts
Showing posts with label Table. Show all posts

Monday, September 04, 2017

Development tutorial: insert_recordset using the Query class

Introduction

I am sure most of you are familiar with set-based X++ CUD operators: insert_recordset, update_recordset and delete_from. They allow performing database operations with a large number of records in a single roundtrip to the server, instead of a row-by-row type of operation, which depends on the number of rows being processed. As a result, they can provide a very significant boost in overall performance of a selected flow.

If not familiar or just need a refresher, you can read more about it by following the link to MSDN.

Problem statement

There's however one significant drawback with these operators - they are all compile-time constructs, so they lack the flexibility of the flow modifying the database request before it is set based on runtime state of the flow.

And, once the application is sealed, that will mean there is no way to modify the request even at compile-time, as X++ statements are not extensible.

Solution

In Microsoft Dynamics AX 2012 R3 a static method was added on the Query class, that allows to solve the two problems above for insert_recordset. This is of course now also available in Microsoft Dynamics 365 for Finance and Operations: Enterprise edition.

Note: update_recordset and delete_from are still not supported through the Query class.

Example

Imagine that we need to write a function that would copy selected sales order line information into a sales order line history table for one or more orders.

1. Data model

Here's how the data model for this table is defined in this example:

DEV_SalesLineHistory data model diagram
DEV_SalesLineHistory data model diagram

2. New DEV_SalesLineHistory table in Visual Studio

And here's how the new table looks in Visual Studio (I created a new model for it in a separate package dependent on Application Suite):

DEV_SalesLineHistory table in Visual Studio
DEV_SalesLineHistory table
Note I skipped all stuff non-essential to this example

3. Copy function through regular X++ insert_recordset statement

Let's first write the statement for inserting the history records using the regular insert_recordset operator:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertXppInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        SalesLine               salesLine;
        InventDim               inventDim;

        JournalPostedDateTime   postedDateTime = DateTimeUtil::utcNow();
        JournalPostedUserId     postedBy = curUserId();
        SalesDocumentStatus     postingType = DocumentStatus::PackingSlip;

        insert_recordset salesLineHistory
        (
            SalesId,
            LineNum,
            InventTransId,
            SalesQty,
            SalesUnit,
            InventSiteId,
            InventLocationId,
            PostedDateTime,
            PostedBy,
            PostingType
        )
        select SalesId, LineNum, InventTransId, SalesQty, SalesUnit from salesLine
            where salesLine.SalesId == _salesId
            join InventSiteId, InventLocationId, postedDateTime, postedBy, postingType from inventDim
                where inventDim.InventDimId == salesLine.InventDimId;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, we do a simple select from SalesLine, specifying the exact fields, joined to selected fields from InventDim, where the field list also contains a few local variables to populate into the rows being inserted.
This is the standard syntax used with X++ insert_recordset statement, which all of you are familiar with.

4. Method signature for Query::insert_recordset()

Now let's convert the above to a Query, and call Query::insert_recordset() instead.
This method accepts three arguments:
  • An instance of a table record. This is where data will be inserted into. We can then use this variable to ask how many rows were inserted, for example.
  • An instance of a Map(Types::String, Types::Container), which defines the mapping of the fields to copy. In X++ operator, this had to be based on the specific order in the field selection lists in the select statement. 
    • The map key is the target field name.
    • The value is a container, which defines a pair of values:
      • the unique identifier of the QueryBuildDataSource object points to the table to copy the value from
      • the field name on the above data source to copy the value from
  • An instance of a Query class, which defines the select statement for the data, similar to what you see in the X++ version above.

As you can see from the above, it does not account for literals, as we did with variables in the X++ operator example.
That is currently not supported with this API.
We can however solve this through a use of a "temporary" table, as suggested below.

5. Define a new table to store calculable literals

Let us define a new table that will store the data required by our insert statement. That means it needs to contain four fields:
  • PostedDateTime
  • PostedBy
  • PostingType
  • SalesId - we'll use this to join to SalesLine. This could be sessionId or whatever is required to ensure concurrency and uniqueness
Here's how the table would look in Visual Studio designer:

DEV_SalesLineHistoryPostingDataTmp table definition

We can now populate this table with the required values and join it to our query.
After executing the bulk insert we can then delete the inserted row (if necessary).


Another possible implementation here could be to use a View, with computed  columns for the different literal values needed. You could select from a table that has only 1 row, like InventParameters or the like. This is however less flexible, as it'll be compiled in, while with a "temporary" table you could determine the values at runtime.

6. Write up the code using the Query::insert_recordset() method

Now we are all set to write the necessary code. It would look like below:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertQueryInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        
        Query query = new Query();
        QueryBuildDataSource qbdsSalesLine = query.addDataSource(tableNum(SalesLine));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, LineNum));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, InventTransId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesQty));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesUnit));
        qbdsSalesLine.addRange(fieldNum(SalesLine, SalesId)).value(queryValue(_salesId));
        QueryBuildDataSource qbdsInventDim = qbdsSalesLine.addDataSource(tableNum(InventDim));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventLocationId));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventSiteId));
        qbdsInventDim.relations(true);
        QueryBuildDataSource qbdsPostingData = qbdsInventDim.addDataSource(tableNum(DEV_SalesLineHistoryPostingDataTmp));
        qbdsPostingData.addLink(fieldNum(SalesLine, SalesId), fieldNum(DEV_SalesLineHistoryPostingDataTmp, SalesId), qbdsSalesLine.name());
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedBy));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostingType));

        Map targetToSourceMap = new Map(Types::String, Types::Container);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesId),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, LineNum),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, LineNum)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventTransId),     [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, InventTransId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesQty),          [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesQty)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesUnit),         [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesUnit)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventLocationId),  [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventLocationId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventSiteId),      [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventSiteId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedDateTime),    [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedBy),          [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedBy)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostingType),       [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostingType)]);

        ttsbegin;
        
        DEV_SalesLineHistoryPostingDataTmp postingData;
        postingData.PostedDateTime = DateTimeUtil::utcNow();
        postingData.PostedBy = curUserId();
        postingData.PostingType = DocumentStatus::Invoice;
        postingData.SalesId = _salesId;
        postingData.insert();
        
        Query::insert_recordset(salesLineHistory, targetToSourceMap, query);

        delete_from postingData 
            where postingData.SalesId == _salesId;

        ttscommit;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, the first part of the code builds a query using the QueryBuild* class hierarchy. The query is identical to the above select statement, with the addition of another join to our "tmp" table to retrieve the literals.
The second part populates the target to source Map object, which maps the fields to insert into to their source.
The third part actually invokes the operation, making sure we have the record populated in our "tmp" table beforehand.

Note. Because the first argument is the record we insert into, we can use it to get the RowCount(), telling us how many records have been inserted.

7. Extensibility aspect

Leaving the code just like it is does not actually make it extensible, as the partners would not be able to add additional fields to copy, or additional conditions/joins to the query. To accomplish this, we'd need to break the logic out into smaller methods with specific responsibilities, or add inline delegates to do the same. Generally speaking, you should always favor breaking the code down into simpler methods over delegates.
I've not done this in the example, as that's not the purpose, but you should follow these guidelines in production code.

8. Execute and test the code

We can execute these methods now, but first we need to ensure we have a test subject, aka a Sales order to practice with. Here's the one I used in the example:

A sales order with 3 order lines
And here's the Runnable Class that invokes the two methods above:

class DEV_Tutorial_InsertRecordset_Runner
{        
    public static void main(Args _args)
    {        
        const SalesId SalesId = '000810';

        if (SalesTable::exist(SalesId))
        {
            Counter xppInsert = DEV_Tutorial_InsertRecordset::insertXppInsert_Recordset(SalesId);
            Counter queryInsert = DEV_Tutorial_InsertRecordset::insertQueryInsert_Recordset(SalesId);

            strFmt('Tutorial Insert_Recordset');
            info(strFmt('Inserted using X++ insert_recordset = %1', xppInsert));
            info(strFmt('Inserted using Query::insert_recordset() = %1', queryInsert));
        }
    }

}

You can download the full project here.


Hope this helps! Let me know if you have any comments.

Saturday, September 24, 2016

Tool: Dynamics AX 7 browser power & Table browser add-in for Google Chrome


Microsoft Dynamics AX "7" is, as you all know, a Cloud release, which means that the one and only client available for AX "7" is the web browser.

Nowadays browsers are pretty advanced, meaning that you get pretty much the same look and feel, as in a "rich" Win32 client. It has its drawback as well, of course, but we are not here to talk about those.

On the positive side, it now allows for a number of entry points that will take you directly into the flow or form you want to execute, because most of the information about where you want to go is provided through the URL itself.

I have already previously posted about the Warehouse Mobile Devices Portal emulator form and how you can access it. Here it is again:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF&mi=action:WHSWorkExecute

You basically specify your Dynamics AX URL, followed by which company you want to connect to, and which menu item to open - in this case, an Action menu item WHSWorkExecute. Easy, right?

Well, you can do the same trick to open other menu items, and that is what this blog post is about - Table Browser.

As some of my old readers know, I'm a big fan. If you are not one of those, and are running previous versions of AX, check out the blog post below, it's awesome!

http://kashperuk.blogspot.dk/2007/09/devsystablebrowser-version-20-is-out.html

Well, Dynamics AX "7" also has a little something to help you browse tables now.
Here is a cool little add-in for Google Chrome, which allows you to open the table browser for a selected table in a selected company (settings are persisted, so you don't need to enter the company name each time):

https://chrome.google.com/webstore/detail/ax-table-browser-caller/nahbldacmaibopfiiaoboloegpobpccn

I've just tried it out and it's pretty neat, so take it for a spin and let me know what you think!

To finish off, let's just take a quick look at what the add-in actually does, which is - opens a pre-defined URL similar to WMDP link above:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?mi=SysTableBrowser&TableName=WHSWorkTable&cmp=USMF&limitednav=true&lng=en-us

We specified that we want to open the Display (which is the default) menu item SysTableBrowser, passed in an argument TableName WHSWorkTable. We also specified the company, as before, as as well the display language. We've also added in the limitednav=true flag, which removes some of the navigation panels and buttons, so you cannot navigate away from this page to another form or menu.

Technical Note. Check out UrlUtility.getQueryParamValue('TableName'); This could potentially be used in some of your customizations

Friday, October 28, 2011

Tutorial: AX 2012 - Invalid field access or Accessing unretrieved fields

Read the post all the way until the end - there's an ask for you guys there!

As many of you have already seen in the new AX 2012 release, there are a lot more tables present in the application, because we went through an exercise of normalizing the data model.
Another feature that was added at the same time was the Table Inheritance, allowing to sub-class tables and add additional fields reusing some of the behaviors of the base table.

All of it is great! The only problem with it is that it comes at a price - we now need to retrieve a lot more data and do a lot more joins between tables. So we tried to mitigate that by using field lists where possible and adding AdHoc query support, which basically eliminates unneccessary joins between tables from the hierarchy, if fields from these tables are not selected.

We have also implemented a number of things that allow us to clearly see if the field was selected in the user interface, the APIs needed for doing the same, as well as special handling for invalid field access.

This tutorial contains a form, which showcases the different aspects of data access in cases described above.
You can download the project with the tutorial from my SkyDrive.

On the first tab, we have the standard "On hand" view, which in the data model is a join between InventSum and InventDim, with group by clauses on selected fields, in this case, ItemId from InventSum and InventLocationId from InventDim, and aggregation on the AvailPhysical column.
As you can see from the screenshot below, the rest of the InventDim fields are shown as Unretrieved. So, naturally, accessing one of them from code, for example, should not be a legal operation.
To verify that statement, there are 2 buttons on the form, the Incorrect and the Correct was of accessing the field. Clicking on the first one simply tries to read the value out of the InventSiteId field, while the second one uses the API method TableBuffer.isFieldDataRetrieved() to first verify if the field can actually be accessed.

Note, that in the below example, both buttons will work, as in, there won't be any errors shown to the user. When accessing the field, even though it is not retrieved to the client, the value will be treated as the default value for that type, that is, an empty string.

In order to verify that we do not access fields in an invalid way like above, we have introduced a parameter, that will throw an exception if a field that was not retrieved is being accessed.

In order to enable this validation, you need to update the below shown parameter in the Server configuration form (Located under System administration \ Setup \ System). After changing the value (note, that it is per AOS) you need to restart the AOS for the changes to take effect.


Now, if you try to use the first button (under Incorrect) from above, you will get a stack trace, notifying you that the specified field was not retrieved.



We suggest that when testing your modifications in the application, you always have that flag enabled, so as to avoid unpleasant and hard-to-find bugs later on in the production environment.


On the second tab of the Tutorial form, the same type of information is presented, only this time the output is actually based on a new table inheritance structure I created.


The base table, GenericBall, contains 2 characteristics of any ball. SoccerBall is extending it and adding an additional characteristic (for the sake of the example, let's assume Brand is only relevant to soccer balls). This is basically a very simple table inheritance structure.

In the form, however, I am only selecting to view the Brand field from SoccerBall, not selecting the other 2 fields from the base table. As expected, they show up as Unretrieved in the user interface.
However, there is one difference in how Scsc tables are handled - and that is, they will always throw an exception when you try to access one of the unretrieved fields.
The two buttons above the grid demonstrate that. Try it out, enabling/disabling the server configuration parameter shown above.


That's pretty much all there's to it. Let me know if you have any questions.

Now, I have one thing to ask you all too.
We in the AX Test team have done our best to find invalid field access problems before the release, but if you find one using the approach above, please log it either through the standard Microsoft channels (partner/ MsConnect/ etc.), or as a comment directly under this blog post.

Thank you!

Saturday, May 07, 2011

Tutorial: Table Relation properties in AX 2012

As many of you already know, Microsoft has put in a lot of effort into normalizing the tables in AX 2012 and consolidating all the data modeling tools in one place - the table itself. As part of this effort, a number of new properties have been introduced on Tables, and in this post I would like to cover some of them, namely the properties on Table Relations.

The new properties you will find on a Table Relation in AX 2012 are:
  • Cardinality
  • RelatedTableCardinality
  • RelationshipType
  • Role
  • RelatedTableRole
  • UseDefaultRoleNames
  • CreateNavigationPropertyMethods
  • NavigationPropertyMethodNameOverride

Hua Chu from the AX team has written a Guideline document, explaining how these properties should be set for Relations you add to Tables in AX. Note that in AX 2012 most of the above information is not actually used at runtime. This is something that will happen in future releases.
I have modified the document so that it contains the information relevant for partners and customers extending the standard application and have uploaded it to my OneDrive.

The document requires certain knowledge of Entity Relationship Modeling (ERM) and UML notation.

Disclaimer:
This document is intended as a guideline only, and should not be used as a Step-by-Step instruction.
Changes to any of the described functionality might still happen before AX 2012 RTM.


Your feedback and questions are, as always, welcome.