背景
これまでの経験では、apex コードはシステム モードで実行されていましたが、Summer’26 リリース (つまり、apex api バージョン: 67) 以降、apex はユーザー モードで実行されるため、将来 apex クラスを作成するときは、現在のシステム設定でデフォルトで生成された API バージョンも厳密に確認する必要があります。そうしないと、プログラムで予期しない結果が発生する可能性があります。
私たちのブログでは、以前にシステム モードとユーザー モードについて説明しました。要約すると次のような文になります。
ユーザー モード: 現在実行中のユーザーの権限に従って実行され、オブジェクト権限 (CRUD)、フィールド セキュリティ (FLS)、および共有ルール (共有) がチェックされます。
システム モード: 現在のユーザーのオブジェクト権限と FLS はチェックされません。これは、ユーザーがすべてのオブジェクトとフィールドへのアクセス権を持っていることと同等です。レコードレベルの共有が有効になるかどうかは、apex クラスによって宣言された共有キーワードによって決まります。
では、API バージョン 66 とそれ以前と後の明らかな違いは何でしょうか?次のデモに従うことで、よりよく理解できます。
AIが生成したテスト例
1. 2 つのフィールド (1 つは FLS あり、もう 1 つは FLS なし) を持つカスタム テーブルを作成します。

2. AccessModeDemoController は主に 3 つの機能を実装します。
- 現在のユーザー情報の表示権限
- システムモードとユーザーモードを実行するためのフロントエンドボタンを介した異なるルーティング
- すべてのクエリ シナリオをテストする
/** * Apex 用户模式 / 系统模式对比演示(LWC 控制器)。 * * 外层类声明为 with sharing;需要绕过记录共享的场景放在内部 without sharing 类 Elevated 中, * 以便同时演示:访问模式(CRUD/FLS)与 sharing 关键字(记录级)是两套独立的机制。 */ public with sharing class AccessModeDemoController { private static final String NOT_RETURNED = '— 未返回 —'; private static final String DEMO_FIELDS = 'Id, Name, Public_Note__c, Secret_Note__c, Owner.Name'; private static final ListDEMO_PATHS = new List return e?.body?.message ; private static final List DEMO_PUBLIC_PATHS = new List ; // ---------------------------------------------------------------- DTO public class ScenarioResult return e?.body?.message public class UserContext e?.message // ---------------------------------------------------------------- 内部 without sharing 类 private without sharing class Elevated { List querySystemMode() { return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate]; } Integer countAll() { return Database.countQuery('SELECT COUNT() FROM Access_Mode_Demo__c', AccessLevel.SYSTEM_MODE); } void insertAsSystem(Access_Mode_Demo__c rec) { insert as system rec; } } // ---------------------------------------------------------------- 当前用户上下文 @AuraEnabled public static UserContext getUserContext() { Id uid = UserInfo.getUserId(); UserContext ctx = new UserContext(); ctx.userId = uid; ctx.userName = UserInfo.getName(); ctx.loginName = UserInfo.getUserName(); User me = [SELECT Profile.Name FROM User WHERE Id = :uid WITH SYSTEM_MODE]; ctx.profileName = me.Profile?.Name; ctx.modifyAllData = [SELECT COUNT() FROM PermissionSetAssignment WHERE AssigneeId = :uid AND PermissionSet.PermissionsModifyAllData = true WITH SYSTEM_MODE] > 0; ctx.canCreateDemo = Schema.SObjectType.Access_Mode_Demo__c.isCreateable(); ctx.canReadSecret = Schema.SObjectType.Access_Mode_Demo__c.fields.Secret_Note__c.isAccessible(); ctx.totalDemoRecords = new Elevated().countAll(); ctx.ownDemoRecords = [SELECT COUNT() FROM Access_Mode_Demo__c WHERE OwnerId = :uid WITH SYSTEM_MODE]; return ctx; } // ---------------------------------------------------------------- 查询对比场景 @AuraEnabled public static List runAllScenarios() { List results = new List (); // A. 未声明访问模式 · 类 API 66.0 ScenarioResult a = newResult('A', '默认模式', '未声明访问模式(类 API 66.0)', '默认(系统)', 'without sharing', '// AccessModeDefaultV66.cls (apiVersion 66.0)\n[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c]', 'API 66.0 及以前默认系统模式:不检查当前用户的对象权限和 FLS;without sharing 也不按共享过滤。'); try { fill(a, AccessModeDefaultV66.queryWithoutModeClause(), DEMO_PATHS); } catch (Exception e) { fail(a, e); } results.add(a); // B. 未声明访问模式 · 类 API 68.0 ScenarioResult b = newResult('B', '默认模式', '未声明访问模式(类 API 68.0)', '默认(用户)', 'without sharing', '// AccessModeDefaultV68.cls (apiVersion 68.0)\n[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c]', 'API 67.0 及以后默认用户模式:同样的代码,仅因类的 API 版本不同,就会检查 CRUD、FLS 和共享。'); try { fill(b, AccessModeDefaultV68.queryWithoutModeClause(), DEMO_PATHS); } catch (Exception e) { fail(b, e); } results.add(b); // C. WITH USER_MODE(包含受限字段) ScenarioResult c = newResult('C', '显式声明', 'WITH USER_MODE(包含 Secret_Note__c)', '用户模式', 'with sharing', '[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH USER_MODE]', '用户模式检查 FLS:用户没有 Secret_Note__c 读取权限时整条查询抛 QueryException,可用 getInaccessibleFields() 拿到全部无权限字段。'); try { fill(c, [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate], DEMO_PATHS); } catch (Exception e) { fail(c, e); } results.add(c); // D. WITH USER_MODE(仅有权限字段) ScenarioResult d = newResult('D', '显式声明', 'WITH USER_MODE(仅查询有权限字段)', '用户模式', 'with sharing', '[SELECT Id, Name, Public_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE]', '用户模式始终遵守共享规则:OWD 为 Private 时,普通用户只能看到自己拥有或被共享的记录。'); try { fill(d, [SELECT Id, Name, Public_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate], DEMO_PUBLIC_PATHS); } catch (Exception e) { fail(d, e); } results.add(d); // E. WITH SYSTEM_MODE + without sharing ScenarioResult e1 = newResult('E', '显式声明', 'WITH SYSTEM_MODE + without sharing', '系统模式', 'without sharing', 'private without sharing class Elevated {\n [SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH SYSTEM_MODE]\n}', '系统模式不检查 CRUD/FLS;类为 without sharing,因此也不按共享过滤 —— 能看到全部记录和 Secret_Note__c。'); try { fill(e1, new Elevated().querySystemMode(), DEMO_PATHS); } catch (Exception e) { fail(e1, e); } results.add(e1); // F. WITH SYSTEM_MODE + with sharing ScenarioResult f = newResult('F', '显式声明', 'WITH SYSTEM_MODE + with sharing', '系统模式', 'with sharing', 'public with sharing class AccessModeDemoController {\n [SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH SYSTEM_MODE]\n}', '系统模式下记录可见性由类的 sharing 关键字决定:with sharing 仍按共享过滤行,但 FLS 不检查,Secret_Note__c 照样返回。'); try { fill(f, [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate], DEMO_PATHS); } catch (Exception e) { fail(f, e); } results.add(f); // G. Security.stripInaccessible ScenarioResult g = newResult('G', '优雅降级', 'Security.stripInaccessible(READABLE)', '系统模式查询 + 剥离', 'with sharing', 'List recs = [SELECT ... FROM Access_Mode_Demo__c WITH SYSTEM_MODE];\n' + 'SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, recs);\n' + 'dec.getRecords(); dec.getRemovedFields();', '不抛异常,而是把当前用户无权读取的字段从结果里移除;getRemovedFields() 返回被移除的字段。'); try { List raw = [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate]; SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, raw); fill(g, dec.getRecords(), DEMO_PATHS); for (String obj : dec.getRemovedFields().keySet()) { for (String fld : dec.getRemovedFields().get(obj)) { g.problemFields.add(obj + '.' + fld); } } } catch (Exception e) { fail(g, e); } results.add(g); return results; } // ---------------------------------------------------------------- DML 对比场景 @AuraEnabled public static ScenarioResult createRecord(String name, String publicNote, String secretNote, String mode) { Boolean asUser = (mode == 'USER'); ScenarioResult r = newResult(asUser ? 'DML_USER' : 'DML_SYSTEM', 'DML', asUser ? 'insert as user' : 'insert as system', asUser ? '用户模式' : '系统模式', asUser ? 'with sharing' : 'without sharing', asUser ? 'insert as user rec;' : 'insert as system rec; // 在 without sharing 内部类中执行', asUser ? '用户模式 DML 检查对象“新建”权限与字段编辑 FLS。本 demo 实测:缺少对象新建权限时抛 System.TypeException(DML operation INSERT not allowed);官方文档说明 FLS 错误可通过 DmlException.getDmlFieldNames() 获取。' : '系统模式 DML 不检查当前用户的 CRUD/FLS,即使用户没有“新建”权限也能写入。'); Access_Mode_Demo__c rec = new Access_Mode_Demo__c( Name = String.isBlank(name) ? 'Demo ' + System.now().format('HH:mm:ss') : name, Public_Note__c = publicNote, Secret_Note__c = secretNote); try { if (asUser) { insert as user rec; } else { new Elevated().insertAsSystem(rec); } r.success = true; r.rowCount = 1; Access_Mode_Demo__c saved = [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WHERE Id = :rec.Id WITH SYSTEM_MODE]; fill(r, new List { saved }, DEMO_PATHS); } catch (Exception e) { fail(r, e); } return r; } // ---------------------------------------------------------------- helpers private static ScenarioResult newResult(String key, String grp, String title, String mode, String sharing, String code, String explanation) { ScenarioResult r = new ScenarioResult(); r.key = key; r.category = grp; r.title = title; r.mode = mode; r.sharing = sharing; r.codeSnippet = code; r.explanation = explanation; return r; } private static void fill(ScenarioResult r, List recs, List paths) { r.success = true; r.rowCount = recs.size(); for (String p : paths) { r.columns.add(new Map { 'label' => p, 'fieldName' => p.replace('.', '_') }); } for (SObject rec : recs) { Map row = new Map { 'Id' => rec.Id }; for (String p : paths) { row.put(p.replace('.', '_'), readPath(rec, p)); } r.rows.add(row); } } private static void fail(ScenarioResult r, Exception e) { r.success = false; r.errorType = e.getTypeName(); r.errorMessage = e.getMessage(); if (e instanceof QueryException) { Map > inaccessible = ((QueryException) e).getInaccessibleFields(); if (inaccessible != null) { for (String obj : inaccessible.keySet()) { for (String fld : inaccessible.get(obj)) { r.problemFields.add(obj + '.' + fld); } } } } else if (e instanceof DmlException) { DmlException de = (DmlException) e; for (Integer i = 0; i < de.getNumDml(); i++) { List names = de.getDmlFieldNames(i); if (names != null) { r.problemFields.addAll(names); } } } } private static Object readPath(SObject rec, String path) { Map populated = rec.getPopulatedFieldsAsMap(); if (!path.contains('.')) { if (!populated.containsKey(path)) { return NOT_RETURNED; } Object v = populated.get(path); return v == null ? '(空值)' : String.valueOf(v); } String rel = path.substringBefore('.'); if (!populated.containsKey(rel) || populated.get(rel) == null) { return NOT_RETURNED; } return readPath(rec.getSObject(rel), path.substringAfter('.')); } }
3. AccessModeDefaultV66: API バージョンを 66 に設定します。
public without sharing class AccessModeDefaultV66 { public static ListqueryWithoutModeClause() { return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate]; } }
4. AccessModeDefaultV68: コードは V66 と同じですが、唯一の違いは API バージョンを 68 に設定することです。
public without sharing class AccessModeDefaultV68 { public static ListqueryWithoutModeClause() { return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate]; } }
5. AccessModeDemo.html
<template> <div class="slds-p-around_small"> <lightning-card title="Apex 用户模式 vs 系统模式 演示" icon-name="standard:user_role"> <div class="slds-p-horizontal_medium"> <template lwc:if={ctx}> <div class="slds-grid slds-wrap slds-gutters_x-small"> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <p class="label">当前运行用户p> <p class="value">{ctx.userName}p> <p class="sub">{ctx.loginName}p> div> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <p class="label">Profilep> <p class="value">{ctx.profileName}p> div> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <p class="label">演示记录(总数 / 本人拥有)p> <p class="value">{ctx.totalDemoRecords} / {ctx.ownDemoRecords}p> div> div> <div class="slds-m-top_small badges"> <span class={modifyAllClass}>Modify All Data:{modifyAllText}span> <span class={canCreateClass}>Access_Mode_Demo__c 新建:{canCreateText}span> <span class={canReadSecretClass}>Secret_Note__c 读取 FLS:{canReadSecretText}span> div> template> <template lwc:if={error}> <div class="slds-text-color_error slds-m-top_small">{error}div> template> div> lightning-card> <lightning-card title="① DML 对比:insert as user / insert as system" icon-name="standard:record_create"> <div class="slds-p-horizontal_medium"> <div class="slds-grid slds-wrap slds-gutters_x-small"> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <lightning-input label="Name" value={newName} data-field="newName" onchange={handleInput}>lightning-input> div> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <lightning-input label="Public Note" value={newPublic} data-field="newPublic" onchange={handleInput}>lightning-input> div> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3"> <lightning-input label="Secret Note(受限字段)" value={newSecret} data-field="newSecret" onchange={handleInput}>lightning-input> div> div> <div class="slds-m-top_small"> <lightning-button label="用户模式新建(insert as user)" data-mode="USER" onclick={handleCreate} disabled={busy}>lightning-button> <lightning-button class="slds-m-left_x-small" variant="brand" label="系统模式新建(insert as system)" data-mode="SYSTEM" onclick={handleCreate} disabled={busy}>lightning-button> div> <template lwc:if={dmlResult}> <div class="slds-m-top_small"> <c-access-mode-demo-scenario scenario={dmlResult}>c-access-mode-demo-scenario> div> template> div> lightning-card> <lightning-card title="② 查询对比场景" icon-name="standard:sossession"> <lightning-button slot="actions" variant="brand" label="运行全部对比场景" onclick={handleRunAll} disabled={busy}>lightning-button> <div class="slds-p-horizontal_medium"> <template lwc:if={busy}> <lightning-spinner alternative-text="运行中" size="small">lightning-spinner> template> <template lwc:if={hasResults}> <p class="slds-text-body_small slds-m-bottom_x-small">运行时间:{lastRunAt}p> <table class="slds-table slds-table_cell-buffer slds-table_bordered summary"> <thead> <tr class="slds-line-height_reset"> <th>#th> <th>场景th> <th>访问模式th> <th>sharingth> <th>结果th> <th>问题字段 / 被剥离字段th> tr> thead> <tbody> <template for:each={summaryRows} for:item="s"> <tr key={s.key}> <td>{s.key}td> <td>{s.title}td> <td>{s.mode}td> <td>{s.sharing}td> <td><span class={s.statusClass}>{s.statusText}span>td> <td class="mono">{s.problemText}td> tr> template> tbody> table> <template for:each={results} for:item="r"> <div key={r.key} class="slds-m-top_medium"> <c-access-mode-demo-scenario scenario={r}>c-access-mode-demo-scenario> div> template> template> <template lwc:else> <p class="slds-text-color_weak">点击右上角“运行全部对比场景”。p> template> div> lightning-card> div> template>
AccessModeDemo.js
import { LightningElement } from 'lwc';
import getUserContext from '@salesforce/apex/AccessModeDemoController.getUserContext';
import runAllScenarios from '@salesforce/apex/AccessModeDemoController.runAllScenarios';
import createRecord from '@salesforce/apex/AccessModeDemoController.createRecord';
const yesNo = (v) => (v ? '有' : '无');
const badge = (v) => `slds-badge ${v ? 'slds-theme_success' : 'slds-theme_error'}`;
export default class AccessModeDemo extends LightningElement {
ctx;
error;
busy = false;
results = [];
dmlResult;
lastRunAt;
newName="";
newPublic="公开备注";
newSecret="机密备注";
connectedCallback() {
this.loadContext();
}
async loadContext() {
try {
this.ctx = await getUserContext();
} catch (e) {
this.error = this.reduceError(e);
}
}
get modifyAllText() { return yesNo(this.ctx?.modifyAllData); }
get modifyAllClass() { return badge(this.ctx?.modifyAllData); }
get canCreateText() { return yesNo(this.ctx?.canCreateDemo); }
get canCreateClass() { return badge(this.ctx?.canCreateDemo); }
get canReadSecretText() { return yesNo(this.ctx?.canReadSecret); }
get canReadSecretClass() { return badge(this.ctx?.canReadSecret); }
get hasResults() {
return this.results && this.results.length > 0;
}
get summaryRows() {
return this.results.map((r) => ({
key: r.key,
title: r.title,
mode: r.mode,
sharing: r.sharing,
statusText: r.success ? `成功 · ${r.rowCount} 行` : `失败 · ${r.errorType}`,
statusClass: badge(r.success),
problemText: (r.problemFields || []).join(', ')
}));
}
handleInput(event) {
this[event.target.dataset.field] = event.target.value;
}
async handleCreate(event) {
const mode = event.target.dataset.mode;
this.busy = true;
this.error = undefined;
try {
this.dmlResult = await createRecord({
name: this.newName,
publicNote: this.newPublic,
secretNote: this.newSecret,
mode
});
await this.loadContext();
} catch (e) {
this.error = this.reduceError(e);
} finally {
this.busy = false;
}
}
async handleRunAll() {
this.busy = true;
this.error = undefined;
try {
this.results = await runAllScenarios();
this.lastRunAt = new Date().toLocaleString();
} catch (e) {
this.error = this.reduceError(e);
} finally {
this.busy = false;
}
}
reduceError(e) {
return e?.body?.message || e?.message || JSON.stringify(e);
}
}
6. 通常ユーザーを作成します。Access_Mode_Demo には作成権限がなく、Secret_Note__c には読み取り権限がありません。
テスト結果の表示:
1.DML シナリオ
1. ユーザーモードで「新規」をクリックします。ユーザー モードが使用されているため、エラーが報告されます: Access_Mode_Demo__c では DML 操作 INSERT は許可されていません。
バックグラウンド DML は主にユーザー rec として挿入を実行します。//rec はこの Access_Mode_Demo のレコードです

2. Access_Mode_Demo__c が新しい権限を作成できるように権限セットを追加します。 「ユーザーモード」をクリックして、新しい権限を作成します。ユーザー モードが使用されているため、エラーが報告されます。System.DmlException:Sobject Access_Mode_Demo__c のフィールドにアクセスできないため、操作が失敗しました。Exception または Result でエラーを確認してください。

3. システムモードで「新規」をクリックします。システムモードを使用しているため、作成は成功します。
バックグラウンド DML は主にシステム記録として挿入を実行します。 //rec はこの Access_Mode_Demo のレコードです

2. クエリシナリオ
クエリシナリオには主に以下の組み合わせが含まれます。以下の図の配置と組み合わせからわかるように、API バージョンが 68.0 の場合、ユーザーモードにより、SOQL の特定の項目に対するクエリ権限がない場合、エラーが報告されます。 System.QueryException、
| シーン | アクセスモード | 共有 | SOQL | 結果 | 質問欄 |
| アクセス モードが宣言されていません (クラス API 66.0) | システムモード | 共有なし |
SELECT ID、名前、Public_Note__c、Secret_Note__c、Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate |
成功 | |
| アクセス モードが宣言されていません (クラス API 68.0) | ユーザーモード | 共有なし |
SELECT ID、名前、Public_Note__c、Secret_Note__c、Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate |
失敗 · System.QueryException | Access_Mode_Demo__c.Secret_Note__c |
| WITH USER_MODE (Secret_Note__c を含む; クラス API 68.0) | ユーザーモード | 共有あり |
SELECT ID、名前、Public_Note__c、Secret_Note__c、Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate |
失敗 · System.QueryException | Access_Mode_Demo__c.Secret_Note__c |
| WITH USER_MODE (許可されたフィールドのみをクエリします。クラス API 68.0) | ユーザーモード | 共有あり |
SELECT ID、名前、Public_Note__c、Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate |
成功 | |
| WITH SYSTEM_MODE + 共有なし(类 API 68.0) | システムモード | 共有なし |
SELECT ID、名前、Public_Note__c、Secret_Note__c、Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate |
成功 | |
| WITH SYSTEM_MODE + 共有あり(类 API 68.0) | システムモード | 共有あり |
SELECT ID、名前、Public_Note__c、Secret_Note__c、Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate |
成功 | |
| Security.stripInaccessible(类 API 68.0) | システム モード クエリ + 不正なフィールドの削除 | 共有あり |
リスト
FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate]; SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, raw); |
成功 | 結果セットには、Secret_Note__c は含まれません。 |
要約:
AI 時代において、Salesforce のセキュリティの重要性はますます高まっているため、最近のリリースでは多くのセキュリティ更新が強化されました。プロジェクト内のコードにアクセス許可のセキュリティに対する高度な要件がある場合は、ユーザー モードを検討するか、stripInaccessible メソッドでシステム モードを使用して、ユーザー レベルのアクセス許可と操作を実装し、データ操作を最小限の範囲に制御することができます。後で apex を作成する前に、プロジェクトのアーキテクチャがシステム モードとユーザー モードのどちらで実行するのに適しているかについても検討してください。シナリオごとに異なる API バージョン (66 以前/66 以降) が使用されます。
参考ドキュメント: