Set<SObject>
を使うべきでない理由
重複のないコレクションを作成するためにSetを使うことがありますが、同じ使い方をSObjectで行うと想定と異なる結果になることがあります。
Apex開発者ガイドにはSObjectのSetについて下記のように記述されています。
セットには一意の要素が含まれます。sObject の一意性は、オブジェクトの項目の比較によって判断されます。
SObjectが同一とみなされるかどうかは「項目が一致しているかどうか」になります。
別のインスタンスでも項目が一致していれば重複とみなされる
新たにnewしても項目が同じであれば重複とみなされてしまいます。
Account acc1 = new Account(Name = 'テストA');
Account acc2 = new Account(Name = 'テストA');
Set<Account> accSet = new Set<Account>{ acc1 };
System.assertEquals(true, accSet.contains(acc2)); // 項目が一致しているので重複とみなされています
accSet.add(acc2);
System.assertEquals(1, accSet.size());
同一IDのインスタンスでも同一とみなされない
同じIDのSObjectインスタンスを重複とみなしたいことがあるかと思いますが、項目が異なる場合、Idが同じでも別のものとみなされてしまいます。
Account acc = [SELECT Id FROM Account LIMIT 1];
Account acc1 = new Account(Id = acc.Id, Name = 'テストA');
Account acc2 = new Account(Id = acc.Id, Name = 'テストA', Phone = '03-0000-0000');
Set<Account> accSet = new Set<Account>{ acc1 };
System.assertEquals(false, accSet.contains(acc2)); // Idが一致していても他の項目が異なれば重複とみなされません
accSet.add(acc2);
System.assertEquals(2, accSet.size());
addしたあとの項目変更が反映されないケースがある
Set<SObject>
重複してSetに追加された場合、1つ目のインスタンスがSetに格納されるようです。そのため、2つ目以降のインスタンスの項目を変更してもSetの内容には反映されません。
Account acc1 = new Account(Name = 'テストA');
Account acc2 = new Account(Name = 'テストA');
Set<Account> accSet = new Set<Account>();
accSet.add(acc1);
accSet.add(acc2);
System.assertEquals(1, accSet.size());
// acc1の項目を変更します
acc1.Name = 'テストB';
System.debug(accSet); // 当然、Setの要素に変更が反映されます: {Account:{Name=テストB}}
System.debug(acc2); // acc2の値は変わりません: Account:{Name=テストA}
System.assertEquals(false, accSet.contains(new Application(Name = 'テストA')));
Account acc1 = new Account(Name = 'テストA');
Account acc2 = new Account(Name = 'テストA');
Set<Account> accSet = new Set<Account>();
accSet.add(acc1);
accSet.add(acc2);
System.assertEquals(1, accSet.size());
// acc2の項目を変更します
acc2.Name = 'テストC';
System.debug(accSet); // Setの要素に変更が反映されません!: {Account:{Name=テストA}}
System.debug(acc1); // acc1の値も変わりません: Account:{Name=テストA}
System.assertEquals(true, accSet.contains(new Account(Name = 'テストA')));
代わりにMap<Id, SObject>
を使う
同一Idのインスタンスを同じものとして扱いたいときはSet<SObject>
の代わりにMap<Id, SObject>
を使用できます。
Map<Id, Account> accMap = new Map<Id, Account>();
for (Opportunity opp : oppList) {
if (opp.IsSomething__c && opp.AccountId != null) {
accMap.put(opp.AccountId, new Account(Id = opp.AccountId, IsSomething__c = true));
}
}
update accMap.values();