[Salesforce] キャンペーンメンバーになっていた大量の取引先責任者をごみ箱から復旧する( Apex による Batch 処理の実装例 )

2021.12.16

ことの起こり

不要な取引先責任者(Contact)の一括削除処理を行なっていたチームメンバーから

「キャンペーンメンバーになっていた取引先責任者も誤って削除してしまいました!!」

との報告を受け、調べてみると約4千件の取引先責任者をごみ箱から復旧しなければいけないことが判明しました。

当然、手でごみ箱からポチポチ戻していられない量なので、メンバーは復旧対象のContact.IdをリストしたCSVを作り workbench を使って復旧を試みたのですが、件数が多すぎてエラーになるとの報告。

Apexで調査

さて、そんな困ったときの Apex ということで、開発者コンソールの「Open Execute Anonymous Window」を立ち上げ、ひとまず以下のコードを実施して、対象Contactの件数などを確認しました。

/* 削除されたキャンペーンメンバー(取引先責任者のみ)のIdを取得 */
List<CampaignMember> cms = [SELECT LeadOrContactId FROM CampaignMember WHERE ContactId != null AND IsDeleted = True ALL ROWS];
List<Id> cmIds = new List<Id>();
for ( CampaignMember cm : cms ) {
    cmIds.add(cm.LeadOrContactId);
}

/* 削除された取引先責任者の中で、削除されたキャンペーンメンバーに含まれているものを取得 */
List<Contact> cons = [SELECT Id, Name, Email FROM Contact WHERE IsDeleted = True AND Id IN :cmIds ALL ROWS];
System.debug(cons.size());
System.debug(cons);

結果は約4千件でメンバーが用意したCSVと件数が一致、内容も一致しました。

先述のコードの SOQL のポイントを少し挙げておきます。

  1. 削除されてごみ箱にいるオブジェクトを SELECT するにはWHERE句で IsDeleted = True を指定し、 SOQLの末尾に ALL ROWS を記述します。
  2. CampaignMember にはリード(Lead)または取引先責任者(Contact)がなりえます。そのため、 CampaignMember.LeadOrContactId にはリードと取引先責任者の双方のIdが入りえます。今回は、取引先責任者だけを対象にしてIdを抽出したかったので、WHERE句に ContactId != null という条件を加えています。
  3. 削除されてごみ箱にいる取引先責任者の中から、削除されたキャンペーンメンバーに一致するものを SELECT するために IN を用いて抽出しています。

さて、復旧ターゲットの List<Contact> が取れましたので、十中八九CPU時間のガバナ制限になってエラーになるだろうと思いつつ、「Open Execute Anonymous Window」で次のコードを実行してみました。

/* 削除されたキャンペーンメンバー(取引先責任者のみ)のIdを取得 */
List<CampaignMember> cms = [SELECT LeadOrContactId FROM CampaignMember WHERE ContactId != null AND IsDeleted = True ALL ROWS];
List<Id> cmIds = new List<Id>();
for ( CampaignMember cm : cms ) {
    cmIds.add(cm.LeadOrContactId);
}

/* 削除された取引先責任者の中で、削除されたキャンペーンメンバーに含まれているものを取得 */
List<Contact> cons = [SELECT Id, Name, Email FROM Contact WHERE IsDeleted = True AND Id IN :cmIds ALL ROWS];

try {
    undelete cons;
} catch (DmlException e) {
    System.debug('復元に失敗しました');
}

これでうまくいけばラッキーなのですが、案の定 Apex CPU time limit exceeded エラーになり復元されませんでした^^;

Apex による Batch 処理の実装

四千件の undelete(復元) 処理は重すぎるので、少ない件数ずつに分解して処理するようにします。 この目的にはApexの一括処理を用います。より具体的には Apex による Batch 処理を実装しました。

まず、下記のDatabase.Batchableインタフェースを実装した CampaignMemberUndeleteBatch クラスを作成します。

public class CampaignMemberUndeleteBatch implements Database.Batchable<sObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        List<CampaignMember> cms = [SELECT LeadOrContactId, LeadOrContactOwnerId FROM CampaignMember WHERE ContactId != null AND IsDeleted = True ALL ROWS];
        List<Id> cmIds = new List<Id>();
        for ( CampaignMember cm : cms ) {
            cmIds.add(cm.LeadOrContactId);
        }
        return Database.getQueryLocator([SELECT Id, Name, Email FROM Contact WHERE IsDeleted = True AND Id IN :cmIds ALL ROWS]);
    }
    public void execute(Database.BatchableContext bc, List<Contact> records){
        try {
            undelete records;
        } catch (DmlException e) {
            System.debug('復元に失敗しました');
        }
    }
    public void finish(Database.BatchableContext bc){
    }
}

startメソッド で復旧ターゲットの List<Contact> を取得する Database.QueryLocator を返すように実装すると、SalesforceのBatch処理が(デフォルトで)200件ずつの復旧ターゲットを対象に executeメソッド を呼び出してくれます。分割された復旧ターゲットは executeメソッド の第二引数(records)で渡されてくるのであとは単純にそれを undelete して復旧するように実装しています。

実装した CampaignMemberUndeleteBatch クラスを組織にデプロイしたら、開発者コンソールの「Open Execute Anonymous Window」を立ち上げ、以下のコードを実行して、Batch処理を起動します。

CampaignMemberUndeleteBatch batch = new CampaignMemberUndeleteBatch();
Id batchId = Database.executeBatch(batch);

Batchの処理状態は batchId を使って、次のSOQLで確認できます。

SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = '<ここにbatchIdを指定>'

処理を開始して数十秒で下記の結果を得ました。

Apex Batch 処理の結果ステータス。Completedになっている。

ステータスが Completed になっていて、 NumberOfErrors0 で全て無事に完了しています。 実際にSalesforce組織にアクセスして、対象の取引先責任者等が復旧できていることも確認できました。

今回は finishメソッド の実装は空にしていますが、長時間かかる見込みの処理の場合はメール送信で完了通知する処理などを実装すると良いかもしれません。

後始末

最後に CampaignMemberUndeleteBatch クラスは使い捨ての実装なので、本番環境から削除して終了です。本番環境からのApexの削除方法については、弊社の清水が記載した記事 「[Salesforce]迷った!困った!Apexクラスの削除のお話」 に詳しく記述されていますので参考にしてください(参考にしました!Thx!)。

なお、今回は使い捨ての実装なのでテストクラスの実装はしませんでしたが、そうでなければ必ず実装するようにしてください。本番組織にコードをリリースできるコードカバレッジの下限75%ギリギリだと、緊急時に使い捨てのクラスを速やかにアップすることができず、この記事のような緊急対応が難しくなるデメリットもあることを覚えておいていただければと思います。

まとめ

  • 大量レコードの処理は workbench では難しい場合がある
  • 削除されてごみ箱にあるレコードの取得もSOQLで可能
  • Apexによる大量レコードの処理はBatchを活用するのが吉