Salesforce Apex 类:深度解析与定制开发指南

背景与应用场景

Salesforce 平台以其强大的声明式(Declarative)配置能力而闻名,允许管理员和业务用户无需编写代码即可构建复杂的业务逻辑和用户界面。然而,在面对高度定制化、集成复杂外部系统或处理大量数据时,声明式工具的局限性便会显现。这时,Apex 类(Apex Classes)作为 Salesforce 平台上的强类型(Strongly-typed)、面向对象(Object-Oriented)编程语言,成为了实现这些高级需求的关键。

Apex 类运行在 Salesforce 的多租户(Multi-tenant)云计算架构之上,为开发者提供了与 Salesforce 数据和 API 进行交互的强大能力。它类似于 Java 语言,但专为 Salesforce 环境量身定制,提供了内置的对 Salesforce 数据操作语言(SOQL)和数据操纵语言(DML)的支持。

Apex 类的主要应用场景包括:

  • 复杂业务逻辑实现:当标准的工作流(Workflow Rules)、流程生成器(Process Builder)或流(Flow)无法满足复杂的计算、验证或多对象联动逻辑时,Apex 类能够提供更精细的控制和更强大的处理能力。例如,根据多个条件动态计算销售佣金,或在特定业务流程中强制执行复杂的数据完整性规则。
  • 与外部系统集成:通过 Apex 类,可以轻松地调用外部系统的 REST API 或 SOAP API,实现 Salesforce 与企业其他系统(如 ERP、BI 工具、呼叫中心等)的双向数据交换和业务流程协调。
  • 定制用户界面(UI/UX):Apex 类作为 Visualforce 页面、Lightning Web Components(LWC)或 Aura Components 的控制器,为前端提供了数据和业务逻辑的支持,实现高度定制化的用户体验。
  • 大规模数据处理:对于需要处理大量记录的批处理任务,如数据迁移、定期数据清理或生成报表,批处理 Apex(Batch Apex)提供了高效且符合平台限制的解决方案。
  • 定时任务:通过计划 Apex(Scheduled Apex),可以定时执行特定的业务逻辑,例如每日生成报表、每周同步数据等。
  • 电子邮件服务:Apex 类可以接收并处理传入的电子邮件,实现自动化的邮件响应或从邮件内容中提取信息并创建/更新 Salesforce 记录。
  • 触发器(Triggers)的业务逻辑封装:尽管触发器本身是 Apex 代码,但最佳实践是将触发器中的逻辑委托给独立的 Apex 类来处理,以提高代码的可维护性和可测试性。

原理说明

Apex 是一种专为在 Salesforce Lightning Platform(原 Force.com 平台)上运行而设计的编程语言。它在服务器端执行,可以控制平台上的事务流、数据操作以及与外部系统的交互。

多租户架构与平台限制(Governor Limits)

Apex 最核心的特性之一是其在多租户环境中的运行机制。Salesforce 的服务器资源由所有客户共享,为了保证系统的稳定性、性能和公平性,Apex 代码在执行时会受到严格的平台限制(Governor Limits)。这些限制包括但不限于:DML 操作的次数、SOQL 查询的次数和返回行数、CPU 执行时间、堆内存使用量、Callout(外部服务调用)次数和时间等。开发者必须在编写 Apex 代码时始终考虑并遵守这些限制,否则代码将因超出限制而失败。

Apex 类基本结构

一个 Apex 类由关键字 public class 开始,后跟类名。类可以包含变量(Variables)、方法(Methods)、构造函数(Constructors)和内部类(Inner Classes)。

  • 变量:用于存储数据。可以是基本数据类型(如 String, Integer, Boolean, Decimal)或 SObject 类型(如 Account, Contact)以及自定义对象。
  • 方法:包含执行特定任务的代码块。方法可以有输入参数并返回一个值。
  • 构造函数:一种特殊的方法,当创建类的新实例(对象)时会自动调用,用于初始化对象的属性。

访问修饰符(Access Modifiers)决定了类、方法或变量的可见性:

  • public:在应用程序中的任何位置都可见。
  • private:只在定义它的类内部可见。
  • protected:在定义它的类内部以及其子类中可见。
  • global:允许在整个 Salesforce 组织以及任何已安装的包(Managed Package)中引用。通常用于 Web 服务或 Visualforce 控制器的方法。

静态成员与实例成员:

  • 静态(Static)成员(方法或变量)属于类本身,无需创建类的实例即可直接通过类名调用。它们在整个执行上下文中共享。
  • 实例(Instance)成员属于类的特定对象,需要先创建类的实例才能访问。每个实例都有自己独立的实例变量副本。

数据操作:SOQL 与 DML

Apex 与 Salesforce 数据库的交互主要通过两种语言:

  • SOQL (Salesforce Object Query Language):用于从 Salesforce 数据库中检索数据,类似于 SQL(Structured Query Language)。它允许您查询标准对象(Standard Objects)和自定义对象(Custom Objects)的记录。
  • DML (Data Manipulation Language):用于插入(insert)、更新(update)、删除(delete)、合并(merge)、恢复(undelete)和提升(upsert)Salesforce 记录。DML 操作以原子性(Atomic)方式执行,要么全部成功,要么全部失败。

异常处理(Exception Handling)

为了代码的健壮性,Apex 支持使用 try-catch-finally 块进行异常处理。当代码执行过程中发生错误时(例如,DML 操作失败或超出平台限制),Apex 会抛出异常(Exception),您可以使用 catch 块来捕获并处理这些异常,防止程序崩溃。

示例代码

以下示例代码将展示 Apex 类的基本结构、如何执行 SOQL 查询和 DML 操作,以及如何编写对应的测试类。

示例1:一个简单的实用工具类

这个 MyCalculator 类提供了一些基本的数学运算方法,全部是静态方法,可以直接通过类名调用。

public class MyCalculator {
    /**
     * @description Adds two decimal numbers.
     * @param a The first decimal number.
     * @param b The second decimal number.
     * @return The sum of a and b.
     */
    public static Decimal add(Decimal a, Decimal b) {
        return a + b;
    }

    /**
     * @description Subtracts the second decimal number from the first.
     * @param a The first decimal number.
     * @param b The second decimal number.
     * @return The result of a minus b.
     */
    public static Decimal subtract(Decimal a, Decimal b) {
        return a - b;
    }

    /**
     * @description Multiplies two decimal numbers.
     * @param a The first decimal number.
     * @param b The second decimal number.
     * @return The product of a and b.
     */
    public static Decimal multiply(Decimal a, Decimal b) {
        return a * b;
    }

    /**
     * @description Divides the first decimal number by the second.
     * @param a The first decimal number.
     * @param b The second decimal number.
     * @return The result of a divided by b.
     * @throws DivideByZeroException if b is zero.
     */
    public static Decimal divide(Decimal a, Decimal b) {
        if (b == 0) {
            // Throw a custom exception if division by zero is attempted
            throw new DivideByZeroException('Cannot divide by zero');
        }
        return a / b;
    }
}

示例2:SOQL 与 DML 操作示例类

AccountManager 类演示了如何在 Apex 中创建新的客户(Account)记录以及如何查询并更新现有客户的描述字段。

public class AccountManager {

    // Custom Exception for AccountManager errors
    public class AccountManagerException extends Exception {}

    /**
     * @description Creates a new Account record.
     * @param accountName The name of the new account.
     * @param phone The phone number of the account.
     * @param employees The number of employees for the account.
     * @return The Id of the newly created account.
     * @throws AccountManagerException if the account name is empty or DML operation fails.
     */
    public static Id createNewAccount(String accountName, String phone, Integer employees) {
        // Ensure accountName is not null or empty
        if (String.isBlank(accountName)) {
            // Throw a custom exception for invalid input
            throw new AccountManagerException('Account name cannot be empty.');
        }

        // Create a new Account sObject instance
        Account newAccount = new Account(
            Name = accountName,
            Phone = phone,
            NumberOfEmployees = employees
        );

        try {
            insert newAccount; // DML operation to insert the new account into the database
            System.debug('Account created with ID: ' + newAccount.Id);
            return newAccount.Id;
        } catch (DmlException e) {
            // Catch DML specific exceptions (e.g., validation rule failures, database errors)
            System.debug('Error creating account: ' + e.getMessage());
            // Re-throw the exception wrapped in our custom exception
            throw new AccountManagerException('Failed to create account: ' + e.getMessage());
        }
    }

    /**
     * @description Queries Account records based on a partial name match and updates their descriptions.
     * @param accountNamePart A part of the account name to search for.
     * @param newDescription The new description to set for matching accounts.
     * @return A list of updated Account records.
     * @throws AccountManagerException if DML update operation fails.
     */
    public static List<Account> updateAccountDescriptionByName(String accountNamePart, String newDescription) {
        // SOQL query to find accounts whose names contain the specified part
        // The LIKE operator performs a pattern match, '%' is a wildcard
        List<Account> accountsToUpdate = [SELECT Id, Name, Description 
                                        FROM Account 
                                        WHERE Name LIKE :('%' + accountNamePart + '%')];

        if (!accountsToUpdate.isEmpty()) {
            for (Account acc : accountsToUpdate) {
                acc.Description = newDescription; // Update the description field of each account in the list
            }
            try {
                update accountsToUpdate; // DML operation to update the list of accounts
                System.debug('Updated ' + accountsToUpdate.size() + ' account(s).');
            } catch (DmlException e) {
                System.debug('Error updating accounts: ' + e.getMessage());
                // Re-throw the exception wrapped in our custom exception
                throw new AccountManagerException('Failed to update accounts: ' + e.getMessage());
            }
        } else {
            System.debug('No accounts found with name part: ' + accountNamePart);
        }
        return accountsToUpdate;
    }
}

示例3:针对 AccountManager 类的测试类

编写高质量的测试代码对于确保 Apex 类的功能正确性至关重要。Salesforce 要求部署到生产环境的代码至少有 75% 的测试覆盖率(Test Coverage)。以下是 AccountManager 类的测试示例,它遵循了最佳实践:

@IsTest // Annotation indicating this is a test class
private class AccountManager_Test {

    /**
     * @description Test method for createNewAccount. Verifies successful account creation.
     */
    @IsTest static void testCreateNewAccount() {
        // Test.startTest() and Test.stopTest() are used to reset governor limits and simulate asynchronous operations.
        // All code between these calls executes with a fresh set of governor limits.
        Test.startTest();
        // Call the method to be tested
        Id newAccountId = AccountManager.createNewAccount('Test Account 123', '123-456-7890', 100);
        Test.stopTest();

        // Assertions to verify the outcome of the method call
        System.assertNotEquals(null, newAccountId, 'New account ID should not be null');

        // Query the created account to verify its data was saved correctly in the database
        Account createdAccount = [SELECT Id, Name, Phone, NumberOfEmployees FROM Account WHERE Id = :newAccountId];

        System.assertEquals('Test Account 123', createdAccount.Name, 'Account name should match');
        System.assertEquals('123-456-7890', createdAccount.Phone, 'Account phone should match');
        System.assertEquals(100, createdAccount.NumberOfEmployees, 'Account employees should match');
    }

    /**
     * @description Test method for createNewAccount. Verifies exception handling when name is empty.
     */
    @IsTest static void testCreateNewAccountWithEmptyName() {
        Boolean caughtException = false;
        Test.startTest();
        try {
            // Attempt to create an account with an empty name, which should trigger an exception
            AccountManager.createNewAccount('', '555-123-4567', 50); 
        } catch (AccountManager.AccountManagerException e) {
            // Verify that the correct custom exception was caught and the message is as expected
            System.assert(e.getMessage().contains('Account name cannot be empty.'), 'Exception message should indicate empty name.');
            caughtException = true;
        }
        Test.stopTest();
        // Assert that an exception was indeed caught
        System.assert(caughtException, 'An exception should have been caught for empty name.');
    }

    /**
     * @description Test method for updateAccountDescriptionByName. Verifies account description updates.
     */
    @IsTest static void testUpdateAccountDescription() {
        // Prepare test data. It's crucial to create test data within the test method context.
        // Data created in test methods is automatically rolled back after the test runs.
        Account acc1 = new Account(Name = 'Account for Update 1', Description = 'Old Description');
        Account acc2 = new Account(Name = 'Another Account', Description = 'Old Description');
        insert new List<Account>{acc1, acc2}; // Insert multiple accounts to test bulkification

        String newDesc = 'New Updated Description';

        Test.startTest();
        // Call the method to be tested
        List<Account> updatedAccounts = AccountManager.updateAccountDescriptionByName('Account', newDesc);
        Test.stopTest();

        // Verify that only the relevant accounts were updated
        System.assertEquals(1, updatedAccounts.size(), 'Only one account should be returned as updated');
        // Query the database to verify the actual state of the record after the update
        Account verifiedAccount = [SELECT Description FROM Account WHERE Id = :acc1.Id];
        System.assertEquals(newDesc, verifiedAccount.Description, 'Account description should be updated');

        // Verify the other account was not affected
        Account notUpdatedAccount = [SELECT Description FROM Account WHERE Id = :acc2.Id];
        System.assertEquals('Old Description', notUpdatedAccount.Description, 'Other account description should not be updated');
    }
}

注意事项

在 Salesforce Apex 开发中,有几个关键方面需要特别注意,以确保代码的性能、安全性和可维护性。

平台限制(Governor Limits)

理解并遵守平台限制是 Apex 开发的核心。由于 Apex 代码运行在共享的多租户环境中,Salesforce 强制执行各种运行时限制,以防止任何代码垄断系统资源。常见的限制包括:

  • SOQL 查询限制:一个事务中最多执行 100 个 SOQL 查询。
  • SOQL 查询行数限制:一个事务中最多检索 50,000 行。
  • DML 操作限制:一个事务中最多执行 150 个 DML 语句(insert, update, delete 等)。
  • DML 记录限制:一个事务中 DML 语句最多处理 10,000 条记录。
  • CPU 时间限制:每个同步事务最多 10,000 毫秒,异步事务(如批处理、计划任务)最多 60,000 毫秒。
  • 堆内存限制:同步事务最多 6 MB,异步事务最多 12 MB。
  • Callout(外部服务调用)限制:一个事务中最多 100 个 Callout,Callout 总时间最多 120,000 毫秒。

开发者可以使用 Limits 类(如 Limits.getQueries(), Limits.getDmlRows())来监控当前事务中已使用的资源,从而避免超出限制。

安全模型(Security Model)

Apex 代码默认运行在系统模式(System Mode)下,这意味着它会忽略当前用户的对象权限(Object Permissions)、字段级安全性(Field-Level Security, FLS)和共享规则(Sharing Rules)。这为开发者提供了极大的灵活性,但同时也带来了潜在的安全风险。

  • 共享行为:
    • with sharing:强制执行当前用户的共享规则。这意味着用户只能看到他们有权限访问的记录。这是 Apex 类的推荐默认行为。
    • without sharing:明确声明不强制执行当前用户的共享规则。代码可以访问所有数据,无论用户是否有权限。仅在确实需要访问所有数据时使用,例如在某些集成场景或管理工具中。
    • inherited sharing:一个类的共享行为由调用它的上下文决定。如果调用者是 with sharing,则它也是 with sharing;如果调用者是 without sharing,则它也是 without sharing。这是 Salesforce 在 API Version 48.0 及更高版本中的默认行为。
  • CRUD/FLS 强制:即使在 with sharing 类中,对象和字段权限也默认不会被强制执行。为了确保代码遵守用户的 CRUD(Create, Read, Update, Delete)和 FLS 权限,应显式使用以下机制:
    • SOQL 查询中的 WITH SECURITY_ENFORCED 子句:在 SOQL 查询中添加此子句,确保用户只查询他们有权限访问的字段和记录。
    • Security.stripInaccessible()在 DML 操作之前,使用此方法从 SObject 列表中剥离用户无权限访问的字段或记录,以防止因权限不足导致 DML 失败。
    • 手动检查权限:使用 Schema.sObjectType.SObject_Name.isAccessible(), isCreateable(), isUpdateable(), isDeleteable() 等方法检查用户对对象或字段的权限。

错误处理(Error Handling)

健壮的 Apex 代码应该能够优雅地处理运行时错误。使用 try-catch-finally 块是标准的异常处理机制:

  • try:包含可能抛出异常的代码。
  • catch:捕获特定类型的异常,并执行相应的处理逻辑(如记录错误、回滚事务、向用户显示消息)。
  • finally:无论是否发生异常,此块中的代码总会执行,通常用于资源清理。

对于 DML 操作,捕获 DmlException 是非常重要的,它包含了 DML 操作失败的详细信息。此外,考虑创建自定义异常类,以提供更具业务含义的错误信息。

测试覆盖率(Test Coverage)与部署

所有部署到生产环境的 Apex 代码(包括类和触发器)都必须拥有至少 75% 的测试覆盖率(Test Coverage),并且所有测试方法必须成功通过。测试覆盖率并不是衡量代码质量的唯一标准,但它是确保代码可靠性的基本要求。

  • 测试方法必须使用 @IsTest 注解。
  • 测试方法中的数据默认不会保存到数据库,并且与实际组织数据隔离(除非使用 @IsTest(SeeAllData=true),但强烈不推荐)。因此,您需要在测试方法内部创建所有必要的测试数据。
  • 使用 Test.startTest()Test.stopTest() 来模拟异步操作和重置 Governor Limits。
  • 使用 System.assert() 系列方法来验证代码的预期行为。

部署 Apex 代码通常通过变更集(Change Sets)、Salesforce CLI (SFDX) 或 Ant Migration Tool 进行。

总结与最佳实践

Apex 类是 Salesforce 平台实现定制化和复杂业务逻辑的强大工具。为了编写高效、健壮、安全且易于维护的 Apex 代码,以下最佳实践至关重要:

  • 模块化与重用性:将复杂的逻辑分解成小的、功能单一的方法和类。鼓励代码重用,避免重复代码(Don't Repeat Yourself, DRY)。
  • 触发器轻量化(Trigger Handler Pattern):触发器本身应该尽可能地“瘦”(Thin),仅用于调用一个或多个 Apex 类来处理实际的业务逻辑。这有助于提高触发器的可维护性和可测试性。
  • 批量化(Bulkification):Apex 代码应始终设计为能够处理多个记录(即 List<SObject>),而不是单个记录。避免在循环中执行 SOQL 查询或 DML 操作,这极易导致超出平台限制。
    // Bad practice: DML inside a loop (will hit governor limit quickly)
    for (Account acc : newAccounts) {
        insert acc; // DML in loop - AVOID!
    }
    
    // Good practice: DML on a list outside the loop
    List<Account> accountsToInsert = new List<Account>();
    for (String accName : accountNames) {
        accountsToInsert.add(new Account(Name = accName));
    }
    if (!accountsToInsert.isEmpty()) {
        insert accountsToInsert; // DML on list - BULKIFIED!
    }
            
  • 避免硬编码 ID:不要在代码中直接使用 Salesforce 记录的 ID。相反,应使用 SOQL 查询来获取记录,或者将配置数据存储在自定义设置(Custom Settings)、自定义元数据类型(Custom Metadata Types)或分层自定义设置(Hierarchical Custom Settings)中。
  • 高效的 SOQL 查询:只查询您需要的字段。在 WHERE 子句中使用选择性索引字段以优化查询性能。
  • 防御性编程:在访问集合(Lists, Sets, Maps)元素之前检查其是否为空(nullisEmpty())。在使用 DML 操作之前,确认列表不为空。
  • 良好的命名约定与注释:使用清晰、描述性的类名、方法名和变量名。对于复杂或不明显的逻辑,添加详细的注释。
  • 全面测试:为所有 Apex 类和触发器编写全面的测试代码,覆盖正常情况、边界条件和错误情况。确保测试数据隔离,不依赖于组织中的现有数据。
  • 持续学习与关注 Salesforce 发布说明:Salesforce 平台不断发展,新的特性、最佳实践和限制可能会出现。保持对最新发布说明(Release Notes)的关注,及时更新您的开发知识。

通过遵循这些原则和最佳实践,您将能够构建出高效、可扩展且符合 Salesforce 平台架构的 Apex 解决方案,从而充分发挥 Salesforce 的强大潜力。

评论

此博客中的热门博文

Salesforce 登录取证:深入解析用户访问监控与安全

Salesforce Experience Cloud 技术深度解析:构建社区站点 (Community Sites)

Salesforce Data Loader 全方位指南:数据迁移与管理的最佳实践