mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Co-locate AL samples next to their knowledge articles
The /samples/ top-level tree is replaced with sibling files in each
knowledge-layer folder. An article and its demonstrations now live
side-by-side:
microsoft/knowledge/<domain>/<slug>.md
microsoft/knowledge/<domain>/<slug>.good.al
microsoft/knowledge/<domain>/<slug>.bad.al
Rationale:
- Proximity. An article and its paired samples are one unit; the
filesystem now reflects that.
- Layer ownership. Samples inherit layer precedence for free -- a
/custom/ fork can override an article and its samples atomically,
which the shared /samples/ tree previously made awkward.
- Trivial migration path. Action-skill source globs
(*/knowledge/<domain>/**/*.md) are unchanged; sample discovery is a
sibling-filename lookup.
Changes:
- git mv of all 65 sample files from samples/<domain>/<slug>/{bad,good}.al
to microsoft/knowledge/<domain>/<slug>.{bad,good}.al (history preserved).
- Update See-sample references in all 37 articles that ship samples.
- skills/read.md: replace the no-code-blocks bullet with a pointer to a
new Sample files section that fully specifies the sibling convention,
the kinds (good/bad + forward-compatible), multi-technology rules,
demonstration-only status, and layer-precedence behaviour.
- skills/write.md: update the samples pointer to match.
- README.md: annotate the knowledge tree with the sample sibling shape.
- samples/README.md deleted; content lifted into skills/read.md.
- Both generators (C:\temp\gen_performance_knowledge.py,
C:\temp\gen_security_knowledge.py) updated to emit at the new paths
and to stop writing samples/README.md. Re-running them is idempotent
against the committed layout.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
0980397d27
commit
62dabf9a11
106 changed files with 89 additions and 98 deletions
|
|
@ -0,0 +1,10 @@
|
|||
tableextension 50118 "Perf Sample SIFTKey" extends "Cust. Ledger Entry"
|
||||
{
|
||||
keys
|
||||
{
|
||||
key(PerfSampleOpenByCustomer; "Customer No.", Open, "Posting Date")
|
||||
{
|
||||
SumIndexFields = "Remaining Amt. (LCY)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ CodeCop rule AA0232 checks that FlowFields backed by CalcSums or aggregation Cal
|
|||
|
||||
For each Sum-style FlowField, ensure the source table has a key whose leading fields match the FlowField's CalcFormula WHERE clause and whose SumIndexFields list includes the summed field. Table extensions adding new FlowFields are responsible for adding the supporting key.
|
||||
|
||||
See sample: `samples/performance/add-sift-keys-for-flowfields/good.al`.
|
||||
See sample: `add-sift-keys-for-flowfields.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
codeunit 50117 "Perf Sample CalcFieldsInLoop Bad"
|
||||
{
|
||||
procedure ProcessLargeLines(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
|
||||
SalesLine.SetRange("Document No.", SalesHeader."No.");
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
SalesHeader.CalcFields(Amount);
|
||||
if SalesHeader.Amount > 1000 then
|
||||
ProcessLine(SalesLine);
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure ProcessLine(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
codeunit 50116 "Perf Sample CalcFieldsInLoop Good"
|
||||
{
|
||||
procedure ProcessLargeLines(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
SalesHeader.CalcFields(Amount);
|
||||
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
|
||||
SalesLine.SetRange("Document No.", SalesHeader."No.");
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
if SalesHeader.Amount > 1000 then
|
||||
ProcessLine(SalesLine);
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure ProcessLine(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ CalcFields evaluates one or more FlowFields for the current record by issuing a
|
|||
|
||||
Move CalcFields out of the iteration. If the total is what you need, use CalcSums on the filtered parent set. If row-by-row FlowField values are needed, reshape the computation so the aggregate runs once — for example by joining against a temporary table populated in a single batched query.
|
||||
|
||||
See sample: `samples/performance/avoid-calcfields-in-loops/good.al`.
|
||||
See sample: `avoid-calcfields-in-loops.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling CalcFields inside `repeat ... until Next() = 0` on a hot parent record is the textbook N+1 pattern. Even a modest parent set size (hundreds of rows) turns into thousands of round-trips.
|
||||
|
||||
See sample: `samples/performance/avoid-calcfields-in-loops/bad.al`.
|
||||
See sample: `avoid-calcfields-in-loops.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 50129 "Perf Sample CommitInLoop Bad"
|
||||
{
|
||||
procedure ReleaseAllOrders()
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
begin
|
||||
SalesHeader.SetRange(Status, SalesHeader.Status::Open);
|
||||
if SalesHeader.FindSet() then
|
||||
repeat
|
||||
SalesHeader.Status := SalesHeader.Status::Released;
|
||||
SalesHeader.Modify();
|
||||
Commit();
|
||||
until SalesHeader.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -23,5 +23,5 @@ If the batch is large enough that a single transaction is untenable, process it
|
|||
|
||||
Placing Commit inside `repeat ... until Next() = 0` is almost always a mistake: it is unusual for the correctness of the operation to depend on per-row commits, and the cost of starting a new transaction on every row dominates the work.
|
||||
|
||||
See sample: `samples/performance/avoid-commit-inside-loops/bad.al`.
|
||||
See sample: `avoid-commit-inside-loops.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50105 "Perf Sample AvoidFindFirstNext Bad"
|
||||
{
|
||||
procedure EmitAllItems(var Item: Record Item)
|
||||
begin
|
||||
if Item.FindFirst() then
|
||||
repeat
|
||||
EmitItem(Item);
|
||||
until Item.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure EmitItem(var Item: Record Item)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -23,5 +23,5 @@ Choose the Find variant that matches the operation: FindSet for full iteration,
|
|||
|
||||
Writing `if Rec.FindFirst() then repeat ... until Rec.Next() = 0` is the canonical AA0233 offender. The loop wastes bandwidth and obscures the author's intent.
|
||||
|
||||
See sample: `samples/performance/avoid-findfirst-with-next/bad.al`.
|
||||
See sample: `avoid-findfirst-with-next.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50127 "Perf Sample UserInTxn Bad"
|
||||
{
|
||||
procedure ArchiveSalesHeader(var SalesHeader: Record "Sales Header")
|
||||
begin
|
||||
SalesHeader.Status := SalesHeader.Status::Released;
|
||||
SalesHeader.Modify();
|
||||
if not Confirm('Archive document %1?', false, SalesHeader."No.") then
|
||||
exit;
|
||||
SalesHeader.Delete(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50126 "Perf Sample UserInTxn Good"
|
||||
{
|
||||
procedure ArchiveSalesHeader(var SalesHeader: Record "Sales Header")
|
||||
begin
|
||||
if not Confirm('Archive document %1?', false, SalesHeader."No.") then
|
||||
exit;
|
||||
DoArchive(SalesHeader);
|
||||
end;
|
||||
|
||||
local procedure DoArchive(var SalesHeader: Record "Sales Header")
|
||||
begin
|
||||
// only Insert/Modify/Delete calls happen here; no prompts
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ Confirm, StrMenu, Message, and any other user-facing dialog pauses execution whi
|
|||
|
||||
Gather every user decision before the writing phase begins. Once the decisions are known, run the transaction end-to-end without prompts.
|
||||
|
||||
See sample: `samples/performance/avoid-user-interaction-in-transactions/good.al`.
|
||||
See sample: `avoid-user-interaction-in-transactions.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling Confirm or StrMenu from inside an OnInsert, OnModify, or OnDelete trigger — or from any code path that has already started modifying records — blocks on user input while holding locks.
|
||||
|
||||
See sample: `samples/performance/avoid-user-interaction-in-transactions/bad.al`.
|
||||
See sample: `avoid-user-interaction-in-transactions.bad.al`.
|
||||
|
||||
|
|
|
|||
16
microsoft/knowledge/performance/filter-before-find.bad.al
Normal file
16
microsoft/knowledge/performance/filter-before-find.bad.al
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50101 "Perf Sample FilterBeforeFind Bad"
|
||||
{
|
||||
procedure ProcessUsCustomers(var Customer: Record Customer)
|
||||
begin
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
if Customer."Country/Region Code" = 'US' then
|
||||
ProcessCustomer(Customer);
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure ProcessCustomer(var Customer: Record Customer)
|
||||
begin
|
||||
// per-customer work
|
||||
end;
|
||||
}
|
||||
16
microsoft/knowledge/performance/filter-before-find.good.al
Normal file
16
microsoft/knowledge/performance/filter-before-find.good.al
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50100 "Perf Sample FilterBeforeFind Good"
|
||||
{
|
||||
procedure ProcessUsCustomers(var Customer: Record Customer)
|
||||
begin
|
||||
Customer.SetRange("Country/Region Code", 'US');
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
ProcessCustomer(Customer);
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure ProcessCustomer(var Customer: Record Customer)
|
||||
begin
|
||||
// per-customer work
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ Every call to FindSet, Find, or FindFirst on an unfiltered record variable scans
|
|||
|
||||
Apply SetRange or SetFilter to narrow the record set before calling FindSet or Find. The filters should match a key on the table (see set-current-key-to-match-filters). When iterating rows that belong to a parent record, set all key-field filters before the find call — never inside the repeat loop.
|
||||
|
||||
See sample: `samples/performance/filter-before-find/good.al`.
|
||||
See sample: `filter-before-find.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling FindSet with no filters and then discarding rows inside the loop with an if-statement forces the platform to read every row of the table before your code even runs.
|
||||
|
||||
See sample: `samples/performance/filter-before-find/bad.al`.
|
||||
See sample: `filter-before-find.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50140 "Perf Sample Subscriber Bad"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterValidateEvent', 'No.', false, false)]
|
||||
local procedure HeavyWorkOnSalesLineNo(var Rec: Record "Sales Line"; var xRec: Record "Sales Line")
|
||||
var
|
||||
HttpClient: HttpClient;
|
||||
HttpResponse: HttpResponseMessage;
|
||||
begin
|
||||
// synchronous external call on a hot event
|
||||
HttpClient.Get('https://example.com/validate?no=' + Rec."No.", HttpResponse);
|
||||
end;
|
||||
}
|
||||
|
|
@ -23,5 +23,5 @@ Keep subscribers small: guard early with inexpensive checks, defer heavy work to
|
|||
|
||||
Calling an external web service, running a report, or iterating a large table from inside an event subscriber on a hot publisher makes every operation on that publisher as slow as the heaviest subscriber.
|
||||
|
||||
See sample: `samples/performance/keep-event-subscribers-lightweight/bad.al`.
|
||||
See sample: `keep-event-subscribers-lightweight.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
codeunit 50128 "Perf Sample TxnScope Bad"
|
||||
{
|
||||
procedure ImportCustomers(var Source: List of [Text])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
HttpClient: HttpClient;
|
||||
HttpResponse: HttpResponseMessage;
|
||||
Row: Text;
|
||||
begin
|
||||
foreach Row in Source do begin
|
||||
// external call inside the write transaction
|
||||
HttpClient.Get('https://example.com/validate?row=' + Row, HttpResponse);
|
||||
Customer.Init();
|
||||
// ... populate from Row ...
|
||||
Customer.Insert(true);
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
codeunit 50123 "Perf Sample TxnScope Good"
|
||||
{
|
||||
procedure ImportCustomers(var Source: List of [Text])
|
||||
var
|
||||
Prepared: Record Customer temporary;
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
// read, validate, and shape outside the transaction
|
||||
PrepareRows(Source, Prepared);
|
||||
|
||||
// transaction starts here: only Insert/Modify calls
|
||||
if Prepared.FindSet() then
|
||||
repeat
|
||||
Customer := Prepared;
|
||||
Customer.Insert(true);
|
||||
until Prepared.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure PrepareRows(var Source: List of [Text]; var Prepared: Record Customer temporary)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ Every write operation runs inside a transaction that holds locks until the trans
|
|||
|
||||
Perform data reads, calculations, and external integrations outside the transaction whenever possible. Enter the writing phase with all inputs computed, execute the minimum set of Insert, Modify, and Delete calls, and exit. If you have a long-running batch, split it into checkpoints at safe boundaries (see avoid-commit-inside-loops).
|
||||
|
||||
See sample: `samples/performance/keep-transaction-scope-short/good.al`.
|
||||
See sample: `keep-transaction-scope-short.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Opening a transaction, then performing external web-service calls, heavy report runs, or user-facing dialogs while the locks are held, suspends every other session that needs the same rows for as long as the external operation takes.
|
||||
|
||||
See sample: `samples/performance/keep-transaction-scope-short/bad.al`.
|
||||
See sample: `keep-transaction-scope-short.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50107 "Perf Sample OnlyFetchUsed Bad"
|
||||
{
|
||||
procedure CustomerHasEntries(CustomerNo: Code[20]): Boolean
|
||||
var
|
||||
CustLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
|
||||
exit(CustLedgerEntry.FindSet());
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50106 "Perf Sample OnlyFetchUsed Good"
|
||||
{
|
||||
procedure CustomerHasEntries(CustomerNo: Code[20]): Boolean
|
||||
var
|
||||
CustLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
|
||||
exit(not CustLedgerEntry.IsEmpty());
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ CodeCop rule AA0175 flags code that retrieves a record and then does not use it.
|
|||
|
||||
Retrieve a record only when you need one or more of its field values. When you only need to know whether at least one row matches a filter, use IsEmpty (see use-isempty-for-existence-checks). When you only need a subset of fields, use SetLoadFields (see use-setloadfields-for-partial-records).
|
||||
|
||||
See sample: `samples/performance/only-fetch-records-you-use/good.al`.
|
||||
See sample: `only-fetch-records-you-use.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling FindSet or Get and then ignoring the result, or using it only as a boolean existence test, performs the full fetch and throws the data away.
|
||||
|
||||
See sample: `samples/performance/only-fetch-records-you-use/bad.al`.
|
||||
See sample: `only-fetch-records-you-use.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
codeunit 50135 "Perf Sample RecordRef Bad"
|
||||
{
|
||||
procedure BlockCustomer(CustomerNo: Code[20])
|
||||
var
|
||||
RecRef: RecordRef;
|
||||
PkRef: KeyRef;
|
||||
NoRef: FieldRef;
|
||||
BlockedRef: FieldRef;
|
||||
begin
|
||||
RecRef.Open(Database::Customer);
|
||||
PkRef := RecRef.KeyIndex(1);
|
||||
NoRef := PkRef.FieldIndex(1);
|
||||
NoRef.SetRange(CustomerNo);
|
||||
if not RecRef.FindFirst() then
|
||||
exit;
|
||||
BlockedRef := RecRef.Field(54);
|
||||
BlockedRef.Value(2);
|
||||
RecRef.Modify(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50134 "Perf Sample RecordRef Good"
|
||||
{
|
||||
procedure BlockCustomer(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if not Customer.Get(CustomerNo) then
|
||||
exit;
|
||||
Customer.Blocked := Customer.Blocked::All;
|
||||
Customer.Modify(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ RecordRef and FieldRef are the platform's reflection API: they work across table
|
|||
|
||||
Use Record variables for code paths that target a known table. Reach for RecordRef and FieldRef only when the table is genuinely dynamic (generic export/import, field-agnostic utilities, cross-table integrations).
|
||||
|
||||
See sample: `samples/performance/prefer-direct-record-over-recordref/good.al`.
|
||||
See sample: `prefer-direct-record-over-recordref.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Using RecordRef as a habit, even when the target table is hardcoded two lines earlier, costs performance and hides intent from reviewers.
|
||||
|
||||
See sample: `samples/performance/prefer-direct-record-over-recordref/bad.al`.
|
||||
See sample: `prefer-direct-record-over-recordref.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50131 "Perf Sample GetVsFind Bad"
|
||||
{
|
||||
procedure CustomerName(CustomerNo: Code[20]): Text[100]
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
Customer.SetRange("No.", CustomerNo);
|
||||
if Customer.FindFirst() then
|
||||
exit(Customer.Name);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50130 "Perf Sample GetVsFind Good"
|
||||
{
|
||||
procedure CustomerName(CustomerNo: Code[20]): Text[100]
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if Customer.Get(CustomerNo) then
|
||||
exit(Customer.Name);
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ Get is a direct primary-key lookup: one index seek, one row, done. FindFirst wit
|
|||
|
||||
When the complete primary key is known, call Get. Use FindFirst only for non-primary-key lookups or when the filter is a partial prefix of the key.
|
||||
|
||||
See sample: `samples/performance/prefer-get-for-primary-key-lookups/good.al`.
|
||||
See sample: `prefer-get-for-primary-key-lookups.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Setting one SetRange per primary-key field and then calling FindFirst reproduces Get with more typing and slightly worse performance.
|
||||
|
||||
See sample: `samples/performance/prefer-get-for-primary-key-lookups/bad.al`.
|
||||
See sample: `prefer-get-for-primary-key-lookups.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 50122 "Perf Sample SetCurrentKey Good"
|
||||
{
|
||||
procedure LinesForDocument(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
SalesLine.SetCurrentKey("Document Type", "Document No.", "Line No.");
|
||||
SalesLine.SetRange("Document Type", DocumentType);
|
||||
SalesLine.SetRange("Document No.", DocumentNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ AL chooses a key for a Find call based on the current SetCurrentKey selection. W
|
|||
|
||||
Call SetCurrentKey with the fields you filter and sort on, in the order they appear in a table key. If no suitable key exists, add one via a table extension rather than relying on an unsupported filter pattern.
|
||||
|
||||
See sample: `samples/performance/set-current-key-to-match-filters/good.al`.
|
||||
See sample: `set-current-key-to-match-filters.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
report 50112 "Perf Sample AddLoadFields Good"
|
||||
{
|
||||
dataset
|
||||
{
|
||||
dataitem(Cust; "Cust. Ledger Entry")
|
||||
{
|
||||
column(CustomerNo; "Customer No.") { }
|
||||
column(PostingDate; "Posting Date") { }
|
||||
column(Amount; Amount) { }
|
||||
|
||||
trigger OnPreDataItem()
|
||||
begin
|
||||
AddLoadFields("Customer No.", "Posting Date", Amount);
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ Reports iterate a dataitem's record automatically; the developer does not contro
|
|||
|
||||
In each dataitem's OnPreDataItem trigger, call AddLoadFields for every field used by the layout, by the dataitem's triggers, and by any code that runs in the row-level event hooks. If the layout uses a FlowField, also ensure CalcFields is called and that the underlying key is loaded (see add-sift-keys-for-flowfields).
|
||||
|
||||
See sample: `samples/performance/use-addloadfields-in-report-layouts/good.al`.
|
||||
See sample: `use-addloadfields-in-report-layouts.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50115 "Perf Sample CalcSums Bad"
|
||||
{
|
||||
procedure OutstandingForCustomer(CustomerNo: Code[20]) Total: Decimal
|
||||
var
|
||||
CustLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
|
||||
CustLedgerEntry.SetRange(Open, true);
|
||||
if CustLedgerEntry.FindSet() then
|
||||
repeat
|
||||
Total += CustLedgerEntry."Remaining Amt. (LCY)";
|
||||
until CustLedgerEntry.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50114 "Perf Sample CalcSums Good"
|
||||
{
|
||||
procedure OutstandingForCustomer(CustomerNo: Code[20]): Decimal
|
||||
var
|
||||
CustLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
|
||||
CustLedgerEntry.SetRange(Open, true);
|
||||
CustLedgerEntry.CalcSums("Remaining Amt. (LCY)");
|
||||
exit(CustLedgerEntry."Remaining Amt. (LCY)");
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ When the task is to compute a sum over a filtered set, CalcSums lets the platfor
|
|||
|
||||
Set the required filters on the record, then call CalcSums on the field you want aggregated. Ensure the table has a key whose SumIndexFields includes the summed field and whose key prefix matches the filters (see add-sift-keys-for-flowfields).
|
||||
|
||||
See sample: `samples/performance/use-calcsums-for-flowfield-totals/good.al`.
|
||||
See sample: `use-calcsums-for-flowfield-totals.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Looping a filtered set with FindSet and adding a field to an accumulator on every iteration performs work in AL that SQL already knows how to do in one aggregate query.
|
||||
|
||||
See sample: `samples/performance/use-calcsums-for-flowfield-totals/bad.al`.
|
||||
See sample: `use-calcsums-for-flowfield-totals.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50109 "Perf Sample FindSetReadonly Bad"
|
||||
{
|
||||
procedure SumInvoiceLines(var SalesLine: Record "Sales Line") Total: Decimal
|
||||
begin
|
||||
if SalesLine.FindSet(true) then
|
||||
repeat
|
||||
Total += SalesLine."Line Amount";
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50108 "Perf Sample FindSetReadonly Good"
|
||||
{
|
||||
procedure SumInvoiceLines(var SalesLine: Record "Sales Line") Total: Decimal
|
||||
begin
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
Total += SalesLine."Line Amount";
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ FindSet has two modes: FindSet() and FindSet(false) are read-only and take no wr
|
|||
|
||||
Call FindSet with no arguments when the loop only reads field values. Pass `true` only when the same loop is expected to call Modify, Delete, or Rename on the record, and the correctness of the operation depends on the table being locked for the full iteration.
|
||||
|
||||
See sample: `samples/performance/use-findset-readonly-by-default/good.al`.
|
||||
See sample: `use-findset-readonly-by-default.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Writing FindSet(true) reflexively for every iteration forces the platform to take a LockTable on every call, even when the loop only reads values. The older two-parameter signature `FindSet(ForUpdate, UpdateKey)` is obsolete and must not be used.
|
||||
|
||||
See sample: `samples/performance/use-findset-readonly-by-default/bad.al`.
|
||||
See sample: `use-findset-readonly-by-default.bad.al`.
|
||||
|
||||
|
|
|
|||
10
microsoft/knowledge/performance/use-findset-with-next.bad.al
Normal file
10
microsoft/knowledge/performance/use-findset-with-next.bad.al
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50103 "Perf Sample FindSetWithNext Bad"
|
||||
{
|
||||
procedure SumLineAmounts(var SalesLine: Record "Sales Line") Total: Decimal
|
||||
begin
|
||||
if SalesLine.FindFirst() then
|
||||
repeat
|
||||
Total += SalesLine."Line Amount";
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50102 "Perf Sample FindSetWithNext Good"
|
||||
{
|
||||
procedure SumLineAmounts(var SalesLine: Record "Sales Line") Total: Decimal
|
||||
begin
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
Total += SalesLine."Line Amount";
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ When iterating over a filtered set of records with repeat-until, use FindSet tog
|
|||
|
||||
Call FindSet to start the iteration and Next to advance. Guard the loop with the standard `if FindSet() then ... until Next() = 0` idiom so callers can still handle the empty-set case.
|
||||
|
||||
See sample: `samples/performance/use-findset-with-next/good.al`.
|
||||
See sample: `use-findset-with-next.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Starting a repeat-until loop with FindFirst or FindLast reads only one row and then calls Next on an iterator that was not intended for full-set traversal. The platform pays extra work to fetch the single row and the loop silhouette is misleading to reviewers.
|
||||
|
||||
See sample: `samples/performance/use-findset-with-next/bad.al`.
|
||||
See sample: `use-findset-with-next.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50132 "Perf Sample InsertParam Good"
|
||||
{
|
||||
procedure BulkLoadTempItems(var TempItem: Record Item temporary; Source: List of [Code[20]])
|
||||
var
|
||||
ItemNo: Code[20];
|
||||
begin
|
||||
foreach ItemNo in Source do begin
|
||||
TempItem."No." := ItemNo;
|
||||
TempItem.Insert(false);
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ Insert, Modify, and Delete accept a boolean that controls whether the table's On
|
|||
|
||||
Call Insert(true), Modify(true), or Delete(true) when the table's trigger logic is part of the operation's semantics. Call Insert(false), Modify(false), or Delete(false) when the operation is bulk data movement or temporary-table manipulation and the trigger would duplicate work or fire invalid side effects.
|
||||
|
||||
See sample: `samples/performance/use-insert-false-when-skipping-triggers/good.al`.
|
||||
See sample: `use-insert-false-when-skipping-triggers.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50121 "Perf Sample IsEmpty Bad"
|
||||
{
|
||||
procedure HasOpenDocuments(CustomerNo: Code[20]): Boolean
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
begin
|
||||
SalesHeader.SetRange("Sell-to Customer No.", CustomerNo);
|
||||
SalesHeader.SetRange(Status, SalesHeader.Status::Open);
|
||||
exit(SalesHeader.Count() > 0);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50120 "Perf Sample IsEmpty Good"
|
||||
{
|
||||
procedure HasOpenDocuments(CustomerNo: Code[20]): Boolean
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
begin
|
||||
SalesHeader.SetRange("Sell-to Customer No.", CustomerNo);
|
||||
SalesHeader.SetRange(Status, SalesHeader.Status::Open);
|
||||
exit(not SalesHeader.IsEmpty());
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ IsEmpty is the cheapest way to answer whether at least one row matches the curre
|
|||
|
||||
Use `if not Rec.IsEmpty() then ...` for existence checks. Reserve Count for cases where the exact number of rows is needed, and FindFirst for cases where you actually want the row's field values.
|
||||
|
||||
See sample: `samples/performance/use-isempty-for-existence-checks/good.al`.
|
||||
See sample: `use-isempty-for-existence-checks.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`if Rec.Count() > 0` iterates the whole set just to answer a yes/no question. `if Rec.FindFirst() then` loads an entire row of data the caller never reads.
|
||||
|
||||
See sample: `samples/performance/use-isempty-for-existence-checks/bad.al`.
|
||||
See sample: `use-isempty-for-existence-checks.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50111 "Perf Sample SetLoadFields Bad"
|
||||
{
|
||||
procedure ExportItemNumbers(var Item: Record Item)
|
||||
begin
|
||||
if Item.FindSet() then
|
||||
repeat
|
||||
Export(Item."No.", Item.Description);
|
||||
until Item.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure Export(ItemNo: Code[20]; Description: Text[100])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 50110 "Perf Sample SetLoadFields Good"
|
||||
{
|
||||
procedure ExportItemNumbers(var Item: Record Item)
|
||||
begin
|
||||
Item.SetLoadFields("No.", Description);
|
||||
if Item.FindSet() then
|
||||
repeat
|
||||
Export(Item."No.", Item.Description);
|
||||
until Item.Next() = 0;
|
||||
end;
|
||||
|
||||
local procedure Export(ItemNo: Code[20]; Description: Text[100])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ SetLoadFields instructs the platform to hydrate only the listed fields on a reco
|
|||
|
||||
Call SetLoadFields before FindSet, FindFirst, or Get whenever the code path only reads a subset of fields. List every field that is read during the operation, including fields used in filters, calculations, and downstream function calls. Omitting a field that is later accessed triggers a second round-trip.
|
||||
|
||||
See sample: `samples/performance/use-setloadfields-for-partial-records/good.al`.
|
||||
See sample: `use-setloadfields-for-partial-records.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Iterating a large set and reading only two or three fields without SetLoadFields forces the platform to transport every column for every row, including BLOBs and unused text fields.
|
||||
|
||||
See sample: `samples/performance/use-setloadfields-for-partial-records/bad.al`.
|
||||
See sample: `use-setloadfields-for-partial-records.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
codeunit 50138 "Perf Sample SingleInstance Good"
|
||||
{
|
||||
SingleInstance = true;
|
||||
|
||||
var
|
||||
Cached: Record "Sales & Receivables Setup";
|
||||
Loaded: Boolean;
|
||||
|
||||
procedure GetSetup(): Record "Sales & Receivables Setup"
|
||||
begin
|
||||
if not Loaded then begin
|
||||
Cached.Get();
|
||||
Loaded := true;
|
||||
end;
|
||||
exit(Cached);
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ A SingleInstance codeunit lives once per session. Variables on it survive across
|
|||
|
||||
Store long-lived, read-often, rarely-changing data on a SingleInstance codeunit, populated lazily on first access. Keep the cached footprint small: a handful of booleans, a setup record, a few derived values. Be explicit about invalidation if the source can change during the session.
|
||||
|
||||
See sample: `samples/performance/use-single-instance-codeunits-for-caching/good.al`.
|
||||
See sample: `use-single-instance-codeunits-for-caching.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
codeunit 50137 "Perf Sample StrSubstNo Bad"
|
||||
{
|
||||
procedure CustomerGreeting(var Customer: Record Customer): Text
|
||||
begin
|
||||
exit('Hello, ' + Customer.Name + ' (' + Customer."No." + ')');
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 50136 "Perf Sample StrSubstNo Good"
|
||||
{
|
||||
procedure CustomerGreeting(var Customer: Record Customer): Text
|
||||
var
|
||||
GreetingLbl: Label 'Hello, %1 (%2)';
|
||||
begin
|
||||
exit(StrSubstNo(GreetingLbl, Customer.Name, Customer."No."));
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ StrSubstNo formats values into a placeholder template in a single call. Manual c
|
|||
|
||||
Declare the template as a Label (so it can be localized) and format with StrSubstNo. Pass values in the order the placeholders expect; StrSubstNo handles locale-sensitive conversions consistently.
|
||||
|
||||
See sample: `samples/performance/use-strsubstno-for-message-formatting/good.al`.
|
||||
See sample: `use-strsubstno-for-message-formatting.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Building a user-facing string by concatenating record field values with string literals ignores locale rules and allocates more than necessary.
|
||||
|
||||
See sample: `samples/performance/use-strsubstno-for-message-formatting/bad.al`.
|
||||
See sample: `use-strsubstno-for-message-formatting.bad.al`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50124 "Perf Sample TempTable Good"
|
||||
{
|
||||
procedure BuildAffectedItems(var TempItem: Record Item temporary)
|
||||
var
|
||||
SalesLine: Record "Sales Line";
|
||||
begin
|
||||
TempItem.Reset();
|
||||
TempItem.DeleteAll();
|
||||
SalesLine.SetRange(Type, SalesLine.Type::Item);
|
||||
if SalesLine.FindSet() then
|
||||
repeat
|
||||
TempItem."No." := SalesLine."No.";
|
||||
if TempItem.Insert(false) then;
|
||||
until SalesLine.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ Temporary tables live in memory, not in SQL. They are the correct primary data s
|
|||
|
||||
Declare the record variable with `temporary` when the data is scratch. Populate it with Insert(false) to avoid firing triggers. Clear the table explicitly with DeleteAll when the variable's scope is long-lived (a SingleInstance codeunit or a reused session variable) and needs to be reset between uses.
|
||||
|
||||
See sample: `samples/performance/use-temporary-tables-for-intermediate-data/good.al`.
|
||||
See sample: `use-temporary-tables-for-intermediate-data.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue