0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Salesforce】カスタムメタデータから取得した値で項目更新をするBatch処理を実装してみた

Posted at

はじめに

今回はカスタムメタデータから取得した値をもとに、Accountレコードの項目を一括更新するバッチ処理の実装例を紹介します。

要件

  • Accountに紐づく求人(JobOffer__c)の「業界」「施設区分」に応じて、カスタムメタデータから画像URLを取得し、Accountの画像URL項目(FacilityImageUrl__c)にランダムで割り当てる
  • 画像URLが未設定かつ、特定の業界・施設区分に該当するAccountのみを対象とする
  • バッチ処理で大量データにも対応できる設計とする

実装方針

Salesforceのバッチクラス(Database.Batchable)を利用し、以下の流れで処理を行います。

  1. 対象となるAccountレコードをSOQLで抽出
  2. Accountごとに最新のJobOffer__cを取得
  3. 業界・施設区分ごとにカスタムメタデータ(FacilityCommonImageGroup__mdt)を取得
  4. Accountに画像URLをランダムで割り当て
  5. 更新対象Accountレコードを更新

コード解説

1. バッチクラスの定義

public virtual class AccountFacilityImageUrlBatch implements Database.Batchable<SObject> {
    private static final String CATEGORY_OTHER = 'その他';
    private static final String DELIMITER = '_';
    private static final Integer DIVISION_KEY_PARTS_SIZE = 2;
    private static final Integer IDX_INDUSTRY = 0;
    private static final Integer IDX_CATEGORY = 1;
    private static final String RECORDTYPE_FACILITY = 'Facility';
    private static final String STATUS_COMMON_IMAGE = 'アップロードなし(共通画像利用)';
    private static final Set<String> TARGET_INDUSTRIES = new Set<String>{'医科', '介護', '保育', '栄養士'};

定数として区切り文字や対象業界などを定義し、可読性・保守性を高めています。

2. Accountレコードの抽出(startメソッド)

public Database.QueryLocator start(Database.BatchableContext bc) {
    return Database.getQueryLocator([
        SELECT Id, FacilityImageUrl__c
        FROM Account
        WHERE RecordType.DeveloperName = :RECORDTYPE_FACILITY
        AND FacilityImageUrl__c = NULL
    ]);
}

画像URL未設定かつ特定レコードタイプのAccountのみを抽出します。

3. executeメソッドの処理フロー

public virtual void execute(Database.BatchableContext bc, List<Account> scope) {
    try {
        if (isTestMode) {
            String testMessage = 'テスト用例外';
            throw new TestBatchException(testMessage);
        }
        // 処理対象のAccountの絞り込み
        Set<Id> accountIds = extractAccountIds(scope);
        Map<Id, JobOffer__c> latestJobOfferMap = fetchLatestJobOfferMap(accountIds);
        List<Account> filteredScope = filterAccounts(scope, latestJobOfferMap);
        Map<Id, String> accountToDivisionKey = buildAccountDivisionKeyMap(filteredScope, latestJobOfferMap);

        // 業界・カテゴリ単位でカスタムメタデータから画像URLリストの組み立て
        Map<String, List<FacilityCommonImageGroup__mdt>> imageGroupMap = buildImageGroupMap(accountToDivisionKey);

        // 画像URLの割り当てと更新対象Accountリストの作成
        List<Account> accountsToUpdate = assignImageUrlAndBuildUpdateList(filteredScope, accountToDivisionKey, imageGroupMap);

        update accountsToUpdate;
    } catch (Exception e) {
        List<Id> scopeIdList = new List<Id>();
        for (Account acc : scope) {
            scopeIdList.add(acc.Id);
        }
        errorEventMessage.addBatchMessage(bc.getJobId(), bc.getChildJobId(), scopeIdList, e);
        throw e;
    }
}

各処理はメソッドで分割されており、可読性・保守性を高めるようにしました。

4. 処理対象のAccountの絞り込み

/**
 * AccountリストからIdのみ抽出する。
 * @param accounts Accountオブジェクトのリスト
 * @return AccountのIdを格納したSet
 */
private Set<Id> extractAccountIds(List<Account> accounts) {
    Set<Id> accountIds = new Set<Id>();
    for (Account acc : accounts) {
        accountIds.add(acc.Id);
    }
    return accountIds;
}

/**
 * Account Idに紐づく最新のJobOffer__cをMapで取得する。
 * @param accountIds AccountのIdを格納したSet
 * @return Account Idをキー、最新のJobOffer__cを値とするMap
 */
private Map<Id, JobOffer__c> fetchLatestJobOfferMap(Set<Id> accountIds) {
    Map<Id, JobOffer__c> latestJobOfferMap = new Map<Id, JobOffer__c>();
    for (JobOffer__c job : [
        SELECT Id, Industry__c, FacilityCategory__c, Institution__c
        FROM JobOffer__c
        WHERE Institution__c IN :accountIds
        AND Industry__c IN :TARGET_INDUSTRIES
        ORDER BY CreatedDate DESC
    ]) {
        if (!latestJobOfferMap.containsKey(job.Institution__c)) {
            latestJobOfferMap.put(job.Institution__c, job);
        }
    }
    return latestJobOfferMap;
}

/**
 * Accountを業界・施設区分・画像URL条件で絞り込む。
 * @param accounts Accountオブジェクトのリスト
 * @param latestJobOfferMap Account Idをキー、最新のJobOffer__cを値とするMap
 * @return 条件に合致したAccountオブジェクトのリスト
 */
private List<Account> filterAccounts(List<Account> accounts, Map<Id, JobOffer__c> latestJobOfferMap) {
    List<Account> filteredScope = new List<Account>();
    for (Account acc : accounts) {
        JobOffer__c jobOffer = latestJobOfferMap.get(acc.Id);
        if (jobOffer != null
            && acc.FacilityImageUrl__c == null
            && String.isNotBlank(jobOffer.FacilityCategory__c)) {
            filteredScope.add(acc);
        }
    }
    return filteredScope;
}

/**
 * Accountリストから、AccountのIdと「業界_施設区分」の対応Mapを生成する。
 * @param accounts Accountオブジェクトのリスト
 * @param jobOfferMap Account Idをキー、最新のJobOffer__cを値とするMap
 * @return AccountのIdをキー、「業界_施設区分」(例: 介護_デイケア)を値とするMap
 */
private Map<Id, String> buildAccountDivisionKeyMap(List<Account> accounts, Map<Id, JobOffer__c> jobOfferMap) {
    Map<Id, String> accountToDivisionKey = new Map<Id, String>();
    for (Account acc : accounts) {
        JobOffer__c jobOffer = jobOfferMap.get(acc.Id);
        if (jobOffer != null) {
            String key = jobOffer.Industry__c + DELIMITER + jobOffer.FacilityCategory__c;
            accountToDivisionKey.put(acc.Id, key);
        }
    }
    return accountToDivisionKey;
}

5. 業界・カテゴリ単位でカスタムメタデータから画像URLリストの組み立て

/**
* 業界・施設区分キーのMapから画像グループMapを構築する。
* @param accountToDivisionKey AccountのIdをキー、「業界_施設区分」を値とするMap
* @return 業界_施設区分キーをキー、FacilityCommonImageGroup__mdtリストを値とするMap
*/
private Map<String, List<FacilityCommonImageGroup__mdt>> buildImageGroupMap(Map<Id, String> accountToDivisionKey) {
    Set<String> industryCategoryKeySet = new Set<String>(accountToDivisionKey.values());
    Set<String> industries = extractIndustries(industryCategoryKeySet);
    Set<String> categories = extractCategories(industryCategoryKeySet);

    List<FacilityCommonImageGroup__mdt> mdListAll = fetchMetaDataList(industries, categories);
    return buildMetaDataMapFromList(mdListAll);
}

/**
 * accountToDivisionsKeyから指定インデックスのSetを抽出する共通関数。
 * @param industryCategoryKeySet 業界_施設区分キーのSet
 * @param splitIdx 定数で指定したインデックス
 * @return 指定インデックスの値を格納したSet
 */
private Set<String> extractDivisionKey(Set<String> industryCategoryKeySet, Integer splitIdx) {
    Set<String> result = new Set<String>();
    for (String key : industryCategoryKeySet) {
        List<String> parts = key.split(DELIMITER);
        if (parts.size() == DIVISION_KEY_PARTS_SIZE) {
            result.add(parts[splitIdx]);
        }
    }
    return result;
}

/**
 * 共通関数を利用して、accountToDivisionsKeyから業界Setを抽出する関数。
 * @param industryCategoryKeySet 業界_施設区分キーのSet
 * @return 業界Set
 */
private Set<String> extractIndustries(Set<String> industryCategoryKeySet) {
    return extractDivisionKey(industryCategoryKeySet, IDX_INDUSTRY);
}

/**
 * 共通関数を利用して、accountToDivisionsKeyから施設区分Setを抽出後、'その他'を追加して返す関数。
 * @param industryCategoryKeySet 業界_施設区分キーのSet
 * @return 施設区分Set('その他'を含む)
 */
private Set<String> extractCategories(Set<String> industryCategoryKeySet) {
    Set<String> categories = extractDivisionKey(industryCategoryKeySet, IDX_CATEGORY);
    categories.add(CATEGORY_OTHER);
    return categories;
}

/**
 * FacilityCommonImageGroup__mdtのリストを取得する。
 * @param industries 業界Set
 * @param categories 施設区分Set
 * @return FacilityCommonImageGroup__mdtのリスト
 */
private List<FacilityCommonImageGroup__mdt> fetchMetaDataList(Set<String> industries, Set<String> categories) {
    return [
        SELECT DeveloperName, Images__c, Industry__c, AccountDivision__c
        FROM FacilityCommonImageGroup__mdt
        WHERE Industry__c IN :industries
        AND AccountDivision__c IN :categories
    ];
}

/**
 * FacilityCommonImageGroup__mdtリストから画像情報Mapを構築する。
 * @param mdListAll FacilityCommonImageGroup__mdtのリスト
 * @return 業界_施設区分キーをキー、FacilityCommonImageGroup__mdtリストを値とするMap
 */
private Map<String, List<FacilityCommonImageGroup__mdt>> buildMetaDataMapFromList(List<FacilityCommonImageGroup__mdt> mdListAll) {
    Map<String, List<FacilityCommonImageGroup__mdt>> imageGroupMap = new Map<String, List<FacilityCommonImageGroup__mdt>>();
    for (FacilityCommonImageGroup__mdt md : mdListAll) {
        String key = md.Industry__c + DELIMITER + md.AccountDivision__c;
        if (!imageGroupMap.containsKey(key)) {
            imageGroupMap.put(key, new List<FacilityCommonImageGroup__mdt>());
        }
        imageGroupMap.get(key).add(md);
    }
    return imageGroupMap;
}

6.画像URLの割り当てと更新対象Accountリストの作成

/**
 * Accountごとに業界_施設区分キーで画像グループリストを取得し、該当がなければ「その他」区分で再取得する。
 * 画像URLと施設画像アップロードステータスを割り当て、更新対象Accountリストを作成する。
 * @param filteredScope 条件に合致したAccountオブジェクトのリスト
 * @param accountToDivisionKey AccountのIdをキー、「業界_施設区分」を値とするMap
 * @param imageGroupMap 業界_施設区分キーをキー、FacilityCommonImageGroup__mdtリストを値とするMap
 * @return 更新対象Accountオブジェクトのリスト
 */
private List<Account> assignImageUrlAndBuildUpdateList(
    List<Account> filteredScope,
    Map<Id, String> accountToDivisionKey,
    Map<String, List<FacilityCommonImageGroup__mdt>> imageGroupMap
) {
    List<Account> accountsToUpdate = new List<Account>();
    for (Account acc : filteredScope) {
        String key = accountToDivisionKey.get(acc.Id);
        if (String.isBlank(key)) continue;

        List<FacilityCommonImageGroup__mdt> mdList = imageGroupMap.get(key);
        if (mdList == null || mdList.isEmpty()) {
            List<String> parts = key.split(DELIMITER);
            if (parts.size() == DIVISION_KEY_PARTS_SIZE) {
                String otherKey = parts[IDX_INDUSTRY] + DELIMITER + CATEGORY_OTHER;
                mdList = imageGroupMap.get(otherKey);
            }
            if (mdList == null || mdList.isEmpty()) continue;
        }

        if (assignRandomImageUrlFromList(acc, key, mdList)) {
            acc.FacilityImageUploadStatus__c = STATUS_COMMON_IMAGE;
            accountsToUpdate.add(acc);
        }
    }
    return accountsToUpdate;
}

/**
 * 画像グループリストからランダムに画像URLをAccountに割り当てる。
 * @param acc Accountオブジェクト
 * @param key 業界_施設区分キー
 * @param mdList FacilityCommonImageGroup__mdtのリスト
 * @return 割り当て成功時はtrue、失敗時はfalse
 */
private Boolean assignRandomImageUrlFromList(
    Account acc,
    String key,
    List<FacilityCommonImageGroup__mdt> mdList
) {
    Integer randomIndex = (Integer)Math.floor(Math.random() * mdList.size());
    FacilityCommonImageGroup__mdt md = mdList[randomIndex];
    if (md == null) return false;

    acc.ImageServerImageUrl__c = md.Images__c;
    return true;
}

まとめ

今回の実装では、カスタムメタデータを活用した柔軟な画像URL割り当てバッチ処理を実現しました。
Salesforceのバッチ処理は、SOQLガバナ制限や大量データ対応、保守性を意識した設計が重要です。
本記事のサンプルを参考に、業務要件に合わせたバッチ処理の設計・実装にぜひチャレンジしてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?