【あべ 開発Tips #3】Apexの静的SOQLと動的SOQLについて。
Photo by Caspar Camille Rubin on Unsplash
【ちょっと息抜きに:読了まで5分】
こんにちは!
代表の阿部です!
本日は、Apexの静的SOQLと動的SOQLについてです。
タイトルで何やら難しいそうと思った初心者の方!
全然難しい話じゃないので、ちょっと読んでみると、開発の際の選択肢が広がると思います!
目次
【ちょっと息抜きに:読了まで5分】
まずは、静的SOQL、動的SOQLについて
静的SOQLとは?
動的SOQLとは?
静的SOQLはどのようなときに使うの?
動的SOQLはどのようなときに使うの?
動的SOQLの落とし穴
SOQLインジェクションについて
どのようなリスクがある?
どうやって防ぐ?
最後に
まずは、静的SOQL、動的SOQLについて
静的SOQLとは?
静的SOQLは、コード内で、確定されたSOQL文を記述して、レコード取得します。
例えば以下です。
List<String> Names = new List<String>{'aaa','bbb','ccc'};
List<Account> Accounts = [SELECT Id , Name FROM Account WHERE Name = :Names];
皆様の知っている普通のSOQL文ですね。
Nameが、'aaa','bbb','ccc'の取引先が取得されます。
バインド変数(条件句のNamesの部分の事)の中身以外は、確定された文を記述しています。
動的SOQLとは?
では、確定されていない記述のSOQL文とは?それが動的SOQLです。
こちらは、処理の中で、動的にSOQL文自体を構成出来ます。
例えば以下です。
//ユーザー入力を以下の変数で受けると仮定
String nameStr;
Boolean parentFlg;
Account parentAccount;
//ユーザー入力により取引先を作る際のサンプル
Account acc= new Account();
acc.Name = nameStr;
acc.IsParent = parentFlg;
acc.ParentId = parentAccount.Id;
insert acc;
//取得しなおす。
//条件句手前までのSOQL文
String queryString = 'SELECT Id , Name FROM Account WHERE ';
//親取引先True/FalseでSOQL文を分ける
if (IsParent )[
queryString += 'Name ='+ acc.Name;
}else{
queryString += 'Parent.Name ='+ acc.Parent.Name;
}
List<Account> accountList = Database.query(queryString);
ちょっとわかりづらいっすね・・・。笑
すいません。取引先をユーザー入力で作成し、取得しなおしてます。あまり現実的な処理じゃないけど。笑
上記の例は、取引先を検索する際に、親取引先項目のTrue/Falseによって、SOQLの条件句の記述を分けています。
取得されるのは、以下の条件の取引先です。
- IsParent =Trueであれば、入力された取引先名で検索
- IsParent = Falseであれば、入力された取引先の親の取引先名で検索
ということを表現しています。
後述しますが、このSOQL文には重大な欠陥があります。(ミステリー風)
動的SOQLは、最後にDatabeseクラスを呼び出して、DML操作します。Database.queryの部分ですね。
静的SOQLはどのようなときに使うの?
基本的には、Apexで作るSOQL文はこちらを採用する場合が多いと思います。処理の中で、必要なレコードを取得する時は、大体これですね。基本的にこっちを使った方がいいです。理由は後述します。
動的SOQLはどのようなときに使うの?
処理の分岐によって、SOQL文を変更したい時です。例えば
- レコードの値によって、他の値も取得条件に加えたい時(WHERE句に、条件文を追加する分岐を作成します)
- SELECTする項目を、動的に変更したい時(例えば、"全項目"のような動的な項目など)
- ユーザー入力によって、取得条件となる項目を変更したい時(例えば、特定のユーザーの入力によってfield1__c,field2__c,field3__c・・・を条件句に加えたいなど)
特にユーザー入力によって動的に条件を変えるSOQL文は、ExperienceCloudで、ポータルサイトの構築などで、よく使います!
動的SOQLの落とし穴
動的に、SOQL文を組み立てられる便利な記述方法ですが、重要な落とし穴があります。それは
SOQL文を入力するユーザーが意図的に、クエリ文を変更する事によって、本来事業側、開発側が意図していないデータベースにアクセスできてしまう事です。
これをSOQLインジェクションと言います。
SOQLインジェクションについて
例えば、ユーザーが取引先名を入力して取引先を検索する場合、以下のような記述になります。入力された取引先名であいまい検索をしています。
//ユーザー入力の取引先名を受け取る変数
String accountName = ''
//受け取ったユーザー名をSOQL文の条件句にする
queryString = 'SELECT Id FROM Account WHERE (IsDeleted = false and Name like \'%' + accountName + '%\') '
この時、ユーザーが「取引先A」という取引先名を入力すると、削除されていない'取引先A'が出力されます。
さて、それではユーザーが以下のように入力すると、どうでしょう。
'test%') OR (Name LIKE'
出来上がるSOQL文は、以下のようになります。
queryString = 'SELECT Id FROM Account WHERE (IsDeleted = false and Name like '%test%') OR (Name LIKE '%')) '
Nameがワイルドカード(なんでもヒットする)となり、すべての取引先が取得されます。
どのようなリスクがある?
SOQLインジェクションは、ITの知見のある方には一般的ですが、初心者の方は、ここを見落としがちです。
アクセス権がしっかりと制御されていれば、いったん大問題は起こりませんが、抜け穴を狙った攻撃をされたり、アクセス件の変更タイミングなどで正しく制御できてない場合、不正アクセスや情報漏洩を引き起こします。
どうやって防ぐ?
StringクラスのescapeSingleQuotesメソッドを使用します。
このメソッドは、バインドされた変数の値をシングルクオートで囲み、入力された値で、SOQL文として組み立てられないようにします。書き方は、リファレンスをご参照ください。
つまり、先ほどの例でいうと、こんな感じです。
queryString = 'SELECT Id FROM Account WHERE (IsDeleted = false and Name like '% 'test%') OR (Name LIKE ''%')) '
入力された文字列のままの「test%') OR (Name LIKE」という名前の取引先を探そうとするので、ORで追加使用しようとしたワイルドカードは無効になります。
ちなみに、静的SOQLでは、このエスケープ処理は仕様に組み込まれているので、SOQLインジェクションは起こりません。
これで、SOQLインジェクションは回避できます。
最後に
いかがでしたか?動的SOQLを使わないと実装出来ない要件も沢山あります!
正しく理解して、使いこなせるように出来るといいですね!
では、また!!