You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: explain LockTable in table triggers and the convert-or-delete choice for LC0031 (#185)
* docs: explain LockTable in table triggers and the convert-or-delete choice for LC0031
Rewrite the LC0031 page with the mechanism-first voice: why LockTable
is transaction-wide, what ReadIsolation changes, and how tri-state
locking falls back. Add Convert or delete (BCQuality criterion),
LockTable in table triggers (the reporter's question), and Code fix
sections. Add Microsoft Learn blockquotes and BCQuality/Mads Gram
references.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: correct the LC0031 BCQuality links and complete the trigger example pairs
Point the BCQuality references at the microsoft/BCQuality knowledge
folder, give both trigger shapes a bad and a fixed example with the
standard diagnostic comment, and fix the highlighted lines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/analyzers/LinterCop/LC0031.md
+94-3Lines changed: 94 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,11 +10,21 @@ linkTitle = 'LC0031'
10
10
ignoreObsolete = true
11
11
+++
12
12
13
-
`LockTable()`is the traditional way to acquire an update lock before reading or modifying records. The problem is that it modifies **global session state**: once called on any record variable, every subsequent read against that table — on *any* variable instance — acquires an update lock for the remainder of the transaction. An event subscriber, a FlowField calculation, or unrelated code further down the call stack can end up locking rows it never intended to touch.
13
+
`LockTable()`does not acquire a lock. It marks the table so that every subsequent read of that table in the transaction, on *any*record variable, uses the SQL `UPDLOCK` hint until the transaction commits. An event subscriber, a FlowField calculation, or unrelated code further down the call stack ends up locking rows it never intended to touch.
14
14
15
-
`ReadIsolation`, introduced in Business Central 2023 wave 1 (v22), sets the isolation level on the **specific record variable** only. Other variables of the same table remain unaffected, keeping lock scope tight and predictable.
15
+
> If `Record.LockTable` is called on an `Item` record, all reads against that table will be done with the `UPDLOCK` hint, not just the variable it was called on.
16
16
17
-
There is another reason to make the switch: **tri-state locking** (default from v25 onward) falls back to pessimistic two-state locking the moment `LockTable()` is called anywhere in the transaction. Replacing `LockTable()` with `ReadIsolation` preserves the tri-state benefits — fewer locks, higher concurrency, and fewer lock timeouts across the system.
17
+
— [Performance Articles for Developers](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/performance/performance-developer) on Microsoft Learn
18
+
19
+
`ReadIsolation`, introduced in Business Central 2023 wave 1 (v22), sets the isolation level on the **specific record variable** only. Other variables of the same table remain unaffected. The two are not equivalent, and that difference is the point: `ReadIsolation` keeps lock scope tight and predictable.
20
+
21
+
There is a second reason to make the switch. [Tri-state locking](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-tri-state-locking) (default from v25 onward) performs reads after writes optimistically, and a single `LockTable()` switches the table back to pessimistic locking for the rest of the transaction:
22
+
23
+
> Explicitly using the LockTable method in code maintains the same behavior, disabling optimistic reads.
24
+
25
+
— [Tri-state locking in database](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-tri-state-locking) on Microsoft Learn
26
+
27
+
Replacing `LockTable()` with `ReadIsolation` preserves the tri-state benefits: fewer locks, higher concurrency, and fewer lock timeouts across the system.
18
28
19
29
### Example
20
30
@@ -44,9 +54,90 @@ begin
44
54
end;
45
55
{{< /highlight >}}
46
56
57
+
### Convert or delete
58
+
59
+
Microsoft's own [coding guidance](https://github.com/microsoft/BCQuality/tree/main/microsoft/knowledge/performance) endorses `LockTable` for exactly one situation: a read that a directly following `Insert`, `Modify`, or `Delete` depends on. One question decides what to do with a flagged call:
60
+
61
+
-**A read of the same table follows, and a write depends on what it returned.** Convert: put `ReadIsolation(IsolationLevel::UpdLock)` on the instance that performs that read.
62
+
-**No such read follows.** The lock protects nothing. Delete the line.
63
+
64
+
The modern shape for "make sure nobody else took this number" is `ReadIsolation` on the instance that reads. The Base Application's `Customer` table does this in `OnInsert`:
The same pattern appears in `Vendor`, `Item`, `Contact`, `Employee`, `Bank Account`, `Resource`, and in the Sustainability ESG tables added in 2025.
76
+
77
+
### LockTable in table triggers
78
+
79
+
A bare `LockTable()` in `OnInsert` or `OnDelete` binds to the same built-in method as `MyVar.LockTable()`: the compiler models no self-receiver special case, and the runtime effect is the same transaction-wide `UPDLOCK` on the table. The diagnostic is therefore reported inside triggers as well.
80
+
81
+
Most trigger `LockTable()` calls in the Base Application come in two shapes.
82
+
83
+
**No read follows.** A journal table locks itself in `OnInsert` and then reads an unrelated template table. The lock on the journal line table serves no read:
84
+
85
+
{{< highlight al "hl_lines=3" >}}
86
+
trigger OnInsert()
87
+
begin
88
+
LockTable(); // Use ReadIsolation instead of LockTable [LC0031]
89
+
ItemJnlTemplate.Get("Journal Template Name");
90
+
end;
91
+
{{< /highlight >}}
92
+
93
+
Delete the line. The Base Application removed exactly this call from `Item Journal Line` in version 26:
94
+
95
+
{{< highlight al >}}
96
+
trigger OnInsert()
97
+
begin
98
+
ItemJnlTemplate.Get("Journal Template Name");
99
+
end;
100
+
{{< /highlight >}}
101
+
102
+
**A dependent read follows.** An entry table locks itself in `OnInsert`, then a second instance reads the last number, and the insert depends on that value:
103
+
104
+
{{< highlight al "hl_lines=5" >}}
105
+
trigger OnInsert()
106
+
var
107
+
Attachment2: Record Attachment;
108
+
begin
109
+
Attachment2.LockTable(); // Use ReadIsolation instead of LockTable [LC0031]
110
+
if Attachment2.FindLast() then
111
+
"No." := Attachment2."No." + 1;
112
+
end;
113
+
{{< /highlight >}}
114
+
115
+
Convert the lock to `ReadIsolation` on the instance that reads:
The Base Application still contains about 78 `LockTable()` calls in table triggers. Since version 23.5 it has removed two and added none, and the newer Microsoft apps (Business Foundation, E-Document Core, Subscription Billing, Excise Taxes) contain no `LockTable` at all.
129
+
130
+
### Code fix
131
+
132
+
The **ALCops: Replace LockTable() with ReadIsolation** code fix rewrites the call to `ReadIsolation(IsolationLevel::UpdLock)` on the same receiver: `MyVar.LockTable()` becomes `MyVar.ReadIsolation(...)`, `Rec.LockTable()` becomes `Rec.ReadIsolation(...)`, and a bare `LockTable()` inside a table or tableextension becomes a bare `ReadIsolation(...)`. It drops the `Wait` and `VersionCheck` arguments of `LockTable(true, true)`, because `ReadIsolation` has no equivalent.
133
+
134
+
The fix always converts and never deletes. Apply it when a dependent read follows; when the call protects nothing, delete the line instead.
135
+
47
136
### See also
48
137
49
138
-[Record instance isolation level](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-read-isolation) — Microsoft Learn reference for `ReadIsolation` and isolation levels
139
+
-[Prefer ReadIsolation over LockTable for reads](https://github.com/microsoft/BCQuality/blob/main/microsoft/knowledge/performance/prefer-readisolation-over-locktable-for-reads.md) and [Do not LockTable in a read-only procedure](https://github.com/microsoft/BCQuality/blob/main/microsoft/knowledge/performance/do-not-locktable-in-read-only-procedure.md) — Microsoft's BCQuality guidance on when `LockTable` is still justified
140
+
-[BC Internals: Tri-state locking](https://bcinternals.com/posts/tri-state-locking/) — The runtime team on the locking model, with the options listed in order of preference
50
141
-[Locking Scope: Differences between LockTable and ReadIsolation](https://www.keytogoodcode.com/post/locking-scope-differences-between-locktable-and-readisolation) — SQL-level analysis showing how `LockTable` leaks locks to unrelated code
51
142
-[Optimized Locking Feature vs Dynamics 365 Business Central](https://duiliotacconi.com/2025/05/30/optimized-locking-feature-vs-dynamics-365-business-central/) — Tri-state locking, RCSI, and transaction isolation in practice
52
143
-[Rec.LockTable: Good Practice or Bad Practice?](https://waldo.be/2024/03/28/rec-locktable-good-practice-or-bad-practice/) — How a long-standing best practice became an anti-pattern
0 commit comments