Salesforce Dynamic SOQL 開発者ガイド:Apex で柔軟なクエリを構築する

背景と応用シナリオ

Salesforce 開発者として、私たちは日々 Apex を使用してビジネスロジックを実装しています。その中心的な要素の一つが、Salesforce データベースからデータを取得するための SOQL (Salesforce Object Query Language) です。ほとんどの場合、クエリの構造が事前にわかっているため、Static SOQL(静的 SOQL)を使用します。これは、コード内に直接クエリを記述する、コンパイル時に検証される安全で効率的な方法です。

例:[SELECT Id, Name FROM Account WHERE Industry = 'Technology'];

しかし、アプリケーションの要件が複雑になるにつれて、実行時までクエリの全容が確定しないシナリオに直面することがあります。例えば、ユーザーが画面上で検索対象の項目や絞り込み条件を自由に選択できるようなコンポーネントを開発する場合です。このような動的な要件に応えるために Salesforce が提供しているのが Dynamic SOQL(動的 SOQL)です。

Dynamic SOQL は、Apex コード内で SOQL クエリを文字列として構築し、実行時にその文字列をデータベースに渡してクエリを実行する機能です。これにより、静的 SOQL では実現不可能な、非常に高い柔軟性を持ったデータ取得ロジックを実装できます。

主な応用シナリオ:

  • カスタム検索画面: ユーザーがUI上で検索対象のオブジェクト、表示したい項目、フィルタリング条件(項目、演算子、値)を動的に選択できる検索機能の実装。
  • 汎用的なユーティリティクラス: オブジェクト名や項目名を引数として受け取り、様々なオブジェクトに対して共通の処理を行う汎用的なメソッドの作成。
  • 設定ベースのロジック: カスタムメタデータやカスタム設定に保存された情報(例:処理対象の項目リスト、絞り込み条件など)に基づいてクエリを生成する。
  • 動的なレポート・データエクスポート機能: ユーザーが表示したい列を自由に選択できるレポート機能や、エクスポートするデータ項目を動的に決定する機能の実装。

この記事では、Salesforce 開発者の視点から Dynamic SOQL の原理を解説し、具体的なコード例を交えながら、その強力な機能を安全かつ効果的に活用するための注意点とベストプラクティスを詳説します。


原理説明

Dynamic SOQL の中核は非常にシンプルです。それは「SOQL クエリを String 型の変数として構築し、それを Database.query() メソッドに渡して実行する」というものです。

Static SOQL がコンパイル時に構文チェックや項目存在チェックを受けるのに対し、Dynamic SOQL は実行時に初めて文字列が SOQL として解釈・検証されます。この「実行時解釈」こそが、Dynamic SOQL の柔軟性の源泉であり、同時に開発者が注意を払うべき点でもあります。

基本構文

Dynamic SOQL の実行には、Database.query(queryString) メソッドを使用します。

String soqlString = 'SELECT Id, Name FROM Account';
List<sObject> records = Database.query(soqlString);

このメソッドは、引数として受け取った queryString を SOQL クエリとして実行し、結果を List<sObject> 型で返します。Static SOQL の結果と同様に、特定の sObject(Salesforce オブジェクトの Apex 表現)型(例:List<Account>)にキャストして使用することも可能です。

変数バインディング

クエリに動的な値(特にユーザー入力など外部からの値)を含める場合、文字列を単純に連結する方法は SOQL Injection(SOQL インジェクション)と呼ばれる重大なセキュリティ脆弱性を引き起こす可能性があります。これを防ぐために、Dynamic SOQL では Static SOQL と同様に「変数バインディング」の仕組みが提供されています。

クエリ文字列内で、コロン (:) をプレフィックスとして Apex 変数を配置することで、その変数の値を安全にクエリに埋め込むことができます。

String industry = 'Technology';
String soqlString = 'SELECT Name FROM Account WHERE Industry = :industry';
List<Account> accounts = Database.query(soqlString);

この方法を使用すると、Salesforce プラットフォームが変数の値を自動的にエスケープ処理してくれるため、悪意のある文字列がクエリの構造を破壊するのを防ぐことができます。変数バインディングは、WHERE 句の値として使用する場合に極めて重要です。ただし、オブジェクト名や項目名、ORDER BY のソート順など、クエリの構造自体を動的に変更する部分には変数バインディングは使用できません。


示例コード

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

基本的な Dynamic SOQL の構築

この例では、引数で受け取ったオブジェクト名と項目名を使って、動的にSOQLクエリを構築し実行します。

/**
 * @description 指定されたオブジェクトと項目に基づいて動的なクエリを実行するメソッド
 * @param objectName API参照名 (例: 'Account')
 * @param fieldName API参照名 (例: 'Name')
 * @return クエリ結果の sObject リスト
 */
public List<sObject> queryForRecords(String objectName, String fieldName) {
    // 1. 基本となるSOQLクエリ文字列を構築する
    // String.formatメソッドを使用すると、プレースホルダーを使って可読性の高い文字列を組み立てることができる
    String soqlQuery = String.format('SELECT Id, {0} FROM {1} WHERE {0} != NULL LIMIT 10', new List<String>{
        String.escapeSingleQuotes(fieldName), // SOQLインジェクション対策として項目名をエスケープ
        String.escapeSingleQuotes(objectName) // 同様にオブジェクト名もエスケープ
    });

    System.debug('実行されるクエリ: ' + soqlQuery);

    // 2. Database.query() を使用して動的クエリを実行する
    // try-catchブロックで囲み、実行時エラー(QueryExceptionなど)を捕捉する
    try {
        List<sObject> records = Database.query(soqlQuery);
        return records;
    } catch (QueryException e) {
        System.debug('Dynamic SOQL の実行中にエラーが発生しました: ' + e.getMessage());
        // エラー発生時は空のリストを返すか、例外を再スローするなど適切に処理する
        return null;
    }
}

// 実行例
// List<sObject> results = queryForRecords('Contact', 'Email');
// for (sObject s : results) {
//     System.debug('Contact ID: ' + s.get('Id') + ', Email: ' + s.get('Email'));
// }

コード解説:

  • 12行目: String.format を使用して、クエリ文字列を組み立てています。{0}{1} はプレースホルダーで、後のリストの要素に置き換えられます。これにより、単純な文字列連結 (+) よりもコードが読みやすくなります。
  • 13-14行目: String.escapeSingleQuotes() は、文字列に含まれるシングルクォートをエスケープ ('\') するための重要なメソッドです。オブジェクト名や項目名に予期せぬ文字が含まれていた場合に、クエリが壊れるのを防ぐための基本的なサニタイズ処理です。
  • 21行目: Database.query(soqlQuery) で、組み立てた文字列を実行しています。
  • 22行目: QueryException は、無効なクエリ(存在しない項目を指定するなど)が実行された場合にスローされるため、try-catch ブロックでの捕捉が不可欠です。

変数バインディングを使用した安全なフィルタリング

次の例は、ユーザー入力などの外部変数を WHERE 句に含める際の、変数バインディングの正しい使い方を示しています。これはセキュリティ上、非常に重要なプラクティスです。

/**
 * @description 指定された取引先名で取引先を検索する
 * @param accountName 検索する取引先名
 * @return 一致する取引先のリスト
 */
public List<Account> searchAccountsByName(String accountName) {
    // 1. WHERE句に含める値を Apex 変数として定義する
    // この変数は、クエリ文字列内でバインドされる
    // 必要に応じて、ワイルドカードを追加するなどの処理を行う
    String searchKeyword = '%' + accountName + '%';

    // 2. 変数バインディングを使用して Dynamic SOQL 文字列を構築する
    // プレフィックス ':' を使用して Apex 変数を指定する
    // バインド変数は文字列リテラルで囲む必要はない
    String soqlQuery = 'SELECT Id, Name, Industry FROM Account WHERE Name LIKE :searchKeyword';

    System.debug('実行されるクエリ: ' + soqlQuery);
    System.debug('バインドされる変数 (searchKeyword): ' + searchKeyword);
    
    // 3. Database.query() でクエリを実行する
    // この場合、searchKeyword の値が安全にクエリに埋め込まれる
    try {
        // 結果を具体的な sObject 型 (List) にキャストできる
        List<Account> results = Database.query(soqlQuery);
        return results;
    } catch (QueryException e) {
        System.debug('クエリエラー: ' + e.getMessage());
        return new List<Account>();
    }
}

// 実行例
// String userInput = 'United';
// List<Account> foundAccounts = searchAccountsByName(userInput);
// for (Account acc : foundAccounts) {
//     System.debug('Found Account: ' + acc.Name);
// }

コード解説:

  • 12行目: WHERE 句で使用する値を searchKeyword というローカル変数に格納しています。
  • 16行目: クエリ文字列内で :searchKeyword と記述しています。これが変数バインディングです。Salesforce は実行時にこの部分を searchKeyword 変数の内容に安全に置き換えます。ユーザーが ' OR 1=1 -- のような悪意のある文字列を入力しても、それは単なる文字列として扱われ、クエリの論理構造が変更されることはありません。
  • 23行目: クエリの結果が Account オブジェクトであることが明確なため、Database.query() の戻り値を List<Account> にキャストしています。

注意事項

Dynamic SOQL は強力なツールですが、その力を正しく制御しなければなりません。開発時には以下の点に常に注意を払う必要があります。

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

最も警戒すべきリスクです。ユーザー入力などの信頼できないデータをエスケープせずにクエリ文字列に直接連結すると、攻撃者によってクエリが不正に操作される可能性があります。

  • 対策: WHERE 句の値は常に変数バインディングを使用してください。オブジェクト名や項目名など、バインディングが使えない部分を動的にする場合は、String.escapeSingleQuotes() でサニタイズするか、事前に許可された値のリスト(ホワイトリスト)と照合するなどの検証ロジックを必ず実装してください。

2. 権限と Field-Level Security (FLS)

Dynamic SOQL は、実行ユーザーの権限(オブジェクトや項目へのアクセス権)を尊重します。ユーザーがアクセス権を持たないオブジェクトや項目をクエリに含めると、System.QueryException: No such column '...' on entity '...' のようなエラーがスローされます。

  • 対策:
    • WITH SECURITY_ENFORCED クエリにこの句を追加すると、実行ユーザーの項目レベル・オブジェクトレベルのセキュリティが自動的に適用されます。アクセスできない項目が含まれている場合、クエリは例外をスローします。これが最も簡単で推奨される方法です。
    • スキーマ情報の確認: クエリを構築する前に、Schema クラスのメソッド(例:SObjectType.getDescribe().isAccessible(), SObjectField.getDescribe().isAccessible())を使用して、ユーザーが必要な権限を持っているかプログラムで確認します。

3. Governor Limits(ガバナ制限)

Dynamic SOQL も Static SOQL と同じガバナ制限に従います。1つの Apex トランザクション内で実行できる SOQL クエリの合計回数(通常100回)や、取得できる合計レコード数(50,000件)などの制限に抵触しないよう、設計段階で考慮する必要があります。特にループ内で Dynamic SOQL を実行するコードは、意図せず制限に達する可能性が高いため、絶対に避けるべきです。

4. コンパイル時 vs 実行時エラー

Static SOQL はコンパイル時に構文や項目の存在がチェックされるため、typoなどの単純なミスは開発段階で発見できます。一方、Dynamic SOQL のクエリ文字列はただの文字列であるため、その内容が正しいかどうかは実行してみるまでわかりません。

  • 対策: すべての Database.query() コールは、必ず try-catch ブロックで囲み、QueryException を捕捉して適切に処理する(ログ出力、ユーザーへのエラーメッセージ表示など)ようにしてください。

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

Dynamic SOQL は、Salesforce プラットフォーム上で柔軟かつ動的なアプリケーションを構築するための不可欠な機能です。しかし、その柔軟性と引き換えに、開発者はセキュリティ、権限、ガバナ制限に対してより一層の注意を払う責任を負います。

Dynamic SOQL を使いこなすためのベストプラクティスは以下の通りです。

  1. 可能な限り Static SOQL を優先する: クエリの構造が事前に固定されている場合は、常に Static SOQL を使用してください。安全性、可読性、パフォーマンスの面で優れています。
  2. SOQL インジェクションを徹底的に防御する: 外部からの入力値を WHERE 句に含める際は、変数バインディング(:)が絶対のルールです。文字列連結は原則として行わないでください。
  3. セキュリティをコードに組み込む: WITH SECURITY_ENFORCED 句を積極的に利用するか、スキーマ記述メソッドで権限チェックを事前に行い、ユーザーのアクセス権を常に尊重する設計を心がけてください。
  4. 堅牢なエラーハンドリングを実装する: Database.query() は常に try-catch ブロックでラップし、実行時エラーからアプリケーションを保護してください。
  5. コードの可독性を維持する: 複雑なクエリ文字列を組み立てる際は、処理を小さなメソッドに分割したり、String.format を活用したりして、後からコードを読む人がロジックを容易に理解できるように工夫してください。
  6. ガバナ制限を意識する: Dynamic SOQL が実行されるコンテキストを常に意識し、大量のクエリが発行されないようにロジックを設計してください。

これらの原則を守ることで、開発者は Dynamic SOQL のパワーを最大限に引き出し、安全でスケーラブル、かつユーザーのニーズに応える高度な Salesforce アプリケーションを構築することができるでしょう。

コメント