Salesforce Dynamic Apex の習得:柔軟でスケーラブルなアプリケーション開発


背景と適用シナリオ

Salesforce プラットフォーム上で Apex を使用して開発を行う際、通常はコード内で操作する SObject や項目をコンパイル時に直接指定します。これを静的 Apex (Static Apex) と呼びます。例えば、Account acc = [SELECT Id, Name FROM Account WHERE Id = :someId]; のようなコードは、コンパイル時に Account オブジェクトと Id, Name 項目が存在するかどうかをシステムが検証します。これにより、コードの安全性とパフォーマンスが保証されます。

しかし、アプリケーションの要件によっては、実行時まで操作対象のオブジェクトや項目が確定しない場合があります。このようなシナリオで静的 Apex は対応できません。そこで登場するのが Dynamic Apex (動的 Apex) です。

Dynamic Apex は、コードの実行時に SObject や項目の名前を文字列として扱い、動的にクエリを生成したり、レコードを操作したりする機能を提供します。これにより、非常に柔軟で再利用性の高いアプリケーションを構築することが可能になります。

主な適用シナリオ

  • 管理パッケージ (Managed Packages) の開発: インストール先の組織に存在するカスタムオブジェクトやカスタム項目を操作する必要がある場合。パッケージ開発者は事前にそれらの名前を知ることができないため、Dynamic Apex を使用して対応します。
  • 汎用的なコンポーネントの開発: 特定のオブジェクトに依存しない、データグリッドやレコード詳細ビューアなどの再利用可能な Lightning Web Components (LWC) や Aura コンポーネントのバックエンド処理。
  • 設定駆動型のアプリケーション: 操作対象のオブジェクト名や項目名をカスタムメタデータ型 (Custom Metadata Type) やカスタム設定 (Custom Setting) に保存し、管理者が設定を変更するだけでアプリケーションの動作を変えたい場合。
  • 動的なデータ連携ツール: 外部システムとの連携において、マッピング定義に基づいて動的に SOQL クエリを生成し、データを取得・更新するツール。

原理説明

Dynamic Apex は主に3つの主要な技術要素で構成されています。これらを組み合わせることで、動的なアプリケーションロジックを実現します。

1. スキーマ記述 (Describe Framework)

Dynamic Apex の根幹をなすのがスキーマ記述機能です。これは、Apex コードの実行時に Salesforce 組織のオブジェクト、項目、リレーションなどのメタデータ情報をプログラム的に取得するための仕組みです。Schema クラスがその中心的な役割を担います。

  • Schema.getGlobalDescribe(): 組織内のすべての SObject に関する情報を Map<String, Schema.SObjectType> 形式で返します。キーは SObject の API 参照名(小文字)です。
  • Schema.SObjectType: 特定の SObject のメタデータを表します。このオブジェクトから getDescribe() メソッドを呼び出すことで、詳細な記述結果を取得できます。
  • Schema.DescribeSObjectResult: オブジェクトのラベル名、API 参照名、作成可能か、更新可能かといったプロパティや、すべての項目のリストなどを保持します。
  • Schema.DescribeFieldResult: 項目のデータ型、ラベル、長さ、アクセス権限(参照可能か、作成可能か)などの詳細な情報を提供します。

このフレームワークにより、「指定された文字列名のオブジェクトは存在するか?」「そのオブジェクトに特定の項目はあるか?」「現在のユーザーはその項目にアクセスできるか?」といったことを実行時に確認できます。

2. Dynamic SOQL

Dynamic SOQL は、SOQL クエリを文字列として構築し、実行時にデータベースに問い合わせる機能です。静的な SOQL が [...] で囲まれるのに対し、Dynamic SOQL は Database.query(queryString) メソッドを使用します。

例えば、'SELECT Id, Name FROM Account' という文字列を生成し、このメソッドに渡すことでクエリが実行されます。文字列であるため、変数を使ってオブジェクト名、項目リスト、WHERE 句などを動的に組み立てることが可能です。これにより、ユーザーの入力や設定に応じて、実行するクエリを自由に変更できます。

ただし、この柔軟性には SOQL インジェクション (SOQL Injection) という重大なセキュリティリスクが伴います。これについては「注意事項」のセクションで詳しく解説します。

3. Dynamic DML

Dynamic DML は、SObject を動的にインスタンス化し、項目値を設定・取得する機能です。特定の SObject クラス(例:Account)にキャストすることなく、汎用的な sObject 型としてレコードを扱います。

  • インスタンス化: Schema.getGlobalDescribe().get('Account').newSObject()Type.forName('Schema.Account').newInstance() を使用して、オブジェクト名を文字列で指定し、新しい SObject インスタンスを作成します。
  • 項目の設定と取得: sObject.put(fieldName, value) メソッドで項目値を設定し、sObject.get(fieldName) メソッドで値を取得します。fieldName は文字列です。

このようにして作成・操作された sObject インスタンスは、通常の DML ステートメント(insert, update, delete)でデータベースに保存できます。


示例代码

ここでは、Dynamic Apex の具体的な使用方法を、Salesforce 公式ドキュメントに基づいたコード例で示します。

例1: 動的なオブジェクトと項目を指定してクエリを実行する

この例では、オブジェクト名と取得したい項目リストを文字列で受け取り、Dynamic SOQL を使してデータを取得する汎用的なメソッドを実装します。スキーマ記述を使用して、指定された項目がアクセス可能かどうかを事前にチェックしています。

public class DynamicQueryBuilder {
    public static List<sObject> performQuery(String objectName, List<String> fieldsToQuery) {
        // 1. 指定されたオブジェクトのメタデータを取得
        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objectName);
        if (targetType == null) {
            // オブジェクトが存在しない場合は例外をスロー
            throw new CalloutException('Invalid SObject name: ' + objectName);
        }
        Schema.DescribeSObjectResult describeResult = targetType.getDescribe();

        // 2. アクセス可能な項目のみをリストアップ
        List<String> accessibleFields = new List<String>();
        Map<String, Schema.SObjectField> fieldMap = describeResult.fields.getMap();

        for (String fieldName : fieldsToQuery) {
            // 項目が存在し、かつ現在のユーザーが参照可能な場合のみクエリ対象に含める
            if (fieldMap.containsKey(fieldName.toLowerCase())) {
                Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName.toLowerCase()).getDescribe();
                if (fieldDescribe.isAccessible()) {
                    accessibleFields.add(fieldDescribe.getName());
                }
            }
        }

        if (accessibleFields.isEmpty()) {
            // クエリ可能な項目が一つもない場合は例外をスロー
            throw new CalloutException('No accessible fields found for the query.');
        }

        // 3. Dynamic SOQL クエリ文字列を構築
        String queryString = 'SELECT ' + String.join(accessibleFields, ', ') +
                             ' FROM ' + describeResult.getName();

        // 4. Database.query を使用してクエリを実行
        try {
            return Database.query(queryString);
        } catch (QueryException e) {
            System.debug('SOQL Query failed: ' + queryString);
            System.debug(e.getMessage());
            throw e;
        }
    }
}

例2: 汎用的なレコード作成

この例では、オブジェクト名と項目値のマップを受け取り、Dynamic DML を使用して新しいレコードを作成します。項目レベルのセキュリティ(作成権限)をチェックすることで、安全なレコード作成を実現します。

public class GenericRecordCreator {
    public static Id createRecord(String objectName, Map<String, Object> fieldValues) {
        // 1. グローバル Describe から SObjectType を取得
        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objectName);
        if (targetType == null) {
            throw new CalloutException('SObject ' + objectName + ' not found.');
        }

        // 2. オブジェクトの作成権限を確認
        Schema.DescribeSObjectResult describeResult = targetType.getDescribe();
        if (!describeResult.isCreateable()) {
            throw new SecurityException('User does not have permission to create ' + objectName);
        }

        // 3. 新しい sObject インスタンスを作成
        sObject newRecord = targetType.newSObject();

        // 4. マップ内の各項目について、権限を確認しながら値を設定
        Map<String, Schema.SObjectField> fieldMap = describeResult.fields.getMap();
        for (String fieldName : fieldValues.keySet()) {
            Schema.SObjectField field = fieldMap.get(fieldName.toLowerCase());
            
            // 項目が存在し、かつ作成可能かチェック
            if (field != null && field.getDescribe().isCreateable()) {
                newRecord.put(field.getDescribe().getName(), fieldValues.get(fieldName));
            } else {
                System.debug('Skipping inaccessible or non-existent field: ' + fieldName);
            }
        }

        // 5. DML 操作を実行
        try {
            Database.SaveResult sr = Database.insert(newRecord, false); // allOrNone=false
            if (sr.isSuccess()) {
                return sr.getId();
            } else {
                for(Database.Error err : sr.getErrors()) {
                    System.debug('Error creating record: ' + err.getStatusCode() + ': ' + err.getMessage());
                    System.debug('Fields that affected error: ' + err.getFields());
                }
                return null;
            }
        } catch (DmlException e) {
            System.debug('DML Exception: ' + e.getMessage());
            throw e;
        }
    }
}

注意事項

Dynamic Apex は強力なツールですが、その力を正しく使うためにはいくつかの重要な注意点を理解しておく必要があります。

1. SOQL インジェクション (SOQL Injection)

これは Dynamic SOQL を使用する上で最も重大なセキュリティリスクです。ユーザーが入力した値をエスケープ処理せずに直接クエリ文字列に連結すると、攻撃者が意図しないクエリを実行できてしまいます。

悪い例:

String userInput = 'Test\') OR Name != \'';
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
List<Account> accs = Database.query(query); // 意図せず全てのレコードが返る

対策: ユーザーからの入力を WHERE 句などのリテラル値として使用する場合は、必ず String.escapeSingleQuotes(stringToEscape) メソッドを使用してください。このメソッドは、文字列内のシングルクォート(')をエスケープ(\')し、インジェクションを防ぎます。

良い例:

String userInput = 'Test\') OR Name != \'';
String sanitizedInput = String.escapeSingleQuotes(userInput);
String query = 'SELECT Id FROM Account WHERE Name = \'' + sanitizedInput + '\'';
List<Account> accs = Database.query(query);

2. 権限と項目レベルセキュリティ (Permissions and FLS)

Dynamic Apex は、コードを実行するユーザーの権限(オブジェクトへの CRUD アクセス権、項目レベルセキュリティ (FLS))を自動的に尊重します。アクセス権のないオブジェクトや項目をクエリしようとすると、例外が発生します。

したがって、動的な操作を行う前には、前述のサンプルコードのように、必ずスキーマ記述を用いてアクセス権を確認することがベストプラクティスです。

  • DescribeSObjectResult.isAccessible(), isCreateable(), isUpdateable(), isDeletable()
  • DescribeFieldResult.isAccessible(), isCreateable(), isUpdateable()

これらのチェックを怠ると、特定のプロファイルを持つユーザーがアプリケーションを使用した際に予期せぬエラーが発生する原因となります。

3. ガバナ制限 (Governor Limits)

Dynamic Apex の操作も、Salesforce のガバナ制限の対象となります。特に以下の点に注意が必要です。

  • SOQL クエリ: Database.query() の呼び出しは、1トランザクションあたり100回までの SOQL クエリ制限にカウントされます。
  • Describe コール: SObjectType.getDescribe()SObjectField.getDescribe() の呼び出しは無制限ですが、Schema.describeSObjects(sObjectTypes) の呼び出しは1トランザクションあたり100回までです。Describe 結果はキャッシュされるため、同じオブジェクトに対する複数回の呼び出しは効率的です。
  • CPU 時間: 文字列操作やループ処理が多用されるため、静的 Apex に比べて CPU 時間を消費しやすい傾向にあります。

4. パフォーマンス (Performance)

静的 Apex はコンパイル時にコードの妥当性検証や最適化が行われるため、一般的に Dynamic Apex よりも高速に動作します。Dynamic Apex は実行時にメタデータの検索や文字列の解析を行うため、わずかなオーバーヘッドが発生します。パフォーマンスが非常に重要な要件である場合は、可能な限り静的 Apex を使用することを検討してください。


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

Dynamic Apex は、静的 Apex では実現不可能な、柔軟で汎用的なアプリケーションを構築するための強力な機能です。スキーマ記述、Dynamic SOQL, Dynamic DML を組み合わせることで、実行時のコンテキストに応じて動作を変化させる、高度なロジックを実装できます。

しかし、その強力さゆえに、セキュリティ、権限、ガバナ制限といった点に細心の注意を払う必要があります。Dynamic Apex を使用する際は、以下のベストプラクティスを常に念頭に置いてください。

  1. 可能な限り静的 Apex を優先する: オブジェクトや項目が事前に分かっている場合は、常に静的 Apex を使用してください。コードの可読性、保守性、安全性が向上します。Dynamic Apex は、「動的でなければならない」場合にのみ使用する切り札です。
  2. SOQL インジェクションを徹底的に防御する: ユーザー入力をクエリに含める際は、String.escapeSingleQuotes() によるサニタイズを絶対に忘れないでください。
  3. 権限チェックを必ず実装する: スキーマ記述メソッド (isAccessible(), isCreateable() など) を使用して、操作の前に必ずユーザー権限を確認し、堅牢なアプリケーションを構築してください。
  4. 厳密な例外処理を行う: 不正なオブジェクト名や項目名が渡された場合や、権限が不足している場合に備え、try-catch ブロックを使用して適切にエラーを処理してください。
  5. ガバナ制限を意識した設計を心掛ける: 特にループ内での Database.query() の呼び出しは避けてください。Describe の結果をキャッシュするなど、効率的な処理を設計することが重要です。

これらの原則を守ることで、Dynamic Apex のメリットを最大限に活用し、安全でスケーラブルな Salesforce アプリケーションを開発することができるでしょう。

コメント