4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Salesforce の SOQL を動的に生成する Apex クラス

Last updated at Posted at 2022-06-04

はじめに

この記事は Salesforce の Apex クラスでクエリビルダおよび DML 操作をより簡潔に記述することを目的とした内容を題材にしています。どのような経緯で開発するに至ったか、そしてその開発したクラスの使い方についてお話していきます。

目次

背景

今年に入って Salesforce と某アプリケーションを API で連携する案件を担当することになって、初めて Apex クラスなるものに触れました。
そもそも Salesforce のアプリケーション自体に初めて触れるので、片っ端からドキュメントを読み込んで基本的な仕組みであったり、Rest API のルールであったり、そして本題の Apex 開発のドキュメントを読み込んで、さてテスト開発してみようかということになりました。

【読み込んだドキュメント類】

カルチャーショック

Apex クラスの開発にあたって、sObject という Salesforce のデータテーブルからレコードを取得したり、逆に DML 操作をしたりするわけですが、それには SOQL というちょっとユニークな SQL に似た言語を扱う必要があります。
ここで衝撃を受けたのが、その SOQL 言語をレコード取得のために逐一記述しないといけないということです!

Salesforce 通の皆さま:「そういうものでしょ。」

と思いますよね。。
それでも、、実際に書いてみてなんかこう、いちいち SOQL を描くのは単調で冗長的だなぁという感じを受けたんです。

例えば、

Account[] accList = [SELECT Name, BillingCity, BillingCountry, NumberOfEmployees FROM Account];

とかいう書き方をするのですが、これを従業員数(NumberOfEmployees)でフィルタリングする条件など増えていくと、

Account[] accList = [SELECT Name, BillingCity, BillingCountry, NumberOfEmployees FROM Account WHERE NumberOfEmployees = 100 OR NumberOfEmployees = 200 OR NumberOfEmployees = 300];

// または

Account[] accList = [SELECT Name, BillingCity, BillingCountry, NumberOfEmployees FROM Account WHERE NumberOfEmployees IN (100, 200, 300)];

まだこれは序の口で、じゃあ「似たような名前で従業員数が 100 ~ 300 人の企業はあるか」となると、

AggregateResult[] accGroup = [
  SELECT
    COUNT(Id),
    Name,
    NumberOfEmployees
  FROM
    Account
  GROUP BY Name, NumberOfEmployees
  HAVING Name LIKE '%とある企業' AND NumberOfEmployees > 100 AND NumberOfEmployees < 300
];

こんな感じで行数を食います。もちろん、改行なしにすれば、

AggregateResult[] accGroup = [SELECT COUNT(Id), Name, NumberOfEmployees FROM Account GROUP BY Name, NumberOfEmployees HAVING Name LIKE '%とある企業' AND NumberOfEmployees > 100 AND NumberOfEmployees < 300];

とても横に長い SOQL クエリの出来上がりです。
(これも序の口かもしれません..)

少数ですが開発チームで議論していて、この記法はだるいしなんだか慣れないよね〜という話をしていてました。
そんな時に、1人が今回の開発のきっかけとなる一言をくれたんです。

チームメイト:「なんかこう、フレームワークみたいに動的な書き方したいですよね。」

確かに、そんなことができれば今後の開発も機能拡張することがあっても楽になるな、と。
色々と調べて行くうちに Database.query() というクラスメソッドの存在も知り、これはもしかしたらクエリ生成をするクラスくらいなら小さく作れるかもしれないと思えるようになりました。

いざ、出陣

幸いにも案件は始まったばかりで、Salesforce 機能の調査として設けられていた期間の真っ只中でした。
時間もまだあるので、今後のことを考えて基盤機能を作ってしまえ! ということで着手に乗り出しました。

その名も 「SOQLBuilder」 クラス

ネーミングはそのままです。
とある言語のフレームワークを参考にしながら、クエリビルダ機能や DML 操作をより簡潔かつ拡張性のある書き方ができるように開発を進めました。

詳細と Apex クラスファイルはこちらの GitHub に掲載しています。
https://github.com/BruceWeyne/Apex-SOQLBuilder

使い方

メソッドなどの細かい説明は GitHub にドキュメントとしてまとめているので、そちらを閲覧してみてください。ここでは、実際にどう使うのか、僕自身がどう使いたくて開発をしたのかを説明していきます。

EX.0 まずはインスタンスを生成する

何よりも先にすべきなのはインスタンスの生成ですよね。
これがなければ始まりません。

SOQLBuilder bld = new SOQLBuilder();

変数名は何でも良いですが、私の場合は「SOQLBuilder」の中の単語「build」を略して「bld」にしています。

EX.1 シンプルに名前とメールアドレスを取って来る

ユーザオブジェクトからレコードを取得する例です。

SOQLBuilder クラスの場合

bld.soqlSelect('Name, Email');
User[] userList = bld.soqlGet('User');

この2行で書ける処理は以下と同等です。

SOQL 直書きの場合

User[] user = [SELECT Name, Email FROM User];

..これではまだまだ有用性は感じられないですよね。。では次。

EX.2 とりあえず全フィールドを取ってきて比較したい

これは飽くまで開発やテストの際に活用する例です。
本番での利用は ガバナ制限 的に当然避けます。

SOQLBuilder クラスの場合

bld.soqlSelect('*'); // もしくはこの soqlSelect メソッド自体をコールしなくても同等です
User[] userList = bld.soqlGet('User');

この 2 行(もしくは 1 行)で書ける処理は以下と同等です。

SOQL 直書きの場合

User[] user = [SELECT Id, Name, Alias, LastName, FirstName, Email, Username, CommunityNickname, Title, Department, UserType, Phone, ProfileId, ...(省略)..., IsActive FROM User];

このあたりでちょっと「使えるかも?」となっていただけたかもしれません。

EX.3 条件分岐でフィルター条件を追加したい

この辺が SOQL の直書きだと表現しにくいなと思うところでした。

SOQLBuilder クラスの場合

SOQLBuilder bld = new SOQLBuilder();
RestRequest req = RestContext.request;

// URL クエリパラメータを取得
Integer limitVal  = (Integer)req.params.get('limit');
Integer offsetVal = (Integer)req.params.get('offset');

// SOQL クエリの構築
bld.soqlSelect('Id, Name, Phone');

// URL クエリパラメータが設定されている場合のみ
if (limitVal != null) { // When Not null
    bld.soqlLimit(limitVal);
}
if (offsetVal != null) { // When Not null
    bld.soqlOffset(offsetVal);
}

// クエリを実行
Account[] accList = bld.soqlGet('Account');

もし上記を SOQL 直書きで表現しようとすると、
(私の拙い知識で恐縮ですが)

SOQL 直書きの場合

RestRequest req = RestContext.request;

// URL クエリパラメータを取得
Integer limitVal  = (Integer)req.params.get('limit');
Integer offsetVal = (Integer)req.params.get('offset');

// 全ての条件パターンで SOQL クエリを記述しなければならない
Account[] accList = new List<Account>();
if (limitVal != null && offsetVal != null) {
    accList = [SELECT Id, Name, Phone FROM Account LIMIT :limitVal OFFSET :offsetVal];
} else if (limitVal != null && offsetVal == null) {
    accList = [SELECT Id, Name, Phone FROM Account LIMIT :limitVal];
} else if (limitVal == null && offsetVal != null) {
    accList = [SELECT Id, Name, Phone FROM Account OFFSET :offsetVal];
} else {
    accList = [SELECT Id, Name, Phone FROM Account];
}

このように、条件分岐の度にクエリを一から書かないといけません。

EX.4 ループでフィルタ条件を追加したい

こちらの使い方は少し極端ですが、こんなケースもあるかもしれません。
(飽くまで例として)

SOQLBuilder クラスの場合

bld.soqlSelect('Name, NumberOfEmployees');

Set<Integer> empNumSet = new Set<Integer>();
for (Integer i = 0; i < 1000; i++) {
  if (i > 300 && i < 700) { // 従業員数が 300 ~ 700 人の企業を条件
    empNumSet.add(i);
  }
}
bld.soqlWhereIn('NumberOfEmployees', empNumSet); // 条件を設定

Account[] accList = bld.soqlGet('Account');

上記はちょっと特殊な例ですが、これを SOQL 直書きで書こうとすると、、書きたくないです。
...強いて書くなら、

SOQL 直書きの場合(間違いの例)

Set<Integer> empNumSet = new Set<Integer>();
for (Integer i = 0; i < 1000; i++) {
  if (i > 300 && i < 700) { // 従業員数が 300 ~ 700 人の企業を条件
    empNumSet.add(i);
  }
}

Account[] accList = [
  SELECT
    Name,
    NumberOfEmployees
  FROM
    Account
  WHERE NumberOfEmployees IN (:empNumSet) // 多分間違っている
];

もしくは、

SOQL 直書きの場合(改めて)

Account[] accList = new Account[];
Account[] targetAccs = new Account[];
for (Integer i = 0; i < 1000; i++) {
  targetAccs = new Account[];
  if (i > 300 && i < 700) { // 従業員数が 300 ~ 700 人の企業を条件
    targetAccs = [SELECT  Name, NumberOfEmployees FROM Account WHERE NumberOfEmployees = :i];
    accList.add(targetAccs);
  }
}

この辺りから SOQLBuilder クラスを活用するとクエリの構築をイメージしながら直感的に書けるようになってくると思います。

EX.5 見やすく整理して書きたい

私の場合はこんなふうに見やすさを意識して書きたい時に使っています。
soqlSelect() メソッドは何度コールしても最終的に一つの SELECT 句に結合して実行してくれます。
なので、標準の項目と参照関係にある項目を分けて見やすくする目的で使用しています。

SOQLBuilder クラスの場合

bld.soqlSelect('Id, CaseNumber, Comments, ContactEmail, ContactPhone');
bld.soqlSelect('Account.Id, Account.Name, Account.PersonEmail'); // 参照取得
bld.soqlOrderBy('Account.Name', 'ASC');
Case[] csList = bld.soqlGet('Case');

これと同じ処理を SOQL で書くと、

SOQL 直書きの場合

Case[] csList = [SELECT Id, CaseNumber, Comments, ContactEmail, ContactPhone, Account.Id, Account.Name, Account.PersonEmail FROM Case ORDER BY Account.Name ASC];

うーん、何とも横長で見ずらい記法に感じます。飽くまで個人の感想ですが。。

EX.6 こんな柔軟な書き方もできます

ちょっと応用的なメソッドも用意しているので、気に入っていただけたらぜひ使ってみてください。

SOQLBuilder クラスの場合

bld.soqlStartCache(); // クエリキャッシュを有効化

bld.soqlSelect('Id, Name'); // ①
bld.soqlLike('Name', '%Misaka%'); // ②
User[] userList = bld.soqlGet('User');

// 上記は以下と同等
// User[] userList = [SELECT Id, Name FROM User WHERE Name LIKE '%Misaka%'];

bld.soqlSelect('BillingCity');
bld.soqlOrNotLike('Name', '%Shirai%');
Account[] accList = bld.soqlGet('Account');

// 上記は以下と同等
// Account[] accList = [SELECT Id, Name, BillingCity FROM Account WHERE Name LIKE '%Misaka%' OR (NOT Name LIKE '%Shirai%')];

bld.clear(); // クエリの初期化とクエリキャッシュを無効化

bld.soqlSelect('Email');
Contact[] cntList = bld.soqlGet('Contact');

// 上記は以下と同等
// Contact[] cntList = [SELECT Email FROM Contact];

これを説明すると、soqlStartCache() メソッドをコールすることにより、その後の ①, ② でコールした SOQL 構文を保持し続け、Account オブジェクトの取得の際に改めて同じ条件のメソッドをコールしなくても良くなる、謂わば「条件の使い回し」機能です。
これキャンセルする場合は、clear() メソッドをコールすれば、それまでの宣言がすべて初期化されます。

まとめ

SOQLBuilder クラスを活用すれば、書きたい SOQL クエリを条件分岐やループ処理などと組み合わせて動的かつ直感的に記述できるようになります。
今回、この基盤開発を通して Apex クラスの仕組みをよく理解できましたし、その後の本題のプロジェクトの機能開発をとても気持ち良く進めることができています。

せっかく良いものができたので、多くの人に役立ってくれたら開発者として本望です。
ぜひテスト環境で試しに使ってみてください!

成果物

改めて、本件のより詳細な情報と実際の Apex クラスファイルはこちらの GitHub に掲載しています。
https://github.com/BruceWeyne/Apex-SOQLBuilder

謝辞

ご一読ありがとうございました!

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?