sObject 集合
你可以在 List、Set 和 Map 中管理 sObject。集合提供强大的方式来批量处理数据、对记录排序以及执行高效的内存内查找。
sObject 的 List
List 可以包含 sObject 以及其他类型元素。sObject 的 List 可用于数据的批量处理。使用 List<sObjectType> 声明。
List<Account> myList = new List<Account>();
从 SOQL 查询自动填充 List:可以直接将 List 变量赋值给 SOQL 查询结果。确保 List 变量与查询的 sObject 类型相同(或使用通用 sObject)。查询最多返回 1,000 条记录包含 Id 和 Name 字段。
List<Account> accts = [SELECT Id, Name FROM Account LIMIT 1000];
// 添加和获取元素
List<Account> myList = new List<Account>();
Account a = new Account(Name='Acme');
myList.add(a);
Account a2 = myList.get(0); // 获取索引 0 处的元素
// 批量处理——一条 DML 插入整个列表
List<Account> acctList = new List<Account>();
acctList.add(new Account(Name='Account1'));
acctList.add(new Account(Name='Account2'));
insert acctList; // 批量插入
记录 ID 生成与重复引用陷阱:Apex 自动为被 DML 插入或 upsert 的 sObject 列表中的每个对象生成 ID。因此,包含对同一 sObject 实例多个引用的列表不能插入或 upsert——会导致 ListException:
Account a = new Account();
List<Account> accs = new List<Account>{a, a}; // 同一对象两次
try {
insert accs; // 抛出 ListException
} catch (ListException e) {
// 两个 ID 无法写入同一内存结构
}
数组表示法:也可以使用方括号声明和引用 sObject 列表:Account[] accts = new Account[1]; → accts[0] = new Account(Name='Acme2');。
排序 sObject 列表
使用 List.sort 方法对 sObject 列表进行升序排序。默认排序按以下比较序列步骤进行:
- sObject 类型的标签——例如,Account 出现在 Contact 之前
- Name 字段(如果适用)——Alpha 在 Beta 之前
- 标准字段(按字母顺序)——排除 Id 和 Name。如果两个账户同名,使用 AccountNumber 等
- 自定义字段(按字母顺序)
排序在找到可区分两个 sObject 的步骤时停止。文本字段使用 Unicode 排序顺序,空字段排在非空字段之前。
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'));
acctList.sort();
// 结果:Acme, sForce(Agriculture), sForce(Biotechnology)
// Name 字段先排序,同名再用 Industry 字段(Industry 在字母顺序中排在 Site 之前)
sObject 的自定义排序
要为 sObject 创建自定义排序顺序,有两种方式:
方式一:实现 Comparator 接口——创建比较器类并作为参数传递给 List.sort:
public class OpportunityAmountComparator implements Comparator<Opportunity> {
public Integer compare(Opportunity o1, Opportunity o2) {
if (o1.Amount == o2.Amount) return 0;
return (o1.Amount > o2.Amount) ? 1 : -1;
}
}
// 使用
List<Opportunity> opps = [SELECT Amount FROM Opportunity];
opps.sort(new OpportunityAmountComparator());
方式二:实现 Comparable 接口——创建包装类并实现 compareTo 方法:
public class OpportunityWrapper implements Comparable {
public Opportunity opp;
public OpportunityWrapper(Opportunity o) { opp = o; }
public Integer compareTo(Object other) {
OpportunityWrapper ow = (OpportunityWrapper) other;
if (opp.Amount == ow.opp.Amount) return 0;
return (opp.Amount > ow.opp.Amount) ? 1 : -1;
}
}
要执行基于语言环境的排序,使用 Collator 类。
展开 sObject 和 List 表达式
与 Java 一样,sObject 和 List 表达式可以通过方法引用和列表表达式展开来形成新表达式:
// 链式访问:创建列表→取第一个→访问Name→获取长度
Integer acctNameLength = new Account[]{new Account(Name='Acme')}[0].Name.length();
// SOQL 结果链式操作
String nameChange = [SELECT Name FROM Account][0].Name.toLowerCase();
sObject 的 Set
Set 可以包含 sObject。sObject 的唯一性通过比较所有对象字段值来确定。如果两个 Account 只有相同的 Name(没有其他字段设置),Set 只保留一个:
Account a1 = new Account(name='MyAccount');
Account a2 = new Account(name='MyAccount');
Set<Account> accountSet = new Set<Account>{a1, a2};
System.assertEquals(accountSet.size(), 1); // 只有一条
// 添加不同字段值后变为唯一
a2 = new Account(name='MyAccount', description='My test account');
accountSet = new Set<Account>{a1, a2};
System.assertEquals(accountSet.size(), 2); // 现在两条
警告:Set 元素是对象时,如果对象添加到集合后发生更改,使用contains或containsAll方法将无法再找到它们(因为字段值改变了)。
sObject 的 Map
Map 的键和值都可以是 sObject 类型。最常见的模式是用 ID 作为键映射到 sObject。可以从 SOQL 查询结果自动填充 Map(键必须是 ID 或 String,值必须是 sObject):
// 定义——ID 为键,Account 为值
Map<ID, Account> m = new Map<ID, Account>();
// 从 SOQL 查询自动填充——常用于内存内联表连接
Map<ID, Account> m = new Map<ID, Account>([SELECT Id, Name FROM Account LIMIT 10]);
// 遍历 Map 条目
for (ID idKey : m.keySet()) {
Account a = m.get(idKey);
System.debug(a);
}
// 使用 Map 方法
Account myAcct = new Account();
Map<Integer, Account> m = new Map<Integer, Account>();
m.put(1, myAcct);
System.assert(!m.containsKey(3));
Account a = m.get(1);
Set<Integer> s = m.keySet();
sObject 作为 Map 键的注意事项
使用 sObject 作为 Map 键需要极其谨慎。键匹配基于所有字段值的比较。如果在将 sObject 添加到 Map 之后其字段值发生更改,尝试从 Map 中检索该 sObject 会返回 null——因为修改后的 sObject 由于字段值不同而无法被找到。
经典陷阱——插入后的 ID 自动填充:
Account a1 = new Account(Name='A1');
Map<Account, Integer> m = new Map<Account, Integer>{a1 => 1};
System.assertEquals(1, m.get(a1)); // 插入前可以找到
System.assertEquals(null, a1.Id); // ID 为 null
insert a1; // ID 现在被自动填充!
System.assertNotEquals(null, a1.Id);
System.assertEquals(null, m.get(a1)); // 无法再找到——ID 字段值改变了
触发器中的场景:before insert 触发器中 Trigger.New 的 sObject 被添加到静态 Map 中后,在 after insert 触发器中这些 sObject 无法在该 Map 中被找到——因为 after trigger 中的 Trigger.New sObject 的系统字段已被填充(ID、CreatedDate、CreatedById、LastModifiedDate、LastModifiedById、SystemModStamp),与添加到 Map 中的原始 sObject 字段值不同。
理解 sObject 集合的正确使用方式——从批量处理到排序再到 Map 键的陷阱——是编写高效、正确的 Apex 代码的关键。