You sometimes need to join only one related record for each parent record, for example one order line for each order header. I’m always suspicious of such requirements, because it often means that users/analysts didn’t realize that there may be more than one record, or they didn’t think enough of identifying the one record they actually need. Nevertheless there are valid scenarios when showing only the first or the last record is exactly what’s required.
If you wrote it in T-SQL, you could use a subquery, but how to achieve the same in AX? If you read my blog post about Subqueries in views, you already know that computed columns can be for building subqueries. Let’s apply the same approach to this problem.
In my example, I’ll create a form showing information from sales lines and from the last shipment for each sales line. It’s based on a real implementation, nevertheless it’s simplified so we don’t have to deal with too complex computed column or form. (Also please don’t forget that computed columns are not supported in AX 2009 and older versions).
First of all, create a view and call it SalesLineLastShipmentView. Add SalesLine datasource and drag InventTransId field from the datasource to the Fields node.
InventTransId is the primary key of the SalesLine table and we’ll later use it for a join with SalesLine. The view will also contain ShipmentId (the primary key of WMSShipment table), which will be used for a join as well. To get the ID of the last shipment, first create a method on the view and put the following code there:
public static server str lastShipmentId()
SysDictTable orderTransDt = new SysDictTable(tableNum(WMSOrderTrans));
SysDictTable productCategoryDt = new SysDictTable(tableNum(EcoResProductCategory));
DictView dv = new DictView(tableNum(SalesLineLastShipmentView));
str s = strFmt('SELECT TOP 1 %1 FROM %2 WHERE %2.%3 = %4 ORDER BY %5 DESC',
orderTransDt.fieldName(fieldNum(WMSOrderTrans, ShipmentId), DbBackend::Sql),
orderTransDt.fieldName(fieldNum(WMSOrderTrans, InventTransId), DbBackend::Sql),
dv.computedColumnString(tableStr(SalesLine), fieldStr(SalesLine, InventTransId), FieldNameGenerationMode::WhereClause),
orderTransDt.fieldName(fieldNum(WMSOrderTrans, DlvDate), DbBackend::Sql));
return strFmt('ISNULL((%1), \'\')', s);
If it’s not clear what it does, wait a moment until we look at the resulting SQL query; it will be much more readable.
Now right-click the Fields node of the view and choose New > String Computed Column. Open properties of the column and set them as follows:
- Name = ShipmentId
- ExtendedDataType = WMSShipmentId
- ViewMethod = lastShipmentId
Save the view, open it in table browser if you have some shipments in your system, you should see them mapped to InventTransIds.
If you’re curious, open SQL Server Management Studio, find views in your AX database, right-click SalesLineLastShipmentView view and choose Design. It opens a view designer and you can find the actual SQL query there. This extremely useful especially if your view doesn’t work as expected and you want to see how exactly was translated to SQL.
This is the definition of our view:
SELECT INVENTTRANSID, DATAAREAID, PARTITION, RECID, CAST(ISNULL
((SELECT TOP (1) SHIPMENTID
WHERE (INVENTTRANSID = T1.INVENTTRANSID)
ORDER BY DLVDATE DESC), '') AS NVARCHAR(10)) AS SHIPMENTID
FROM dbo.SALESLINE AS T1
You should recognize the subquery defined in our view method. It finds the first only (TOP (1)) WMSOrderTrans record for each SalesLine and return the ShipmentId. If none is find, it returns an empty string instead of NULL (because AX doesn’t expect NULL values in this case). That’s handled by ISNULL() function.
Note that you should always use ORDER BY if you look for first or last records, because otherwise the order is not defined and a different row may be returned every time you execute the query.
Now we have a mapping between InventTransId and ShipmentId and because we want to see more fields in a form, we have to join tables to our view and build a form showing the fields.
Create a new form and add three datasources:
- SalesLine (the root datasource)
- SalesLineLastShipmentView (inner-joined to SalesLine)
- WMSShipment (joined SalesLineLastShipmentView with outer join, because some sales lines have no shipment at all)
Because we don’t have any relation between SalesLine and SalesLineLastShipmentView, we have to add it in code. Override init() method of SalesLineLastShipmentView datasource and add the following code below super():
We don’t need the same procedure for WMSShipment, because it’s already handled by the table relation on WMSShipmentID data type.
The last thing is to create a form design. To keep is simple, create a grid and drag some fields there from SalesLine and WMSShipment.
Views are powerful even without computed columns. For example, if you simply wanted the highest shipment ID for each line, you could build a view like this (and join it with SalesLine and WMSShipment tables):
SELECT InventTransId, MAX(ShipmentId) AS ShipmentId FROM WMSOrderTrans
GROUP BY InventTransId
Nevertheless computed columns add another dimension to views and allow you to use many more features of T-SQL. This ultimately allows you to implement business requirements that would be difficult to achieve by other means. Unfortunately the syntax for defining computed is far from perfect – it’s not easy to write nor read and you get little compile-time control. Here definitely is room for improvement. But this is definitely no argument for giving up all the power that computed columns offer.
Zipped .xpo with the view and the form (built in AX 2012 R3