mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Correct SetLoadFields JIT-load article to match MS docs
The draft claimed accessing an unlisted field "reloads the entire row" per record. Microsoft's partial-records docs say otherwise: the platform does an implicit Get that loads the missing field(s), and in a direct var loop the first JIT updates the enumerator so later iterations do not re-load. The genuine per-row penalty is the pass-by-value case, where the copy's enumerator is not updated. Rewrite the article around JIT loading and the by-value footgun, rename the slug from ...full-reload to ...jit-load, and fix the good/bad samples to demonstrate the by-value repetition accurately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5156c61de
commit
d45068969a
6 changed files with 74 additions and 62 deletions
|
|
@ -1,20 +0,0 @@
|
||||||
codeunit 50132 "LoadFields Bad Sample"
|
|
||||||
{
|
|
||||||
procedure TotalReleasedAmount(): Decimal
|
|
||||||
var
|
|
||||||
SalesHeader: Record "Sales Header";
|
|
||||||
Total: Decimal;
|
|
||||||
begin
|
|
||||||
// "Currency Code" is not in the list. Reading it each iteration forces
|
|
||||||
// a second database round-trip that reloads the WHOLE row — N full
|
|
||||||
// reloads, slower than never calling SetLoadFields at all.
|
|
||||||
SalesHeader.SetLoadFields("Amount Including VAT", Status);
|
|
||||||
if SalesHeader.FindSet() then
|
|
||||||
repeat
|
|
||||||
if (SalesHeader.Status = SalesHeader.Status::Released) and
|
|
||||||
(SalesHeader."Currency Code" = '') then
|
|
||||||
Total += SalesHeader."Amount Including VAT";
|
|
||||||
until SalesHeader.Next() = 0;
|
|
||||||
exit(Total);
|
|
||||||
end;
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
codeunit 50132 "LoadFields Good Sample"
|
|
||||||
{
|
|
||||||
procedure TotalReleasedAmount(): Decimal
|
|
||||||
var
|
|
||||||
SalesHeader: Record "Sales Header";
|
|
||||||
Total: Decimal;
|
|
||||||
begin
|
|
||||||
// Every field read in the loop is listed, so each row stays a cheap
|
|
||||||
// partial load with no hidden second round-trip.
|
|
||||||
SalesHeader.SetLoadFields("Amount Including VAT", Status);
|
|
||||||
if SalesHeader.FindSet() then
|
|
||||||
repeat
|
|
||||||
if SalesHeader.Status = SalesHeader.Status::Released then
|
|
||||||
Total += SalesHeader."Amount Including VAT";
|
|
||||||
until SalesHeader.Next() = 0;
|
|
||||||
exit(Total);
|
|
||||||
end;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
bc-version: [all]
|
|
||||||
domain: performance
|
|
||||||
keywords: [setloadfields, partial-records, just-in-time-load, field-reload, round-trip, lazy-load]
|
|
||||||
technologies: [al]
|
|
||||||
countries: [w1]
|
|
||||||
application-area: [all]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Touching an unlisted field after SetLoadFields forces a full-row reload
|
|
||||||
|
|
||||||
> Contributions welcome — open a PR to refine or extend this article.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`SetLoadFields` loads only the named fields, but the trap is what happens when code later reads a field that was *not* listed: the platform silently issues a second database round-trip and reloads the **entire row** for that record — per record. In a loop, a single overlooked field turns one cheap partial read into N full-row reloads, which is slower than never calling `SetLoadFields` at all. The optimization is only a win if the listed set covers every field touched anywhere downstream, not just in the immediate code block.
|
|
||||||
|
|
||||||
## Best Practice
|
|
||||||
|
|
||||||
Before adding `SetLoadFields`, audit the *whole* access lifecycle of the record variable — every field read in the loop body, in called procedures, in `OnValidate`/`OnAfterGetRecord`, and in anything that receives the record by reference — and list all of them. If you cannot enumerate them confidently (for example the record is passed to code you do not control), prefer not to call `SetLoadFields` rather than risk the reload penalty. See the existing guidance on when partial records pay off (`use-setloadfields-for-partial-records`).
|
|
||||||
|
|
||||||
## Anti Pattern
|
|
||||||
|
|
||||||
Adding `SetLoadFields(Field1, Field2)` at the top of a loop, then reading `Field3` deeper in the body or in a helper. The code compiles and returns correct data, but each iteration pays a hidden full-row reload — the change reads as an optimization while regressing performance. Reviewer signal: a `SetLoadFields` list that does not include every field subsequently referenced through that record variable.
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
codeunit 50132 "LoadFields Bad Sample"
|
||||||
|
{
|
||||||
|
procedure TotalReleasedAmount(): Decimal
|
||||||
|
var
|
||||||
|
SalesHeader: Record "Sales Header";
|
||||||
|
Total: Decimal;
|
||||||
|
begin
|
||||||
|
// "Currency Code" is not listed. The helper takes SalesHeader BY VALUE,
|
||||||
|
// so the copy neither shares the load set nor updates the enumerator:
|
||||||
|
// reading the unlisted field triggers a fresh JIT load (an extra Get)
|
||||||
|
// on EVERY iteration, quietly reversing the saving.
|
||||||
|
SalesHeader.SetLoadFields("Amount Including VAT", Status);
|
||||||
|
if SalesHeader.FindSet() then
|
||||||
|
repeat
|
||||||
|
if IsLocalReleased(SalesHeader) then
|
||||||
|
Total += SalesHeader."Amount Including VAT";
|
||||||
|
until SalesHeader.Next() = 0;
|
||||||
|
exit(Total);
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure IsLocalReleased(SalesHeader: Record "Sales Header"): Boolean
|
||||||
|
begin
|
||||||
|
exit((SalesHeader.Status = SalesHeader.Status::Released) and
|
||||||
|
(SalesHeader."Currency Code" = ''));
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
codeunit 50132 "LoadFields Good Sample"
|
||||||
|
{
|
||||||
|
procedure TotalReleasedAmount(): Decimal
|
||||||
|
var
|
||||||
|
SalesHeader: Record "Sales Header";
|
||||||
|
Total: Decimal;
|
||||||
|
begin
|
||||||
|
// Every field read anywhere downstream is listed — including the one
|
||||||
|
// the by-var helper reads — so no JIT load is ever triggered.
|
||||||
|
SalesHeader.SetLoadFields("Amount Including VAT", Status, "Currency Code");
|
||||||
|
if SalesHeader.FindSet() then
|
||||||
|
repeat
|
||||||
|
if IsLocalReleased(SalesHeader) then
|
||||||
|
Total += SalesHeader."Amount Including VAT";
|
||||||
|
until SalesHeader.Next() = 0;
|
||||||
|
exit(Total);
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure IsLocalReleased(var SalesHeader: Record "Sales Header"): Boolean
|
||||||
|
begin
|
||||||
|
exit((SalesHeader.Status = SalesHeader.Status::Released) and
|
||||||
|
(SalesHeader."Currency Code" = ''));
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: performance
|
||||||
|
keywords: [setloadfields, partial-records, just-in-time-load, jit-load, round-trip, pass-by-value, enumerator]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Reading an unlisted field after SetLoadFields triggers a JIT load
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`SetLoadFields` loads only the named fields, but the trap is what happens when code later reads a field that was *not* listed: the platform silently issues a **just-in-time (JIT) load** — an implicit `Get` that fetches the missing field(s) in a second database round-trip. A single JIT load can erase the saving; the real danger is a JIT that repeats per record. The optimization is only a win if the listed set covers every field touched anywhere downstream, not just in the immediate code block.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Before adding `SetLoadFields`, audit the *whole* access lifecycle of the record variable — every field read in the loop body, in called procedures, in `OnValidate`/`OnAfterGetRecord`, and in anything that receives the record — and list all of them via `SetLoadFields`/`AddLoadFields`. Be especially careful when passing a partial record **by value**: the copy does not share the load set and its enumerator is not updated, so a helper that reads an unlisted field re-triggers the JIT on *every* iteration. Pass by `var` where you can (a JIT then updates the enumerator, so later iterations don't re-load), or call `AddLoadFields` before passing by value. If you cannot enumerate the fields confidently, prefer not to call `SetLoadFields` at all. See the existing guidance on when partial records pay off (`use-setloadfields-for-partial-records`).
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Adding `SetLoadFields(Field1, Field2)` at the top of a loop, then reading `Field3` deeper in the body or inside a by-value helper. The code compiles and returns correct data, but pays a hidden JIT round-trip — and in the by-value case it repeats once per row, quietly reversing the gain. JIT loads also introduce `Inconsistent read` / record-modified race errors that a full non-partial load avoids. Reviewer signal: a `SetLoadFields` list that omits a field later read through that record variable, especially a record passed by value to a procedure that reads a field the caller never listed.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue