Salesforce App Cloud Advent Calendar 2015の18日目の記事ということでApex開発で押さえておきたいポイントについて投稿したいと思います。
#はじめに
Apex開発ですがいくつか押さえておきたいポイントがあり、それを対応しておかないとデータ移行で動かないトリガができたり、バッチサイズ1じゃないとエラーになるバッチができたり、保守がしずらいクラスになったりします。
今回はそういったよくハマる落とし穴の回避方法について紹介したいと思います。
#for loopとSOQLクエリ
まずはfor loopとSOQLクエリについてです。Apexでは一度の処理で実行できるクエリは100件までとなっています。そのため次のようにループ処理の中でクエリを実行しようとすると『Too many SOQL queries: 101』のエラーが発生してしまいます。
for(Integer i = 0; i < 101; i++) {
Account acc = [SELECT Id,Name FROM Account
WHERE Id =: accIds[i] LIMIT 1];
// 何か処理・・・
}
この問題はループ内で1件ずつ取得しながら処理するのではなく、クエリで取得した結果をループ内で処理するようにすれば回避できます。
List<Account> accounts = [SELECT Id,Name FROM Account WHERE Id IN: accIds];
for (Account a : accounts) {
// 何か処理・・・
}
INSERTやUPDATEなどのDML処理もループ内で1件ずつ実行するとガバナ制限のエラーに引っかかります。
for (Integer i = 0; i < 151; i++) {
Account acc = new Account(
Name = accName[i]
);
insert acc;
}
ApexのDML処理はList型にセットしてから実行することで一括実行できます。これを活用すればループ処理内でDML処理を実行しなくて済むようになります。
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 151; i++) {
Account acc = new Account(
Name = accName[i]
);
// Add List
accounts.add(acc);
}
// ループの処理で一括実行
insert accounts;
ループ内でのクエリやDML処理はこのように一括で実行するようにしましょう。
#ロールバックとID項目
ロールバック処理の注意点です。INSERTやUPDATEなどのDML処理で作成/更新/削除したレコードを処理の前まで戻してくれるロールバックですが、1つ注意点があります。
それはINSERT時にsObject変数のID項目にセットされた値がクリアされないことです。その結果、再度DML処理を実行しようとしたさいに存在しないIDに対して処理を行おうとしてエラーとなってしまいます。
これを回避するには登録用の変数を用意してcloneで値をセットしてから、そっちの変数で処理を行えば回避できると思います。
Account upsertAccount = this.objAccount.clone(true, false, true, true);
upsert upsertAccount;
詳しくはこちらにまとめてあります。
SFDC:ロールバックについて
http://tyoshikawa1106.hatenablog.com/entry/2013/03/20/231909
#スケジュールバッチの開発
Apexバッチはスケジュールに登録して一定のタイミングで処理を行うことが可能です。このスケジュールバッチにも1つ注意点があります。
それはスケジュールに登録したApexバッチは編集できなくなるということです。
この問題は海外の開発者が調べて公開してくれたスケジュールバッチ開発のデザインパターンを導入することで回避できます。
メンテナンスするたびにスケジュールの登録を解除したり、元に戻したりするのは保守性が下がるのでぜひ導入しましょう。
詳細はこちらです。
SFDC:スケジュールバッチ開発のデザインパターン
http://tyoshikawa1106.hatenablog.com/entry/2015/06/29/202657
#ApexTestとSystem.runAs
ApexテストではSystem.runAsを宣言することで実行ユーザを指定することができます。宣言しない場合はログインユーザの権限になるのですが、このSystem.runAsを宣言することで回避できるトラブルがあります。
それは『MIXED_DML_OPERATION』のエラーです。
発生してから対象のテストクラスにSystem.runAsを宣言していくのは大変なので最初から宣言しておくといいと思います。
詳しくはこちらです。
SFDC:How to Test - System.runAs と MIXED DML OPERATION
http://tyoshikawa1106.hatenablog.com/entry/2014/04/24/003139
#HTTP Calloutsのテスト
Apexでは外部サービスのAPIを実行するときなどにHTTP Calloutsの処理を実装することがあります。コールアウト処理は通常テストクラスで実行できないようになっています。
そのため、テストクラスでのテストは諦めなければならないと考えてしまいたくなりますが、"implements HttpCalloutMock"を宣言したクラスを用意することでテストクラス内で擬似的なコールアウトを実行できるようになり、テストが可能になります。
Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
詳しくはこちらです。
SFDC:HTTP Calloutsのテスト
http://tyoshikawa1106.hatenablog.com/entry/2013/09/08/213715
#CSVファイルの取込テスト
ApexでCSVファイルを読み込み処理を実行するといったことがあると思います。テストクラスではCSV読み込みの操作をしたりできないので、テストできないように思えますが、静的リソースにアップロードしたものをSOQLで取得して対応することが可能です。
静的リソースにテスト用のデータをアップしておくとどういったデータが必要かも確認しやすくなるのでおすすめです。
#マスタ系データのテストデータ作成
テストクラス内でマスタ系のデータを作成していくのはとても大変です。場合によってはINSERTの実行制限に引っかかってしまう可能性もあります。
こういった問題を回避するためにApexにはTest.loadというメソッドが用意されています。Test.loadを使うとテストクラス内で静的リソースのファイルを読み込みテストデータを作成することができます。
List<sObject> accountsList = Test.loadData(Account.sObjectType, 'AccountsTest');
作成数の制限はありますが、テストクラス内でテストする分には十分なはずです。マスタ系のテストデータを作成する際にはぜひ利用しましょう。
詳しくはこちらです。
SFDC:Test MethodのTest.loadについて
http://tyoshikawa1106.hatenablog.com/entry/2013/07/04/222428
#VisualforceとCSV読込み処理
VisualforceでCSVファイルを読み込んでデータ登録を行うというケースはよくあると思います。このとき処理件数が多くなると、処理に時間がかかり画面が固まってしまったように見えてしまいます。
特定の件数(400件ぐらい)を超えるような場合は普通にINSERTするのではなく、Apexバッチに切り替えてしまいましょう。
#System.debugの使い方
ApexにはSystem.debugで実行ログを出力できます。開発者コンソールのDebug Onlyを使えばそれ以外のログを非表示にもできます。
このSystem.debugですが、使い終わったらなるべく削除するようにしましょう。Apexトリガなどでそのままにしておくと、デバッグが必要になった際に、その他の機能のログが一緒に出力されてで確認しづらくなります。
開発者コンソールにはブレークポイントを配置して値を確認する機能もついています。(ステップの一時停止はできませんが..)何か障害が発生したときはこちらの機能で対応できると思います。
ブレークポイントの詳細についてはこちらです。
SFDC:開発者コンソールとブレークポイント
http://tyoshikawa1106.hatenablog.com/entry/2015/11/28/234827
#Apexトリガの無限ループ
よくApexトリガと項目自動更新が複雑に絡んで無限ループのような動きをしてしまうことがあります。どうしても回避できないようなケースもあるかもしれませんが、Updateトリガで1つ判定を追加するだけで、大抵回避できると思います。
それは新/旧の値の比較です。ApexトリガではTrigger.oldに更新前の値が格納されています。これをつかって新/旧の値の比較ができます。
例えば取引先番号に登録された値を別の項目にセットするような処理があるとします。Updateが実行されるたびに毎回この処理を実行すると状況によってはトリガが何度も実行されてしまいます。
次のように判定を行い、値が変更されたときのみ処理を行うようにすればこの問題を回避できると思います。
public Map<Id, List<Contact>> getAccountIdMap(List<Contact> contacts, Map<Id, Contact> contactOldMap) {
Map<Id, List<Contact>> accountIdKeyMap = new Map<Id, List<Contact>>();
for (Contact contact : contacts) {
// 変更前の取引先ID
if (contactOldMap.containsKey(contact.Id)) {
Id oldAccountId = contactOldMap.get(contact.Id).AccountId;
Contact oldContact = contactOldMap.get(contact.Id);
// 取引先IDが変更されたか
if (contact.AccountId != oldAccountId) {
// [New Contact] accountIdKeyMapにセットするContactListを取得
List<Contact> contactNewList = this.getContactListByAccountIdKeyMap(contact, accountIdKeyMap);
// Add Map
accountIdKeyMap.put(contact.AccountId, contactNewList);
// [Old Contact] accountIdKeyMapにセットするContactListを取得
List<Contact> contactOldList = this.getContactListByAccountIdKeyMap(oldContact, accountIdKeyMap);
// Add Map
accountIdKeyMap.put(oldAccountId, contactOldList);
}
}
}
return accountIdKeyMap;
}
詳しくはこちらです。
#Updateトリガのテスト
上記で紹介したようにトリガ内で新・旧の値の比較を行う処理を実装することがあると思います。以外と落とし穴なのがその処理のテストの方法です。
実際にUpdateを実行してテストを通す方法があると思いますが、ビジネスロジックの判定処理部分をテストする為だけに毎回Updateして他の処理を経由させるのは大変です。
そういった場合はcloneをつかって対応するのがおすすめです。ふたつ目の引数をfalseにすれば値のみコピーできるのでこれで新・旧の擬似的なテストデータを用意できると思います。
// テストデータ作成
Account account = CommonTester.createAccount(true);
Contact contact = CommonTester.createContact(account, true);
// 変更後取引先
Account accountOther = CommonTester.createAccount(true);
// パラメータ
List<Contact> contacts = new List<Contact>();
contacts.add(contact);
// OldMap
Contact oldContact = contact.clone(true, false, true, true);
oldContact.AccountId = accountOther.Id;
update oldContact;
Map<Id, Contact> contactOldMap = new Map<Id, Contact>();
contactOldMap.put(oldContact.Id, oldContact);
// 取引先判定
System.assertNotEquals(contact.AccountId, oldContact.AccountId);
#これからApex開発を始めたい人向けに
Apex開発をどのように進めていくかイメージしやすいように動画をつくってみました。もしかすると参考になるかもしれません。
https://www.youtube.com/playlist?list=PLFSi-6JPTf9glvEZhBJNYouZPXfd4PVIi
動画内のサンプルコードはこちらです。
https://github.com/tyoshikawa1106/salesforce-live-coding
#まとめ
Apex開発はいくつか落とし穴があってやりずらく感じることもあるかもしれませんが、上記のように押さえるべきポイントを押さえておけば大抵の問題は解決できると思います。
今回紹介したポイント以外にも落とし穴が見つかると思います。最後にそういった問題点をできるかぎりチームで把握できる方法を紹介したいと思います。
##Chatterをつかった情報共有
情報の共有にはChatterが便利です。Chatterにはコードを挿入したりする機能はないので、詳細なコードレビューには向きませんが、こういったナレッジの共有では活躍してくれると思います。
###トピック
Chatterに投稿するときトピックを紐つけることが可能です。これで最近話題になっている情報を確認しやすくなります。
トピックページではその分野に詳しい人を支持する機能もあります。スキル機能を活用することで社内で誰が知っていそうかを確認しやすくなります。
###質問機能
便利なのに以外と使われていないと思うのが質問機能です。Chatterでかなり便利な機能だと思います。
質問機能には最良の回答を選択する機能があります。この機能により複数のコメントがついた場合でも回答部分を強調表示して確認しやすくできます。
###ファイルやリンクの共有
Chatterにはファイルをアップロードしたりリンクを共有したりする機能もついています。
これらを活用することでより解決方法がまとめられたブログやGitHubのサンプルコードなどをよりわかりやすく共有できます。
###Chatterグループ
Chatterにはグループ機能も付いています。これを活用することで特定の情報のみをまとめることができます。リストに記載しないグループ機能を使えば関係の無い人には見えないグループも作成できます。
###Chatterフィードの検索
Chatterといえばせっかく投稿しても流れていってしまうイメージがありますが、そんなときはフィードの検索機能が以外と活躍してくれます。
Chatterには他にもブックマーク機能があったりもするので検索、トピック管理と一緒に活用していけばナレッジ管理として活躍してくれる機能です。ひとつひとつの投稿はそれほど大きな価値はないかもしれませんが、こういった社内ナレッジを溜めていくことで、自分がハマった落とし穴や失敗を他のメンバーに共有していくことができると思います。
以上、Apex開発で押さえておきたいポイントについてでした。
##追記
Apex開発と多言語化対応についてまとめた『Apex開発で押さえておきたいポイント - その2』を書きました。