formatted nav patterns
This commit is contained in:
parent
38a19d75d8
commit
2fad891a77
59 changed files with 1403 additions and 1970 deletions
|
|
@ -4,28 +4,23 @@ weight = 1040
|
|||
+++
|
||||
_By Bogdana Botez, at Microsoft Development Center Copenhagen_
|
||||
|
||||
### **Abstract**
|
||||
### Abstract
|
||||
|
||||
This pattern explains how to perform SELECT DISTINCT by using queries in Dynamics NAV.
|
||||
|
||||
****
|
||||
|
||||
**Description**
|
||||
|
||||
When working with tables, sometimes a developer needs to perform a SELECT DISTINCT (also known as SELECT UNIQUE) from a table. As NAV does not provide this out of the box, we present below a way to select unique records by using queries.
|
||||
|
||||
****
|
||||
|
||||
**Problem statement**
|
||||
|
||||
Let's consider the **VAT Entry** table as below: [
|
||||
][anchor0]
|
||||
Let's consider the **VAT Entry** table as below: [ ][anchor0]
|
||||
|
||||
[![ ][image0]][anchor1]
|
||||
|
||||
The goal is to select one line for each separate document that produced VAT Entries. In other words, we want records grouped by **Type, Document Type** and **Document No.**. However, if there are multiple lines with the same value of the triad **Type, Document Type** and **Document No.** in the **VAT Entry** table, we only want to see one of them.
|
||||
|
||||
#### **Solution**
|
||||
#### Solution
|
||||
|
||||
Create a new query object **VAT Entry Distinct Document No.**, with a single DataItem sourced from **VAT Entry** table. Add the three desired group-by fields **Type, Document Type** and **Document No.** as columns.
|
||||
|
||||
|
|
@ -41,7 +36,7 @@ Running the query yields a single record per document. You can notice in the sec
|
|||
|
||||
[![ ][image3]][anchor4]
|
||||
|
||||
### **Example**
|
||||
### Example
|
||||
|
||||
One thing is to be noted: there is a limitation to how much information you can take out from the records. For example, if we need to extract more information than just the one we already have in the columns, then the following apply: adding one more column of **Method Type** = **None** will indeed show more information, but it _might_ affect the grouping. More details below.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,37 +4,37 @@ weight = 1340
|
|||
+++
|
||||
_Originally by Abshishek Ghosh and Bogdan Sturzoiu at Microsoft Development Center Copenhagen_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This pattern uses queries to create an efficient way to detect duplicate entries in a table. This is, for example, useful when trying to find out which customers or contacts have the same names, so we can merge them later.
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
Duplicate detection has several requirements in Microsoft Dynamics NAV. One method to eliminate duplication is by defining the relevant field as the primary key. However, this method is not always practical either due to the size of the field or due to business requirements that dictate how duplicates are detected but not necessarily how they are eliminated. An example of this method is to detect contacts with the same name and take action to merge them if they are.
|
||||
|
||||
Before Dynamics NAV 2013, the only possibility was to iterate through the table in a loop and then create a sub-loop where another instance of the same table is filtered to check for duplicates. For example, to check for duplicate names in the Customer table, the code would look like this:
|
||||
|
||||
PROCEDURE HasDuplicateCustomers@26() : Boolean;
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE HasDuplicateCustomers@26() : Boolean;
|
||||
VAR
|
||||
Customer@1000 : Record 18;
|
||||
Customer2@1001 : Record 18;
|
||||
BEGIN
|
||||
BEGIN
|
||||
IF Customer.FINDSET THEN
|
||||
REPEAT
|
||||
Customer2.SETRANGE(Name,Customer.Name);
|
||||
IF Customer2.COUNT \> 1 THEN
|
||||
EXIT(TRUE);
|
||||
UNTIL Customer.NEXT = 0;
|
||||
Customer2.SETRANGE(Name,Customer.Name);
|
||||
IF Customer2.COUNT \> 1 THEN
|
||||
EXIT(TRUE);
|
||||
UNTIL Customer.NEXT = 0;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
|
||||
####
|
||||
END;
|
||||
```
|
||||
|
||||
This code would involve setting filters on the **Customer** table as many times as there are records in the table. This is an expensive operation.
|
||||
|
||||
Starting with Dynamics NAV 2013, we can use queries to create a more efficient implementation of the same logic.
|
||||
|
||||
## **Usage**
|
||||
## Usage
|
||||
|
||||
The solution involves that you create a query to return duplicates, and then invoke it from a method that would test the value of the query to identify if duplicates were found.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ The solution involves that you create a query to return duplicates, and then inv
|
|||
|
||||
* The query must be created with the table we want to search in as the dataitem.
|
||||
* The field we want to search for must be created as a grouped field.
|
||||
* Create a totaling field on the count, and add a filter for Count \> 1\. This ensures that only records with more than one instance of the field that we selected in the previous step are included in the query result.
|
||||
* Create a totaling field on the count, and add a filter for Count > 1. This ensures that only records with more than one instance of the field that we selected in the previous step are included in the query result.
|
||||
|
||||
Continuing with our Customer Name example, here is how the query would look:
|
||||
|
||||
|
|
@ -62,24 +62,21 @@ Continuing with our Customer Name example, here is how the query would look:
|
|||
Method=Count }
|
||||
}
|
||||
|
||||
**
|
||||
**
|
||||
|
||||
**Step 2 -- Invoking the Query to Check for Duplicates**
|
||||
|
||||
Now that the query is created, all we need to do is to invoke the query and check if any records are returned, which would mean that there are duplicates.
|
||||
|
||||
Here is an alternate implementation of the **HasDuplicateCustomers** method using the query that we created:
|
||||
|
||||
PROCEDURE HasDuplicateCustomersWithQuery@27() : Boolean;
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE HasDuplicateCustomersWithQuery@27() : Boolean;
|
||||
VAR
|
||||
CustomerDuplicate@1000 : Query 70000;
|
||||
BEGIN
|
||||
BEGIN
|
||||
CustomerDuplicate.OPEN;
|
||||
EXIT(CustomerDuplicate.READ);
|
||||
END;
|
||||
|
||||
####
|
||||
END;
|
||||
```
|
||||
|
||||
**Examples**
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,23 @@ weight = 1350
|
|||
+++
|
||||
_Originally by Bogdan Sturzoiu, Microsoft Development Center Copenhagen_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This pattern shows how the new query object type introduced in NAV 2013 allows you to replace costly loops when inspecting data from two or more tables.
|
||||
|
||||
### **Description**
|
||||
### Description
|
||||
|
||||
One of the core operations in a relational database is joining two or more tables. For example, you might need to extract all sales lines in the database together with information regarding the related sales header. This requires joining the Sales Header and Sales Line tables using Sales Header No. as the connecting field.;
|
||||
|
||||
The join operation has traditionally been done in C/AL by record looping. When NAV 2013 introduced the query object, it allowed us to produce a data set that is the result of a join operation between two or more tables. This simplifies the problem of finding related records in two tables linked through a foreign key.
|
||||
|
||||
#### **Pattern Elements**
|
||||
#### Pattern Elements
|
||||
|
||||
1. Two or more tables that contain records linked through a foreign key: Table 1, Table 2, Table n.
|
||||
2. A query object Query X, that joins Table 1, Table 2, etc. based on the connecting key.
|
||||
3. A processing codeunit that loops through the query records (or any other code-bearing object).
|
||||
|
||||
#### **Pattern Steps**
|
||||
#### Pattern Steps
|
||||
|
||||
1. Run the query on the connected tables.
|
||||
2. Loop through the records returned by the query.
|
||||
|
|
@ -30,13 +30,13 @@ The join operation has traditionally been done in C/AL by record looping. When N
|
|||
|
||||
_Figure 1\. The pattern elements_
|
||||
|
||||
### **Usage**
|
||||
### Usage
|
||||
|
||||
#### **Sample Problem**
|
||||
#### Sample Problem
|
||||
|
||||
The Bank Acc. Reconciliation Line table (274) and the Bank Account Ledger Entry table (271) are connected through the Bank Account No. field. Identify the matching pairs of records based on having the same remaining amount and transaction date.
|
||||
|
||||
#### **Solution Using Nested Loops**
|
||||
#### Solution Using Nested Loops
|
||||
|
||||
The classic C/AL approach is to:
|
||||
|
||||
|
|
@ -45,32 +45,34 @@ The classic C/AL approach is to:
|
|||
3. For each record in the filter, find the related records in the right table (table 271) and set the required filters on it.
|
||||
4. For each pair of records from the left and right table, decide if they are a solution and if so, apply them to each other.
|
||||
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
BankAccRecLine@1005 : Record 274;
|
||||
BankAccLedgerEntry@1006 : Record 271;
|
||||
BankAccEntrySetReconNo@1007 : Codeunit 375;
|
||||
BEGIN
|
||||
BEGIN
|
||||
BankAccRecLine.SETRANGE("Bank Account No.",BankAccReconciliation."Bank Account No.");
|
||||
BankAccRecLine.SETRANGE("Statement No.",BankAccReconciliation."Statement No.");
|
||||
BankAccRecLine.SETFILTER(Difference,'<\>%1',0);
|
||||
BankAccRecLine.SETRANGE(Type,BankAccRecLine.Type::"Bank Account Ledger Entry");
|
||||
IF BankAccRecLine.FINDSET THEN
|
||||
REPEAT
|
||||
BankAccLedgerEntry.SETRANGE("Bank Account No.",BankAccRecLine."Bank Account No.");
|
||||
BankAccLedgerEntry.SETRANGE(Open,TRUE);
|
||||
BankAccLedgerEntry.SETRANGE("Statement Status",BankAccLedgerEntry."Statement Status"::Open);
|
||||
BankAccLedgerEntry.SETFILTER("Remaining Amount",'<\>%1',0);
|
||||
IF BankAccLedgerEntry.FINDSET THEN
|
||||
REPEAT
|
||||
IF (BankAccRecLine.Difference = BankAccLedgerEntry."Remaining Amount") AND
|
||||
(BankAccRecLine."Transaction Date" = BankAccLedgerEntry."Posting Date") THEN
|
||||
BankAccEntrySetReconNo.ApplyEntries(
|
||||
BankAccRecLine,BankAccLedgerEntry, Relation::"One-to-One");
|
||||
UNTIL BankAccLedgerEntry.NEXT = 0;
|
||||
UNTIL BankAccRecLine.NEXT = 0;
|
||||
END;
|
||||
|
||||
IF BankAccRecLine.FINDSET THEN
|
||||
REPEAT
|
||||
BankAccLedgerEntry.SETRANGE("Bank Account No.",BankAccRecLine."Bank Account No.");
|
||||
BankAccLedgerEntry.SETRANGE(Open,TRUE);
|
||||
BankAccLedgerEntry.SETRANGE("Statement Status",BankAccLedgerEntry."Statement Status"::Open);
|
||||
BankAccLedgerEntry.SETFILTER("Remaining Amount",'<\>%1',0);
|
||||
IF BankAccLedgerEntry.FINDSET THEN
|
||||
REPEAT
|
||||
IF (BankAccRecLine.Difference = BankAccLedgerEntry."Remaining Amount") AND
|
||||
(BankAccRecLine."Transaction Date" = BankAccLedgerEntry."Posting Date") THEN
|
||||
BankAccEntrySetReconNo.ApplyEntries(
|
||||
BankAccRecLine,BankAccLedgerEntry, Relation::"One-to-One");
|
||||
UNTIL BankAccLedgerEntry.NEXT = 0;
|
||||
UNTIL BankAccRecLine.NEXT = 0;
|
||||
END;
|
||||
```
|
||||
|
||||
**Solution Using Query**
|
||||
|
||||
|
|
@ -80,86 +82,90 @@ The new query-based approach involves:
|
|||
2. Loop through the records returned by the query.
|
||||
3. For each query record, decide if it represents a solution and then connect the two table records that formed it through an application.
|
||||
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
BankRecMatchCandidates@1001 : Query 1252;
|
||||
BankAccEntrySetReconNo@1007 : Codeunit 375;
|
||||
BEGIN
|
||||
BankRecMatchCandidates.SETRANGE(Rec\_Line\_Bank\_Account\_No,
|
||||
BankAccReconciliation."Bank Account No.");
|
||||
BankRecMatchCandidates.SETRANGE(Rec\_Line\_Statement\_No,
|
||||
BankAccReconciliation."Statement No.");
|
||||
IF NOT BankRecMatchCandidates.OPEN THEN
|
||||
EXIT;
|
||||
WHILE BankRecMatchCandidates.READ DO BEGIN
|
||||
BankAccLedgerEntry.GET(BankRecMatchCandidates.Entry\_No);
|
||||
BankAccRecLine.GET(BankAccRecLine."Statement Type"::"Bank Reconciliation",
|
||||
BankRecMatchCandidates.Rec\_Line\_Bank\_Account\_No,
|
||||
BankRecMatchCandidates.Rec\_Line\_Statement\_No,
|
||||
BankRecMatchCandidates.Rec\_Line\_Statement\_Line\_No);
|
||||
BankAccEntrySetReconNo.ApplyEntries(BankAccRecLine,BankAccLedgerEntry,
|
||||
Relation::"One-to-One");
|
||||
END;
|
||||
END;
|
||||
BEGIN
|
||||
BankRecMatchCandidates.SETRANGE(Rec_Line_Bank_Account_No,
|
||||
BankAccReconciliation."Bank Account No.");
|
||||
BankRecMatchCandidates.SETRANGE(Rec_Line_Statement_No,
|
||||
BankAccReconciliation."Statement No.");
|
||||
|
||||
IF NOT BankRecMatchCandidates.OPEN THEN
|
||||
EXIT;
|
||||
|
||||
WHILE BankRecMatchCandidates.READ DO BEGIN
|
||||
BankAccLedgerEntry.GET(BankRecMatchCandidates.Entry_No);
|
||||
BankAccRecLine.GET(BankAccRecLine."Statement Type"::"Bank Reconciliation",
|
||||
BankRecMatchCandidates.Rec_Line_Bank_Account_No,
|
||||
BankRecMatchCandidates.Rec_Line_Statement_No,
|
||||
BankRecMatchCandidates.Rec_Line_Statement_Line_No);
|
||||
BankAccEntrySetReconNo.ApplyEntries(BankAccRecLine,BankAccLedgerEntry,
|
||||
Relation::"One-to-One");
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
where the query 1252 is defined as:
|
||||
|
||||
OBJECT Query 1252 Bank Rec. Match Candidates
|
||||
{
|
||||
```al
|
||||
OBJECT Query 1252 Bank Rec. Match Candidates
|
||||
{
|
||||
OBJECT-PROPERTIES
|
||||
{
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
}
|
||||
PROPERTIES
|
||||
{
|
||||
}
|
||||
ELEMENTS
|
||||
{
|
||||
{ 1 ; ;DataItem; ;
|
||||
DataItemTable=Table274;
|
||||
DataItemTableFilter=Difference=FILTER(<\>0),
|
||||
Type=FILTER(=Bank Account Ledger Entry) }
|
||||
{ 2 ;1 ;Column ;Rec\_Line\_Bank\_Account\_No;
|
||||
DataSource=Bank Account No. }
|
||||
{ 3 ;1 ;Column ;Rec\_Line\_Statement\_No;
|
||||
DataSource=Statement No. }
|
||||
{ 4 ;1 ;Column ;Rec\_Line\_Statement\_Line\_No;
|
||||
DataSource=Statement Line No. }
|
||||
{ 5 ;1 ;Column ;Rec\_Line\_Transaction\_Date;
|
||||
DataSource=Transaction Date }
|
||||
{ 6 ;1 ;Column ;Rec\_Line\_Difference ;
|
||||
DataSource=Difference }
|
||||
{ 7 ;1 ;DataItem; ;
|
||||
DataItemTable=Table271;
|
||||
DataItemTableFilter=Remaining Amount=FILTER(<\>0),
|
||||
Open=CONST(Yes),
|
||||
Statement Status=FILTER(Open);
|
||||
DataItemLink=Bank Account No.=Bank\_Acc\_Reconciliation\_Line."Bank Account o.",
|
||||
Remaining Amount=Bank\_Acc\_Reconciliation\_Line.Difference,
|
||||
Posting Date=Bank\_Acc\_Reconciliation\_Line."Transaction Date" }
|
||||
{ 8 ;2 ;Column ; ;
|
||||
DataSource=Entry No. }
|
||||
{ 9 ;2 ;Column ;Bank\_Account\_No ;
|
||||
DataSource=Bank Account No. }
|
||||
{ 10 ;2 ;Column ; ;
|
||||
DataSource=Posting Date }
|
||||
{ 11 ;2 ;Column ; ;
|
||||
DataSource=Remaining Amount }
|
||||
{ 12 ;2 ;Column ;Bank\_Ledger\_Entry\_Open;
|
||||
DataSource=Open }
|
||||
{ 13 ;2 ;Column ; ;
|
||||
DataSource=Statement Status }
|
||||
{ 1 ; ;DataItem; ;
|
||||
DataItemTable=Table274;
|
||||
DataItemTableFilter=Difference=FILTER(<>0),
|
||||
Type=FILTER(=Bank Account Ledger Entry) }
|
||||
{ 2 ;1 ;Column ;Rec_Line_Bank_Account_No;
|
||||
DataSource=Bank Account No. }
|
||||
{ 3 ;1 ;Column ;Rec_Line_Statement_No;
|
||||
DataSource=Statement No. }
|
||||
{ 4 ;1 ;Column ;Rec_Line_Statement_Line_No;
|
||||
DataSource=Statement Line No. }
|
||||
{ 5 ;1 ;Column ;Rec_Line_Transaction_Date;
|
||||
DataSource=Transaction Date }
|
||||
{ 6 ;1 ;Column ;Rec_Line_Difference ;
|
||||
DataSource=Difference }
|
||||
{ 7 ;1 ;DataItem; ;
|
||||
DataItemTable=Table271;
|
||||
DataItemTableFilter=Remaining Amount=FILTER(<\>0),
|
||||
Open=CONST(Yes),
|
||||
Statement Status=FILTER(Open);
|
||||
DataItemLink=Bank Account No.=Bank_Acc_Reconciliation_Line."Bank Account o.",
|
||||
Remaining Amount=Bank_Acc_Reconciliation_Line.Difference,
|
||||
Posting Date=Bank_Acc_Reconciliation_Line."Transaction Date" }
|
||||
{ 8 ;2 ;Column ; ;
|
||||
DataSource=Entry No. }
|
||||
{ 9 ;2 ;Column ;Bank_Account_No ;
|
||||
DataSource=Bank Account No. }
|
||||
{ 10 ;2 ;Column ; ;
|
||||
DataSource=Posting Date }
|
||||
{ 11 ;2 ;Column ; ;
|
||||
DataSource=Remaining Amount }
|
||||
{ 12 ;2 ;Column ;Bank_Ledger_Entry_Open;
|
||||
DataSource=Open }
|
||||
{ 13 ;2 ;Column ; ;
|
||||
DataSource=Statement Status }
|
||||
}
|
||||
CODE
|
||||
{
|
||||
BEGIN
|
||||
END.
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
```
|
||||
|
||||
####
|
||||
When comparing the two implementations, we notice the following advantages of using a query instead of two loops:Comparison
|
||||
|
||||
1. A query produces the Cartesian product of tables 1 and 2 faster than by looping through both of them. The advantage grows as there are more tables linked.
|
||||
|
|
@ -170,11 +176,11 @@ The query object leverages the power of SQL Server (as it basically executes a S
|
|||
2. Using a query only requires one loop, whereas joining two or more tables requires multiple code loops that quickly become difficult to read and follow.
|
||||
3. Queries are easy to create and maintain and generally provide a cleaner design.
|
||||
|
||||
### **NAV Specific Examples**
|
||||
### NAV Specific Examples
|
||||
|
||||
In Microsoft Dynamics NAV 2013 R2, we can see the query object used in the bank account reconciliation matching algorithm. The object is query Bank Rec. Match Candidates query (1252), and it is called by the matching engine in the Match Bank Rec. Lines codeunit (1252).
|
||||
|
||||
### **Ideas for Improvement**
|
||||
### Ideas for Improvement
|
||||
|
||||
The query object type could be improved to allow the passing of parameters at runtime, or, in general, being built dynamically at runtime. This will remove the need for multiple static definitions of the same base query used in slightly different contexts.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue