Apexクラスをマスターする:スケーラブルで保守性の高いコードを作成するためのSalesforce開発者ガイド

背景と応用シナリオ

Salesforceプラットフォームで働く開発者として、私たちは日々、Apexという強力なツールを駆使しています。Apexは、Salesforceが提供する、静的型付けのオブジェクト指向プログラミング言語(Object-Oriented Programming language)であり、Force.comプラットフォームのサーバー上で実行されます。この言語の中核をなすのがApexクラスです。

Apexクラスは、オブジェクトを作成するための設計図またはテンプレートです。特定のビジネスロジック、データ処理、他のシステムとの連携など、Salesforceの標準機能だけでは実現できない複雑な要件を満たすために不可欠な要素です。

主な応用シナリオ:

  • カスタムビジネスロジックの実装:トリガー(Triggers)から呼び出されるハンドラークラスを作成し、レコードの作成・更新・削除時に複雑な検証やデータ操作を実行します。
  • カスタムUIのバックエンド処理:Lightning Web Components (LWC) や Aura コンポーネント、Visualforceページから呼び出されるコントローラーとして機能し、データの取得や更新を行います。
  • 非同期処理:大量のデータを処理するためのバッチApex(Batch Apex)、将来の特定の時間に実行するスケジュールApex(Scheduled Apex)、または外部システムへのコールアウトなど、長時間実行される可能性のある処理を非同期で実行します。
  • Webサービスの構築:RESTful APIやSOAP Webサービスを公開し、外部システムがSalesforceのデータやビジネスロジックに安全にアクセスできるようにします。

堅牢でスケーラブルなアプリケーションを構築するためには、適切に設計されたApexクラスが土台となります。この記事では、Salesforce開発者の視点から、保守性が高く、パフォーマンスに優れたApexクラスを作成するための原理、ベストプラクティス、そして具体的なコード例を解説します。


原理説明

優れたApexクラスを作成するためには、オブジェクト指向プログラミング(OOP)の基本原則を理解することが重要です。ApexはJavaに似た構文を持ち、OOPの主要な概念をサポートしています。

クラスとオブジェクト

クラス(Class)は、プロパティ(変数)とメソッド(関数)をまとめたテンプレートです。例えば、「AccountHandler」というクラスは、取引先オブジェクトに関連するロジックをカプセル化するための設計図となります。一方、オブジェクト(Object)は、そのクラスから作成された具体的なインスタンスです。AccountHandler handler = new AccountHandler();というコードは、AccountHandlerクラスの新しいインスタンス(オブジェクト)を作成しています。

アクセス修飾子

Apexでは、クラス、プロパティ、メソッドの可視性を制御するためにアクセス修飾子(Access Modifiers)を使用します。これにより、カプセル化(Encapsulation)が促進され、意図しない外部からのアクセスを防ぎます。

  • private: そのクラス内からのみアクセス可能です。最も厳格な修飾子です。
  • public: 同じ名前空間内のどのApexコードからでもアクセス可能です。
  • global: すべてのApexコードからアクセス可能です。名前空間を越えてアクセスする必要がある場合(例:管理パッケージのAPI)に使用されます。
  • protected: そのクラス、またはそのクラスを拡張(extends)したサブクラス内からのみアクセス可能です。

共有モデルのキーワード

Salesforceは厳格なデータセキュリティモデルを持っています。Apexクラスが実行される際のレコード共有ルールを明示的に定義することが極めて重要です。

  • with sharing: クラスは現在のユーザーの共有ルールを強制します。ユーザーが表示できないレコードは、このクラス内のSOQLクエリでも返されません。
  • without sharing: クラスは共有ルールを無視して実行されます。システムコンテキストで動作し、すべてのレコードにアクセスできる可能性があります。慎重に使用する必要があります。
  • inherited sharing: クラスは、それを呼び出したクラスの共有コンテキストを継承します。柔軟性が高く、多くのシナリオで推奨される選択肢です。

単一責任の原則

優れたソフトウェア設計の原則の一つに、単一責任の原則(Single Responsibility Principle)があります。これは、「 mộtクラスは一つの、そして唯一の責任を持つべきである」という考え方です。例えば、トリガーロジック、データクエリ、外部連携のロジックをすべて一つの巨大なクラスに詰め込むのではなく、それぞれを専門のクラス(例:Trigger Handler, Selector Class, Service Class)に分割することで、コードの可読性、保守性、再利用性が劇的に向上します。


示例代码

ここでは、Salesforceの公式ドキュメントに基づいた、実践的なシナリオでのApexクラスのコード例をいくつか紹介します。

例1:一括処理を考慮したトリガーハンドラークラス

トリガーはレコードのリストを処理するように設計されるべきです。ロジックをトリガーファイルからハンドラークラスに分離するのはベストプラクティスです。

// 取引先 (Account) の更新を処理するハンドラークラス
public with sharing class AccountTriggerHandler {

    // after update イベントで呼び出される静的メソッド
    // List<Account> を受け取ることで、一括処理 (Bulkification) に対応
    public static void afterUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        
        List<Task> tasksToInsert = new List<Task>();
        
        // 更新された各取引先をループ処理
        for (Account acct : newAccounts) {
            // 古い値を取得するために oldAccountMap を使用
            Account oldAcct = oldAccountMap.get(acct.Id);
            
            // 特定の条件(例:年間売上が変更された)をチェック
            if (acct.AnnualRevenue != oldAcct.AnnualRevenue) {
                // 新しいタスクを作成し、リストに追加
                Task newTask = new Task(
                    Subject = 'Follow up on Annual Revenue change',
                    WhatId = acct.Id
                );
                tasksToInsert.add(newTask);
            }
        }
        
        // ループの外で一度だけDML操作を実行する
        // これによりガバナ制限(Governor Limits)の消費を最小限に抑える
        if (!tasksToInsert.isEmpty()) {
            try {
                insert tasksToInsert;
            } catch (DmlException e) {
                // エラー処理:挿入に失敗した場合のロジックをここに追加
                System.debug('Could not insert tasks. Error: ' + e.getMessage());
            }
        }
    }
}

このクラスを呼び出すトリガーは非常にシンプルになります。

trigger AccountTrigger on Account (after update) {
    if (Trigger.isAfter && Trigger.isUpdate) {
        // ハンドラークラスのメソッドにトリガーコンテキスト変数を渡す
        AccountTriggerHandler.afterUpdate(Trigger.new, Trigger.oldMap);
    }
}

例2:Queueable Apexによる非同期処理

外部システムへのコールアウトや複雑な計算など、時間がかかる処理は非同期で行うべきです。Queueableインターフェースを実装することで、トランザクションを分離し、ガバナ制限を緩和できます。

// Queueableインターフェースを実装したクラス
public class UpdateAccountRatingQueueable implements Queueable {

    private List<Id> accountIds;

    // コンストラクタで処理対象のレコードIDリストを受け取る
    public UpdateAccountRatingQueueable(List<Id> ids) {
        this.accountIds = ids;
    }

    // Queueableインターフェースで必須の execute メソッド
    // このメソッド内のロジックが非同期で実行される
    public void execute(QueueableContext context) {
        // IDに基づいて取引先レコードを取得
        List<Account> accounts = [SELECT Id, AnnualRevenue FROM Account WHERE Id IN :this.accountIds];

        for (Account acc : accounts) {
            // ビジネスロジック:年間売上に基づいて評価 (Rating) を更新
            if (acc.AnnualRevenue > 1000000) {
                acc.Rating = 'Hot';
            } else {
                acc.Rating = 'Warm';
            }
        }

        // DML操作を実行
        // 非同期コンテキストでは、同期トランザクションとは別のガバナ制限が適用される
        try {
            update accounts;
        } catch (DmlException e) {
            // 適切なエラーログや通知のロジックを実装
            System.debug('Error updating accounts: ' + e.getMessage());
        }
    }
}

このQueueableジョブを呼び出すには、以下のようにSystem.enqueueJobを使用します。

// 処理したい取引先のIDリスト
List<Id> accountIdsToProcess = new List<Id>{ '001...' , '001...' };

// Queueableクラスのインスタンスを作成
UpdateAccountRatingQueueable job = new UpdateAccountRatingQueueable(accountIdsToProcess);

// ジョブをキューに追加して非同期実行を開始
Id jobId = System.enqueueJob(job);

注意事項

Apexクラスを開発する際には、Salesforceプラットフォーム特有の制約や考慮事項を常に念頭に置く必要があります。

ガバナ制限 (Governor Limits)

Salesforceはマルチテナント環境であるため、すべての組織がリソースを公平に利用できるよう、厳格なガバナ制限が設けられています。

  • SOQLクエリ:1回のトランザクションで発行できるSOQLクエリは100回までです。ループ内でSOQLクエリを実行するのは最も避けるべきアンチパターンです。
  • DMLステートメント:1回のトランザクションで実行できるDML操作(insert, update, delete)は150回までです。これも同様に、ループ内での実行は避けるべきです。
  • CPU時間:1回のトランザクションで消費できるCPU時間にも制限があります(同期処理で10秒)。非効率なループや複雑な計算は、この制限に抵触する可能性があります。
常に一括処理(Bulkification)を意識し、一度に複数のレコードを効率的に処理するコードを書いてください。

セキュリティと共有 (Security and Sharing)

with sharingキーワードを適切に使用し、ユーザーの権限を尊重することはセキュリティの基本です。しかし、それだけでは不十分な場合があります。特に、動的SOQLや汎用的なコンポーネントを開発する際は、項目レベルセキュリティ(Field-Level Security, FLS)やオブジェクト権限をコードレベルでチェックすることが推奨されます。Schema.DescribeSObjectResultisAccessible(), isUpdateable()などのメソッドを活用してください。

エラー処理 (Error Handling)

堅牢なアプリケーションは、予期せぬエラーを適切に処理できなければなりません。

  • try-catchブロック:DML操作やSOQLクエリ、外部コールアウトなど、失敗する可能性のある処理は必ずtry-catchブロックで囲みます。
  • Databaseメソッド:Database.insert(records, false)のように、第二引数にfalseを指定すると、一部のレコードが失敗してもトランザクション全体がロールバックされるのを防ぎ、成功したレコードと失敗したレコードの結果をDatabase.SaveResultオブジェクトで受け取ることができます。
  • カスタム例外:独自の例外クラス(Exceptionクラスを拡張)を作成することで、アプリケーション固有のエラーをより明確に表現し、ハンドリングしやすくなります。

テストカバレッジ (Test Coverage)

本番環境にApexコードをデプロイするには、最低でも75%のコードカバレッジが必要です。しかし、この数値を満たすことだけが目的ではありません。テストクラスの真の目的は、コードの品質を保証することです。

  • アサーション:System.assertEquals()などを使用して、コードが期待通りに動作することを検証します。
  • 一括処理のテスト:単一レコードだけでなく、200レコードのような大量のデータを処理するシナリオをテストします。
  • ポジティブ/ネガティブシナリオ:成功するケースと、意図的に失敗するケース(必須項目がnullの場合など)の両方をテストします。

まとめとベストプラクティス

Apexクラスは、Salesforceプラットフォームの能力を最大限に引き出すための鍵です。この記事で解説した原則と実践方法を適用することで、スケーラブルで、保守性が高く、安全なアプリケーションを構築することができます。

ベストプラクティス一覧:

  • コードの一括処理 (Bulkify Your Code):

    常にレコードのリストを念頭に置いて設計し、ループ内でのSOQLやDMLは絶対に避けてください。

  • 1オブジェクトにつき1つのトリガー (One Trigger Per Object):

    トリガー自体はシンプルに保ち、実際のロジックはハンドラークラスやヘルパークラスに委譲します。

  • 関心の分離 (Separation of Concerns):

    ロジックを機能ごとにクラスに分割します(例:サービスクラス、セレクタークラス)。これにより、再利用性とテストのしやすさが向上します。

  • ハードコーディングされたIDの回避 (Avoid Hardcoded IDs):

    レコードIDやURLなどをコードに直接書き込むのではなく、カスタムメタデータ型、カスタム設定、またはカスタム表示ラベルを使用して動的に管理します。

  • 非同期Apexの活用 (Leverage Asynchronous Apex):

    リソースを大量に消費する処理や、即時完了する必要のない処理には、Future、Queueable、Batch、Scheduled Apexを積極的に利用します。

  • 意味のあるコードを書く (Write Meaningful Code):

    変数名やメソッド名は、その目的が明確にわかるように命名し、複雑なロジックには適切なコメントを追加して、将来の自分や他の開発者を助けましょう。

これらのベストプラクティスを日々の開発業務に取り入れることで、あなたはより優れたSalesforce開発者となり、顧客にとって価値のある、長期的に成功するソリューションを提供できるようになるでしょう。

コメント