Update 6 knowledge articles to align with revised instructions

- performance/use-setloadfields-for-partial-records: Clarify that
  filter-only fields (SetRange/SetFilter) do not need to be listed in
  SetLoadFields — the DB resolves them via the index without hydrating
  the value into AL memory.

- performance/avoid-calcfields-in-loops: Add explicit exception for
  OnAfterGetRecord and OnValidate triggers, which are platform-managed
  and not developer-authored loops.

- performance/split-read-only-and-write-paths-to-avoid-locktable: Add
  ReadIsolation as the primary recommendation for read-only paths;
  LockTable reserved for confirmed write paths only.

- performance/prefer-direct-record-over-recordref: Scope the finding to
  hot unbounded loops (10k+ rows) over ledger-entry-scale tables;
  RecordRef in bounded/admin/setup contexts is not a concern.

- upgrade/enum-changes-must-be-additive-at-the-end: Replace direct
  ObsoleteState = Removed guidance with the two-stage workflow (Pending
  first, Removed later); reference use-obsolete-pending-before-removed.

- upgrade/use-datatransfer-for-large-dataset-initialization: Add the
  >300,000 records threshold as the concrete trigger for requiring
  DataTransfer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-04-24 10:36:08 +02:00
parent b8c543dedb
commit dc14e7bb2a
6 changed files with 21 additions and 4 deletions

View file

@ -17,6 +17,8 @@ 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. 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.
**Acceptable exceptions:** CalcFields inside an `OnAfterGetRecord` page trigger is the standard pattern for displaying computed FlowField values — the platform calls this trigger once per row and it is not a developer-authored loop. Similarly, CalcFields inside an `OnValidate` field trigger fires at most once per user action and is acceptable. The concern is only developer-written `FindSet … repeat … until Next() = 0` loops.
See sample: `avoid-calcfields-in-loops.good.al`. See sample: `avoid-calcfields-in-loops.good.al`.
## Anti Pattern ## Anti Pattern

View file

@ -19,6 +19,8 @@ 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). 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).
Only flag RecordRef usage as a performance concern when it appears inside a **hot, unbounded loop** — typically iterating over ledger-entry-scale tables (10,000+ rows) — where a strongly-typed Record alternative exists. RecordRef in bounded contexts, one-off operations, admin tools, setup helpers, or wizard code is not a performance concern and should not be flagged.
See sample: `prefer-direct-record-over-recordref.good.al`. See sample: `prefer-direct-record-over-recordref.good.al`.
## Anti Pattern ## Anti Pattern

View file

@ -15,7 +15,9 @@ LockTable takes an exclusive write lock on the affected table for the remainder
## Best Practice ## Best Practice
Factor the helper so readers return immediately without a lock and only writers reach the LockTable call. A common pattern: attempt `Rec.Get()` first; if it returns the row, exit with the value; otherwise LockTable and proceed with the Insert. Document the pattern in a comment on the helper so callers understand why the LockTable is inside a branch. For paths that are read-only, prefer `ReadIsolation` over `LockTable`. Setting `Rec.ReadIsolation := IsolationLevel::ReadCommitted` on a record variable gives fine-grained, per-instance control over the isolation level without taking an update lock on the table for the rest of the transaction. Use `LockTable` only for paths that genuinely write to the table.
For helpers that may or may not modify records, factor the code so readers return immediately without a lock and only writers reach the LockTable call. A common pattern: attempt `Rec.Get()` first; if it returns the row, exit with the value; otherwise LockTable and proceed with the Insert. Document the pattern in a comment on the helper so callers understand why the LockTable is inside a branch.
See sample: `split-read-only-and-write-paths-to-avoid-locktable.good.al`. See sample: `split-read-only-and-write-paths-to-avoid-locktable.good.al`.

View file

@ -15,7 +15,9 @@ SetLoadFields instructs the platform to hydrate only the listed fields on a reco
## Best Practice ## Best Practice
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. Call SetLoadFields before FindSet, FindFirst, or Get whenever the code path only reads a subset of fields. List every field that is read or written during the operation, including fields used in calculations and downstream function calls. Omitting a field that is later accessed triggers a second round-trip.
Fields that appear **only** in SetRange or SetFilter calls do not need to be included — the database resolves the filter using the index without hydrating the value into AL memory. Including filter-only fields wastes bandwidth and is not required.
See sample: `use-setloadfields-for-partial-records.good.al`. See sample: `use-setloadfields-for-partial-records.good.al`.

View file

@ -15,7 +15,14 @@ AL enums store their ordinal on disk. Inserting a new value in the middle of an
## Best Practice ## Best Practice
Append new enum values at the end, taking the next free ordinal. When a value must be retired, mark it with `ObsoleteState = Removed`, `ObsoleteReason`, and `ObsoleteTag` so tooling and downstream code can detect the deprecation; do not reclaim the ordinal. Renaming the caption on an existing ordinal is fine. Append new enum values at the end, taking the next free ordinal. Renaming the caption on an existing ordinal is fine.
When a value must be retired, follow the two-stage obsoletion workflow:
1. **First release:** Mark the value with `ObsoleteState = Pending`, `ObsoleteReason`, and `ObsoleteTag`. This gives callers at least one release cycle to migrate.
2. **Later release:** Advance to `ObsoleteState = Removed` once all callers have been updated.
Never skip straight to `ObsoleteState = Removed` without first going through `Pending` — doing so removes the warning cycle that callers depend on. Do not reclaim the ordinal in either stage. See also: `use-obsolete-pending-before-removed.md`.
See sample: `enum-changes-must-be-additive-at-the-end.good.al`. See sample: `enum-changes-must-be-additive-at-the-end.good.al`.

View file

@ -15,7 +15,9 @@ An upgrade that populates a new field on millions of existing rows with a FindSe
## Best Practice ## Best Practice
Use DataTransfer for field-default initialization on existing tables, especially when the target is a ledger-entry or document-line table. Set tables, add source filters, add constant values, call CopyFields, clear, and repeat for additional slices. When trigger or subscriber behaviour is required, do that work separately against a filtered result set so the bulk update remains set-based. Use DataTransfer when initializing a new field on an existing table that **can contain more than 300,000 records**, or whenever a new field is added to an existing table and the initialization must run across all existing rows. Tables in the ledger-entry and document-line category reliably exceed this threshold; treat them as requiring DataTransfer by default.
Set tables, add source filters, add constant values, call CopyFields, clear, and repeat for additional slices. When trigger or subscriber behaviour is required, do that work separately against a filtered result set so the bulk update remains set-based.
See sample: `use-datatransfer-for-large-dataset-initialization.good.al`. See sample: `use-datatransfer-for-large-dataset-initialization.good.al`.