⚠️

[Salesforce] Apexで汎用的なバリデーションの仕組みを作る

2025/03/24に公開

株式会社TERASSでSalesforceをやったりしているimslpです。

はじめに

皆さんはSalesforceで入力検証を行う時、何で検証していますか?
私はもっぱらオブジェクトの入力規則を利用しています。実装も有効・無効の切り替えもブラウザ上で完結できる楽さが何より気に入っています。

...しかし、個人的によく遭遇する制限がありました。
それは「オブジェクトのリレーションを15個までしか使えない」ということです。
しかもこれは入力規則に限った話ではなく、数式項目などとも合わせての数です。
参考:
https://help.salesforce.com/s/articleView?id=000382367&type=1

なんでもかんでも入力規則で実装しようとすると痛い目を見るわけです。
というわけで今回は、汎用的に適用可能なApexのバリデーションの仕組みを作ってみましたので紹介です。

実装の要件

  • オブジェクトごとにバリデーションルールを定義し、柔軟に適用できるようにする
  • 新規作成と更新、またはどちらもの場合で異なるルールを適用可能にする
  • バリデーションのロジックを共通化し、拡張しやすい設計にする

実装の全体像

このバリデーション仕組みは、以下の3つの主要なクラスで構成されます。

  1. SObjectValidator(SObjectのバリデーションを行う抽象クラス)
  2. SObjectValidationRule(SObjectのバリデーションルールを定義する抽象クラス)
  3. CaseValidator & CaseValidationRule(Caseオブジェクトのバリデーションルール)

SObjectのバリデーションを行う抽象クラス

public inherited sharing abstract class SObjectValidator {
  protected final List<SObject> newRecords;
  protected final Map<Id, SObject> oldRecordsMap;

  // コンストラクタ
  protected SObjectValidator(List<SObject> newRecords, Map<Id, SObject> oldRecordsMap) {
    this.newRecords = newRecords;
    this.oldRecordsMap = oldRecordsMap != null ? oldRecordsMap : new Map<Id, SObject>();
  }

  // エントリーポイントの抽象メソッド
  public abstract void runValidation();

  // 渡されたルールのバリデーションを実行する
  protected void applyValidation(SObjectValidationRule rule) {
    rule.validate(newRecords, oldRecordsMap);
  }
}

SObjectのバリデーションルールを定義する抽象クラス

public abstract class SObjectValidationRule {
  // 新規作成時のバリデーションルールを定義する抽象メソッド
  protected abstract void forInsert(SObject newRecord);

  // 更新時のバリデーションルールを定義する抽象メソッド
  protected abstract void forUpdate(SObject newRecord, SObject oldRecord);

  // 作成時と更新時の両方で適用するバリデーションルールを定義する抽象メソッド
  protected abstract void forBoth(SObject newRecord, SObject oldRecord);

  // バリデーションを実行する
  public void validate(List<SObject> newRecords, Map<Id, SObject> oldRecordsMap) {
    if (newRecords == null || newRecords.isEmpty()) {
      return;
    }

    for (SObject newRecord : newRecords) {
      SObject oldRecord = oldRecordsMap.get(newRecord.Id);

      if (oldRecord == null) {
        // 新規作成時のバリデーション
        forInsert(newRecord);
      } else {
        // 更新時のバリデーション
        forUpdate(newRecord, oldRecord);
      }

      // 共通のバリデーション
      forBoth(newRecord, oldRecord);
    }
  }
}

Caseオブジェクトのバリデーション

public class CaseValidator extends SObjectValidator {
  private CaseValidator(List<Case> newCases, Map<Id, Case> fetchedCasesMap) {
    super((List<SObject>) newCases, (Map<Id, SObject>) fetchedCasesMap);
  }

  /**
   * (insert用)ファクトリーメソッド
   */
  public static CaseValidator of(List<Case> newCases) {
    return new CaseValidator(newCases, new Map<Id, Case>());
  }

  /**
   * (update用)ファクトリーメソッド
   */
  public static CaseValidator of(List<Case> newCases, Map<Id, Case> fetchedCasesMap) {
    return new CaseValidator(newCases, fetchedCasesMap);
  }

  /**
   * エントリーポイント
   */
  public override void runValidation() {
    applyValidation(new CaseValidationRule());
  }
}

Caseオブジェクトのバリデーション

ここのそれぞれのメソッドに実際のバリデーションルールを記載します。

public class CaseValidationRule extends SObjectValidationRule {
  // 新規作成時用のバリデーションルール
  protected override void forInsert(SObject newRecord) {
    Case newCase = (Case) newRecord;
  }

  // 更新時用のバリデーションルール
  protected override void forUpdate(SObject newRecord, SObject fetchedRecord) {
    Case newCase = (Case) newRecord;
    Case fetchedCase = (Case) fetchedRecord;
  }

  // 作成時と更新時の両方で適用するバリデーションルール
  protected override void forBoth(SObject newRecord, SObject fetchedRecord) {
    Case newCase = (Case) newRecord;
    Case fetchedCase = (Case) fetchedRecord;

    // 必須項目のバリデーションなどを実装
  }
}

バリデーションの実行方法

トリガから呼び出す場合はこんな感じになると思います。

trigger CaseTrigger on Case(before insert, before update, after insert, after update) {
  if (Trigger.isBefore && Trigger.isInsert) {
    CaseValidator.of(Trigger.new).runValidation();
  }
  if (Trigger.isBefore && Trigger.isUpdate) {
    CaseValidator.of(Trigger.new, Trigger.oldMap).runValidation();
  }
}

おわり

これがちゃんと運用できれば、入力規則の制限に悩まされることもなくなると信じています。
間違っている箇所や最適化できる部分は色々あるかもしれませんがご容赦ください。
ここまで見てくださりありがとうございました。

Terass Tech Blog

Discussion