More on views – computed columns

The view we created earlier in this book was a simple view with an aggregation, serving as an introduction to views in AX. In this section, we will expand on this to cover some important features. For this section, we will need administrator access to AX SQL Server and have SQL Server Management Studio (SSMS) installed.

We won't use many of the features from SSMS, but it is worth reading the following:

How SQL Server executes views

When we create views, we are effectively storing a query definition that is executed by SQL Server, even though we treat them as read-only tables in AX. If we write a view that uses another view as its data source, SQL Server will determine the best execution plan based on the desired output columns and the queries defined by the views.

This is important to understand, and it will help us create more efficient views. For example, there is an almost natural belief that writing a view that filters data to be used by another view is more efficient, as the data source has fewer rows. This is incorrect because the base view's data doesn't physically exist; it is merely a query definition.

Another example is how aggregated columns in a base view are treated by views based on it. Let's say our base view has an aggregated column, and we write another view that uses this base view but does not use the base view's aggregated column. The aggregation will not be executed by SQL Server.

For example, suppose we created the following views using SSMS:

CREATE VIEW [baseView] AS
SELECT SalesId, Sum(QtyOrdered) totalOrdered
    FROM SalesLine 
    GROUP BY SalesId
GO
CREATE view [otherView] AS
SELECT SalesId from baseView
GO

Enable Include actual execution plan in the Query menu, and write the following code:

SELECT SalesId FROM otherView

All of this produces the following execution plan:

How SQL Server executes views

It is clear that the query executed by SQL Server is directly querying SalesLine.

Note

The preceding code was merely meant to explain quickly what happens when a view uses another view. We must never create tables or views directly in SQL Server.

Adding a computed column

One of the limitations of SQL views defined in AX is that you cannot use CASE statements or subqueries as columns. To solve this problem, Microsoft now allows methods to be created that return a SQL statement that can be used as a column in the view. These are called computed columns.

Note

For this section, you will need Microsoft SSMS.

View methods are static server methods that should return a string of valid Transact-SQL (actual SQL code).

This could be something like the following:

public server static str thisIsOne()
{

    return "SELECT 1";
}

When added to the view as an integer view method called One, it adds a column with the following code:

CAST((SELECT 1) as INTEGER) as One

Note

The returned string can largely be any valid Transact-SQL code that can execute under the security context of the AX server service account. Another reason we lock down service accounts is to minimize the exposed surface area.

A note on performance of computed columns

We can therefore write a computed column that returns a complicated subquery that is used as a column in a view. When we do this, we must ensure that SQL Server is executing the code efficiently, and it is hard to know this without testing it.

Although computed column methods may seem far easier to use than building a query with aggregations, if the aggregation can be done in the query that the view uses, that is where it should be done. The following example explains why:

  1. Open the SQL Server Management Studio and select your business database.
  2. Create a new query window and type the following:
    SELECT InventTable.ItemId, 
        ISNULL(SUM(SalesLine.QtyOrdered), 0.00) AS QtySold,
        ISNULL(SUM(PurchLine.LineAmount), 0.00) AS Purchased,
        ISNULL(SUM(PurchLine.QtyOrdered), 0.00) AS QtyBought,
        ISNULL(SUM(PurchLine.LineAmount), 0.00) AS Purchased
        FROM InventTable
      LEFT OUTER JOIN SalesLine 
          ON SalesLine.ItemId     = InventTable.ItemId
         AND SalesLine.DataAreaID = InventTable.DataAreaID
         AND SalesLine.Partition  = InventTable.Partition
      LEFT OUTER JOIN PurchLine 
          ON PurchLine.ItemId     = InventTable.ItemId
         AND PurchLine.DataAreaID = InventTable.DataAreaID
         AND PurchLine.Partition  = InventTable.Partition
      WHERE InventTable.DataAreaId = 'USMF'
        AND InventTable.Partition = 5637144576
        GROUP BY InventTable.DataAreaID, 
                 InventTable.Partition,
                 InventTable.ItemId
  3. Ensure that Include Actual Execution Plan and Include Client Statistics are selected in the Query menu.
  4. Navigate to Query | Reset Client Statistics.
  5. Execute the query and note the following graphic in the Execution plan tab:
    A note on performance of computed columns
  6. Let's try the same code, but using subqueries for the two aggregations:
    SELECT IT.ItemId, 
           ISNULL((SELECT SUM(SL.QtyOrdered) 
                 FROM SalesLine SL
                 WHERE SL.ItemId = IT.ItemId
                   AND SL.DataAreaID = IT.DataAreaID
                   AND SL.Partition  = IT.Partition), 0)
           AS QtySold,
           ISNULL((SELECT SUM(SL.LineAmount) 
                 FROM SalesLine SL
                 WHERE SL.ItemId = IT.ItemId
                   AND SL.DataAreaID = IT.DataAreaID
                   AND SL.Partition  = IT.Partition), 0)
           AS Sales,
           ISNULL((SELECT SUM(PL.QtyOrdered) 
                 FROM PurchLine PL
                 WHERE PL.ItemId     = IT.ItemId
                   AND PL.DataAreaID = IT.DataAreaID
                     AND PL.Partition  = IT.Partition), 0)
           AS QtyBought,
           ISNULL((SELECT SUM(PL.LineAmount) 
                 FROM PurchLine PL
                 WHERE PL.ItemId     = IT.ItemId
                   AND PL.DataAreaID = IT.DataAreaID
                     AND PL.Partition  = IT.Partition), 0)
           AS Purchased
        FROM InventTable IT
      WHERE IT.DataAreaId = 'USMF'
        AND IT.Partition = 5637144576
      ORDER BY IT.DataAreaId, IT.Partition, IT.ItemId
  7. The execution plan reveals the cost of doing it this way, as shown here:
    A note on performance of computed columns

The execution plans look completely different, and the second example shows that some more work is being done in putting the results together before it is returned to the client. The cost of this additional work, however, is shown as 0 %, so the execution plan makes the difference appear worse than it actually is. The subquery method can be faster, especially for simple aggregations where the table is used once. Should we have many complicated subqueries, the join method is usually much faster.

Tip

Checking and understanding the execution plan is generally important, and it should be tested against a real-life data scenario—not just on our development machines.

Creating a computed column – concatenating two fields

To get started with our first computed column method, we will create a simple concatenation of two fields. The first part to understand is that we have access to the tables and fields in the view's data sources, and we can use them to perform calculations or join other tables not in the view's data source list.

We will use the ConFMSVehicleUtilisation view, which we created earlier in this book. The view's data sources include the ConFMSVehicleTable table and the VehicleId and Name fields.

To do this, create a new method under the view's Methods node and write the following code:

public server static str vehicleIdAndName()
{
    str viewName = tableStr(ConFMSVehicleUtilisation);
    str sql;
    // this is the data source's name, 
    // which may not be the same as the table name
    str datasourceName = identifierStr(ConFMSVehicleTable);
    str vehicleIdField = fieldStr(ConFMSVehicleTable, VehicleId);
    str nameField      = fieldStr(ConFMSVehicleTable, Name);
    // to contain the actual SQL column name
    str vehicleId; 
    str name;
    // the following will return a string similar to T1.VehicleId
    vehicleId = SysComputedColumn::returnField(viewName, 
                                               datasourceName,
                                               vehicleIdField);
    name      = SysComputedColumn::returnField(viewName, 
                                               datasourceName, 
                                               nameField);
    sql = vehicleId + "+ '-' + " + name;
    return sql;
}

We use SysComputedColumn::returnField, as AX will sometimes use a different physical field name in the actual SQL table. It will also alias each table from T1 upwards, like this:

SELECT T1.SalesId from SalesTable T1

Next, add this to the view's field list by following these steps:

  1. Right-click on ConFMSVehicleUtilisation | Fields and choose String Computed Column.
  2. Open the property sheet for the new field.
  3. Change the Name to VehicleIdAndName.
  4. Enter a suitable label in the Label property.
  5. Select vehicleIdAndName from the ViewMethod list.
  6. Set ExtendedDateType to Name.
  7. Save and synchronize the view (right on the view and choose Synchronize).

This fails because we used the Name field, which is not included in the group by the clause of the query. The VehicleId field is fine because AX automatically adds fields in the output list as a group by for aggregated views.

In this case, it is okay to use the MIN command to select the Name field first. This is because the VehicleId and Name fields are on the same data source, and grouping VehicleId means that ConFMSVehicleTable is not aggregated. This may seem obvious in this case, but if we were grouping by VehiceGroupId, using MIN would return the lowest Name field for each grouped set of ConFMSVehicleTable records.

Change the code as follows:

sql = vehicleId + "+ '-' + MIN(" + name + ")";

When you open the view, you should see the new field. The cool part is that the calculation is done by SQL Server and is treated like a real view field in AX, which means we can filter and sort by it.

The actual Transact-SQL returned by this function will be similar to the following:

T1.VehicleId +'-'+MIN(T1.Name)

By adding this to the view as a string computed column, it adds a CAST command to ensure that it returns the correct type. The relevant portion of the actual Transact-SQL generated is as follows:

SELECT T1.VEHICLEID AS VEHICLEID,
--/
(CAST((T1.VEHICLEID+ '-' + min(T1.NAME)) AS NVARCHAR(60)))
AS VEHICLEIDANDNAME
--/

Creating a computed column – return data from related tables

The code is similar to the preceding code, but we have a big consideration here when joining a table as a subquery. AX will naturally join the two system fields, Partition and DataAreaId, and they are also added to the indexes. Simply, we must add these fields to our joins (depending on the table's SaveDataPerPartition and SaveDataPerCompany properties) when writing a subquery as a view method.

The following method will sum records from ConFMSVehServiceTable for each vehicle:

public server static str vehicleServiceCount()
{
    str viewName = tableStr(ConFMSVehicleUtilisation);
    str sql;
    // this is the data source's name,
    // which may not be the same as the table name
    str datasourceName = identifierStr(ConFMSVehicleTable);
    str vehicleIdField = fieldStr(ConFMSVehicleTable, VehicleId);
    str dataAreaFIeld  = fieldStr(ConFMSVehicleTable, DataAreaId);
    str partitionField = fieldStr(ConFMSVehicleTable, Partition);
    // to contain the actual SQL column name
    str vehicleId;
    str dataAreaId;
    str partition;
    // the following will return a string similar to T1.VehicleId
	dataAreaId = SysComputedColumn::returnField(viewName,
                                               datasourceName,
                                               dataAreaFIeld);
    partition = SysComputedColumn::returnField(viewName,
                                               datasourceName,
                                               partitionField);
    vehicleId = SysComputedColumn::returnField(viewName,
                                               datasourceName,
                                               vehicleIdField);

    sql  = "SELECT COUNT(*) FROM ConFMSVehServiceTable LVS 
";
    sql += "    WHERE LVS.VehicleId  = " + vehicleId  + "
";
    sql += "      AND LVS.DataAreaId = " + dataAreaId + "
";
    sql += "      AND LVS.Partition  = " + partition + "
";

    return sql;
}

Note

In the preceding code, we added a newline character. We do this when writing more complicated code so that we can read the resulting Transact-SQL in SSMS.

Next, add this to the view's field list by following these steps:

  1. Right-click on ConFMSVehicleUtilisation | Fields and choose Real Computed Column.
  2. Open the property sheet for the new field.
  3. Change Name to ServiceRecords.
  4. Enter Service records as the Label property.
  5. Select vehicleServiceCount from the ViewMethod list.
  6. Set ExtendedDateType to Total.
  7. Save the view and synchronize it as before.

Other useful functions

Although we can write Transact-SQL directly, there are some useful functions that are provided to us. All of these are stored in the SysComputedColumn class.

The common functions we tend to use are as follows:

To return a fully-qualified SQL field name use the command SysComputedColumn ::returnField.

To create a Transact-SQL CASE statement use SysComputedColumn::switch. This has two parameters:

  • _controlExpression: The value to be compared, such as a field. This is useful for mapping enum values to strings, for example, SalesStatus.
  • _comparisionExpressionMap: The map of comparison and result values. The map's key attribute will be the value to be compared, and the value attribute will be the value returned. For the SalesStatus example it would replace the numeric value of 1 with 'Open order'.

You can use the SysComputedColumn::if command to create a simple CASE statement with on CASE and one ELSE.

Tip

With regard to the switch method example using SalesStatus, since you have learned about metadata you can create the map using the DictEnum class.

The reason these methods exist is twofold: firstly, they are a helper to those not skilled in Transact-SQL, and secondly, the code returned will be valid for supported versions of SQL Server. This doesn't mean you can't use Transact-SQL, and in some cases, it will be easier to read and possibly perform faster. If you do write Transact-SQL, you are responsible for ensuring that the code will work on the current and future versions of SQL Server.

Common misconceptions

The most common mistake we have seen is using X++ functions when building the SQL statement, like this for example:

    sql += "      AND LVS.DataAreaId = " + curExt() + "
";

This will hardcode the company that was active when the view was synchronized into the statement; it will return a value only if the user is in that company.

Others are attempts to use X++ table methods when creating the statement. Again, the result of the method was determined when the view was synchronized, hardcoding the static result of the statement.

Finally, the data returned will bypass any security you have in place within AX. We therefore need to be mindful of this when writing the computed column. For example, if a user doesn't have access to CustTabe.CreditMax and we write a computed column that gets this value of AX on a view the user does have access to, they will see this data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.225