Salesforceの柔軟性を解き放つ:開発者のためのダイナミックApex完全ガイド

執筆者:Salesforce 開発者


背景と応用シーン

私たち Salesforce 開発者は、日々の業務で Apex を使用してビジネスロジックを実装しています。多くの場合、コンパイル時にオブジェクト名や項目名が確定している静的 (Static) Apex を使用します。例えば、[SELECT Id, Name FROM Account WHERE Name = 'Test'] のような SOQL や、Account acc = new Account(Name='New Account'); といった DML 操作です。これらはコンパイル時に Salesforce が参照先のオブジェクトや項目が存在するかを検証してくれるため、コードの安全性が高く、開発ツールによる補完も効くため非常に効率的です。

しかし、アプリケーションの要件が複雑化するにつれて、コンパイル時には操作対象が不明で、実行時に初めて決定されるケースに直面します。例えば、以下のようなシナリオです。

  • 汎用的なコンポーネントの開発:特定のオブジェクトに依存せず、様々な標準オブジェクトやカスタムオブジェクトで動作する Lightning Web Component (LWC) のバックエンドコントローラを開発したい。
  • 動的な検索画面の実装:ユーザーが画面上で検索対象のオブジェクト、項目、検索条件を自由に選択できるカスタムレポートビルダーのような機能を作りたい。
  • 外部システムとの連携:連携先のシステムから送られてくるデータ形式(どのオブジェクトのどの項目を更新するか)が、設定によって動的に変わる場合。
  • 管理パッケージの開発:インストール先の組織(顧客環境)に存在するカスタムオブジェクトやカスタム項目に適応して動作する必要があるパッケージを開発したい。

このような静的なアプローチでは対応が困難な課題を解決するために存在するのが、Dynamic Apex (動的Apex) です。Dynamic Apex は、オブジェクト名、項目名、SOQL クエリなどを文字列として扱い、実行時にそれらを解決してコードを実行する能力を開発者に提供します。これにより、極めて柔軟で再利用性の高い、メタデータ駆動型のアプリケーションを構築することが可能になります。

この記事では、Salesforce 開発者の視点から、Dynamic Apex の核心である「動的SOQL」、「動的DML」、そしてそれらを支える「スキーマ記述」の仕組みを、公式ドキュメントのコード例を交えながら徹底的に解説します。

原理説明

Dynamic Apex の魔法は、主に3つの要素によって実現されます。それぞれが連携し合うことで、実行時の動的な処理を可能にしています。

1. 動的 SOQL (Dynamic SOQL)

動的 SOQL は、SOQL クエリを文字列として構築し、Database.query(queryString) メソッドに渡して実行する手法です。静的な SOQL [SELECT ... FROM ...] がコンパイル時に検証されるのに対し、動的 SOQL は実行時に文字列が解析・実行されます。これにより、実行時の条件に応じてクエリの SELECT 句、FROM 句、WHERE 句などを自由自在に組み立てることができます。

例えば、ユーザーが選択した項目リストを変数 fieldList に、オブジェクト名を変数 objectName に格納し、'SELECT ' + fieldList + ' FROM ' + objectName のような文字列を動的に生成してクエリを実行できます。

2. 動的 DML (Dynamic DML) と sObject の動的アクセス

動的 DML は、特定の sObject 型(例: Account, Contact)にキャストすることなく、汎用的な sObject 型の変数を使って DML 操作(insert, update, delete など)を行うことを指します。これを実現する鍵となるのが、以下のメソッドです。

  • Type.forName(namespace, className).newInstance(): クラス名(オブジェクトのAPI参照名)の文字列から、そのオブジェクトのインスタンスを生成します。
  • sObject.put(fieldName, value): 項目名の文字列を指定して、sObject インスタンスに値を設定します。
  • sObject.get(fieldName): 項目名の文字列を指定して、sObject インスタンスから値を取得します。

これらのメソッドを組み合わせることで、「文字列で指定されたオブジェクトのインスタンスを作成し、文字列で指定された項目に値をセットし、データベースに挿入する」といった一連の処理を完全に動的に実装できます。

3. スキーマ記述 (Schema Describe)

動的 SOQL や動的 DML を安全かつ堅牢に実装するためには、実行時に組織のメタデータ(どのオブジェクトが存在し、各オブジェクトがどんな項目を持っているか、その項目のデータ型は何か、ユーザーはアクセス権限を持っているかなど)を知る必要があります。これを可能にするのが Schema (スキーマ) クラスです。

Schema クラスのメソッド群(通称 Describe Result)を使うことで、以下のような情報をプログラムで取得できます。

  • 組織に存在する全てのオブジェクトのリスト (Schema.getGlobalDescribe())
  • 特定のオブジェクトの詳細情報(表示ラベル、API参照名など) (SObjectType.getDescribe())
  • 特定のオブジェクトが持つ全ての項目のリストと、各項目の詳細情報(データ型、長さ、選択リスト値、作成可能か、更新可能かなど) (SObjectField.getDescribe())

例えば、動的 SOQL を構築する前に、Schema Describe を使ってユーザーが指定した項目名が実際にオブジェクトに存在するかを確認したり、ユーザーがその項目に対する参照権限を持っているかをチェックしたりすることで、実行時エラーを未然に防ぐことができます。


示例代码

ここでは、Salesforce の公式ドキュメントに掲載されているコード例を基に、Dynamic Apex の具体的な実装方法を解説します。

1. 動的 SOQL の基本的な使用例

この例では、オブジェクト名と項目名を引数で受け取り、動的に SOQL クエリを構築して実行します。シンプルな例ですが、Dynamic Apex の基本が詰まっています。

// objectName に 'Account', fieldName に 'Name' を渡して呼び出すことを想定
public List<sObject> queryAccounts(String objectName, String fieldName) {
    // SOQLクエリを文字列として動的に構築する
    // fieldName と objectName はメソッドの引数から渡されるため、実行時まで内容が確定しない
    String queryString = 'SELECT Id, ' + fieldName + ' FROM ' + objectName + ' LIMIT 10';

    // Database.query() メソッドを使用して、文字列で定義されたクエリを実行する
    // 戻り値は、具体的な sObject 型 (Accountなど) ではなく、汎用的な List<sObject> 型となる
    return Database.query(queryString);
}

// 呼び出し側のコード
List<sObject> results = queryAccounts('Account', 'Name');
for (sObject s : results) {
    // sObject.get() メソッドを使用して、動的に項目値を取得する
    System.debug('Account Name: ' + s.get('Name'));
}

注釈: Database.query(queryString) は、静的 SOQL とは異なり、List<sObject> を返します。そのため、結果のレコードから項目値を取得する際には、acc.Name のような静的なアクセスではなく、s.get('Name') のように項目名を文字列で指定する動的なアクセス方法を用います。

2. 動的 DML と sObject の動的生成

この例では、オブジェクト名を文字列で受け取り、そのオブジェクトの新しいレコードを作成してデータベースに挿入します。汎用的なレコード作成ユーティリティなどで活用できます。

public sObject createRecord(String objectApiName, Map<String, Object> fieldValues) {
    // 1. グローバルDescribeを使用して、文字列から sObjectType を取得
    // これにより、API参照名が有効なオブジェクトのものであるかを確認できる
    Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objectApiName);
    if (targetType == null) {
        // オブジェクトが存在しない場合は例外をスローするなど、エラーハンドリングを行う
        throw new MyException('Invalid sObject Type: ' + objectApiName);
    }

    // 2. sObjectType の newSObject() メソッドを呼び出して、新しい sObject インスタンスを生成
    // Type.forName(objectApiName).newInstance() と同等の機能
    sObject newRecord = targetType.newSObject();

    // 3. 渡された Map をループし、sObject.put() を使って動的に項目値を設定
    for (String fieldName : fieldValues.keySet()) {
        newRecord.put(fieldName, fieldValues.get(fieldName));
    }

    // 4. DML 操作を実行
    // この insert 文は、newRecord が実行時にどのオブジェクト型であっても機能する
    try {
        insert newRecord;
    } catch (DmlException e) {
        System.debug('An error occurred during DML operation: ' + e.getMessage());
        // 適切なエラー処理を実装
        return null;
    }
    
    return newRecord;
}

// 呼び出し側のコード
Map<String, Object> leadFields = new Map<String, Object>{
    'FirstName' => 'Dynamic',
    'LastName' => 'Lead',
    'Company' => 'Apex Inc.'
};
// 'Lead' オブジェクトのレコードを動的に作成
sObject myNewLead = createRecord('Lead', leadFields);
System.debug('Created record ID: ' + myNewLead.Id);

注釈: このコードは、どのオブジェクトに対してもレコードを作成できる再利用性の高いメソッドです。Schema.getGlobalDescribe().get(objectApiName) でオブジェクトの存在を確認し、newSObject() でインスタンス化、put() で項目を設定するという流れが、動的 DML の典型的なパターンです。

3. スキーマ記述 (Schema Describe) を活用した堅牢な実装

動的な処理を実装する際、存在しない項目にアクセスしようとしたり、権限のない項目を更新しようとしたりすると、実行時エラーが発生します。この例では、Schema Describe を使ってオブジェクトの全項目を安全にループ処理する方法を示します。

public void processAllFields(String objectApiName) {
    // グローバルDescribeから対象の sObjectType を取得
    Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();
    Schema.SObjectType sObjectType = gd.get(objectApiName);

    if (sObjectType == null) {
        System.debug(objectApiName + ' sObject not found.');
        return;
    }

    // sObjectType から DescribeSObjectResult を取得
    Schema.DescribeSObjectResult R = sObjectType.getDescribe();

    // DescribeSObjectResult から全ての項目の Map を取得
    Map<String, Schema.SObjectField> MapofField = R.fields.getMap();

    // 全ての項目をループ処理
    for(String fieldName : MapofField.keySet()){
        // 各項目の DescribeFieldResult を取得
        Schema.DescribeFieldResult fieldResult = MapofField.get(fieldName).getDescribe();
        
        // 項目がカスタム項目かどうかをチェック
        boolean isCustom = fieldResult.isCustom();
        
        // 項目のデータ型を取得
        Schema.DisplayType fieldType = fieldResult.getType();

        // 実行ユーザーがこの項目への参照アクセス権を持っているかを確認
        boolean isAccessible = fieldResult.isAccessible();

        System.debug('Field: ' + fieldName + 
                     ', Type: ' + fieldType + 
                     ', IsCustom: ' + isCustom +
                     ', IsAccessible for current user: ' + isAccessible);

        // 例えば、アクセス可能な文字列型の項目のみをリストアップする、といったロジックをここに追加できる
    }
}

// 呼び出し例: 'Contact' オブジェクトの全ての項目情報をコンソールに出力
processAllFields('Contact');

注釈: このように、処理の前に isAccessible() (参照可能か), isCreateable() (作成可能か), isUpdateable() (更新可能か) といったメソッドで権限チェックを行うことは、堅牢な Dynamic Apex アプリケーションを構築する上で不可欠です。

注意事項

Dynamic Apex は強力なツールですが、その力を正しく使わなければ、セキュリティリスクやパフォーマンスの問題、予期せぬエラーを引き起こす可能性があります。開発者は以下の点に常に注意を払う必要があります。

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

動的 SOQL で最も注意すべきセキュリティ脆弱性です。これは、ユーザーが入力した文字列をエスケープ処理せずに直接 SOQL クエリに結合することで、攻撃者が意図しないクエリを実行できてしまう問題です。例えば、WHERE 句の条件値をユーザー入力から受け取る場合に発生しやすいです。

対策: ユーザーからの入力を SOQL クエリに含める場合は、必ず String.escapeSingleQuotes(stringToEscape) メソッドを使用して、文字列内のシングルクォートをエスケープしてください。これにより、悪意のある入力がクエリの構造を破壊するのを防ぎます。

2. 権限と項目レベルセキュリティ (Field-Level Security - FLS)

デフォルトでは、Apex はシステムモードで実行されるため、実行ユーザーのオブジェクト権限や Field-Level Security (項目レベルセキュリティ - FLS) を無視してデータにアクセスできてしまいます。これは意図しない情報漏洩に繋がる可能性があります。Dynamic Apex では、コードがどの項目にアクセスするかが実行時まで不明なため、特に注意が必要です。

対策:

  • WITH SECURITY_ENFORCED 句 (SOQL): SOQL クエリにこの句を追加すると、実行ユーザーの FLS とオブジェクト権限が自動的に適用されます。権限がない項目にアクセスしようとすると、クエリは QueryException をスローします。これは最も推奨される現代的なアプローチです。
  • Security.stripInaccessible() メソッド (DML/SOQL): DML 操作の前や SOQL の結果に対してこのメソッドを適用すると、ユーザーがアクセスできない項目を sObject リストから安全に削除してくれます。
  • Schema Describe による手動チェック: 前述の例のように、isAccessible() などのメソッドを使って、処理の前に手動で権限を確認します。

3. ガバナ制限 (Governor Limits)

Dynamic Apex も、静的 Apex と同様に全てのガバナ制限(SOQLクエリの発行回数、DMLステートメントの回数、CPU時間など)に従います。動的にクエリや DML を生成するコードは、ループ内で意図せず大量のクエリを発行してしまうなど、静的なコードよりも非効率になりがちです。パフォーマンスへの影響を常に意識し、コードを設計する必要があります。

4. エラー処理とデバッグ

動的コードはコンパイル時チェックの恩恵を受けられないため、「オブジェクト名の間違い」「項目名のタイポ」「データ型の不一致」といったエラーが実行時に発生しやすくなります。try-catch ブロックを適切に配置し、QueryException, DmlException, TypeException などを捕捉して、ユーザーに分かりやすいフィードバックを返す、あるいはログに詳細を記録するなどの堅牢なエラー処理が不可欠です。

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

Dynamic Apex は、Salesforce プラットフォームの柔軟性を最大限に引き出すための強力な機能です。汎用的なフレームワークの構築や、ユーザー主導の動的なアプリケーション開発において、その価値は計り知れません。

しかし、その力には相応の責任が伴います。私たち開発者は、以下のベストプラクティスを遵守することで、安全で、保守性が高く、効率的な Dynamic Apex コードを記述することができます。

  1. 静的 Apex を優先する: 操作対象のオブジェクトや項目がコンパイル時に分かっている場合は、常に静的 Apex を使用してください。コードの可読性、安全性、および開発効率が向上します。
  2. 使用目的を明確にする: Dynamic Apex は「最後の手段」あるいは「明確な目的がある場合」にのみ使用します。その必要性を慎重に評価してください。
  3. SOQL インジェクションを絶対に防ぐ: ユーザー入力を含む動的 SOQL では、String.escapeSingleQuotes() の使用を徹底してください。
  4. セキュリティを強制する: WITH SECURITY_ENFORCEDSecurity.stripInaccessible() を積極的に活用し、ユーザーの権限を尊重したコードを記述してください。
  5. スキーマ情報を活用する: コードの実行前に Schema Describe を用いてメタデータの存在確認や権限チェックを行うことで、実行時エラーを未然に防ぎ、コードの信頼性を高めます。
  6. 堅牢なエラーハンドリングを実装する: 予期せぬ実行時エラーに備え、網羅的な try-catch ブロックを実装してください。
  7. コードに詳細なコメントを記述する: 動的コードは静的コードよりもロジックが複雑で理解しにくいため、なぜ動的なアプローチが必要なのか、どのような処理を行っているのかをコメントとして明確に残すことが、将来の保守性のために極めて重要です。

Dynamic Apex をマスターすることは、Salesforce 開発者として一段上のレベルに到達するための重要なステップです。その原理を正しく理解し、ベストプラクティスを遵守することで、より高度で価値の高いソリューションを顧客に提供できるようになるでしょう。

コメント