はじめに
今回はカスタムメタデータから取得した値をもとに、Accountレコードの項目を一括更新するバッチ処理の実装例を紹介します。
要件
- Accountに紐づく求人(JobOffer__c)の「業界」「施設区分」に応じて、カスタムメタデータから画像URLを取得し、Accountの画像URL項目(FacilityImageUrl__c)にランダムで割り当てる
- 画像URLが未設定かつ、特定の業界・施設区分に該当するAccountのみを対象とする
- バッチ処理で大量データにも対応できる設計とする
実装方針
Salesforceのバッチクラス(Database.Batchable)を利用し、以下の流れで処理を行います。
- 対象となるAccountレコードをSOQLで抽出
- Accountごとに最新のJobOffer__cを取得
- 業界・施設区分ごとにカスタムメタデータ(FacilityCommonImageGroup__mdt)を取得
- Accountに画像URLをランダムで割り当て
- 更新対象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ガバナ制限や大量データ対応、保守性を意識した設計が重要です。
本記事のサンプルを参考に、業務要件に合わせたバッチ処理の設計・実装にぜひチャレンジしてみてください。