プログラマ38の日記

主にプログラムメモです。

Salesforce: データローダの起動についてのメモ(メモリ不足の対応)

Salesforceのデータをまとめて編集をする時に、データローダを使います。

環境的な制約でうまく動作しなかった時のメモになります。

※データローダのバージョンは40を前提としています。

 

環境的な問題

  1. Javaのインストールが許可されていない
  2. メモリ不足のエラー(heap size error)が出る

 

解決策

1. Javaのインストールが許可されていない

データローダは32bit、Java8以上のJavaがインストールされている前提となっています。そのため、Javaのインストールが許可されていない場合は「dataloader-40.0.0.exe」からは起動できません。

 

というのは「dataloader-40.0.0.exe」はlaunch4jで作成されていて、インストールされている32bit、Java8以上をレジストリから探すようになっているからです。例え、環境変数「Path」にjavaw.exeへのパスを通していても、「JAVA_HOME」の環境変数を設定していても起動ができないということになります。

 

そこで、まずJavaをインストールをしないで、モジュール一式をファイルとして配置します。(一旦インストールできる環境でインストールをしてからjavaファイル自体をコピーすることになります。)

f:id:crmprogrammer38:20171010091533p:plain

上のjavaモジュール一式をフォルダにつめて持って行きます。

 

次に、起動用のショートカットを作成します。

f:id:crmprogrammer38:20171010091836p:plain

上のデータローダの中身から、「dataloader-40.0.0-uber.jar」のショートカットを作成します。

作成したショートカットのプロパティから、リンク先を次のように修正します。

"~javaw.exe"  -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"

上の「~javaw.exe」 は、最初に配置したjavaモジュール一式の中のjavaw.exeへのフルパスになり、「~dataloader-40.0.0-uber.jar」は、ショートカットを作成した「dataloader-40.0.0-uber.jar」へのフルパスになります。

 

これで、Javaをインストールすることなくデータローダが使えます。

※さらにデータローダのインストールも許可されていない場合は、データローダのモジュール一式をファイルとして配置した後に、上記を行うことで起動することができます。

 

2. メモリ不足のエラー(heap size error)が出る

通常の「dataloader-40.0.0.exe」から起動したデータローダでメモリ不足のエラーがでる場合、データローダのフォルダ内にある「dataloader-40.0.0.l4j.ini」を修正することで対応します。

[変更前 dataloader-40.0.0.l4j.ini]
-Dappdata.dir="C:ProgramData"
-jar "D:programfilessf_dataloaderdataloader40dataloader-40.0.0-uber.jar"
[変更後 dataloader-40.0.0.l4j.ini]
-Xmx1G
-Dappdata.dir="C:ProgramData"
-jar "D:programfilessf_dataloaderdataloader40dataloader-40.0.0-uber.jar"

 上記のようにjavaコマンドのメモリサイズ指定を追加します。

 

「1. Javaのインストールが許可されていない」で、ショートカットから起動するようにした場合は、

[変更前 ショートカット]
"~javaw.exe"  -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"
[変更後 ショートカット]
"~javaw.exe" -Xmx1G -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"

 上記のようにjavaコマンドのメモリサイズ指定を追加します。

 

 

最後に

ロングテキストエリアの10万文字とか、大きい添付ファイルやコンテンツなどを扱うとメモリ不足のエラーが出る時があるので、メモリサイズの指定を行う機会はあると思います。

 

以前はデータローダはjavaが同梱されていたのですが、最近はjavaのインストールは別に行うようになっています。javaのサイズが大きくなったことも原因の1つなんじゃないかなーと勝手に考えています。

Salesforce: データ連携時に気をつけること(Salesforce→他システム)

Salesforceから他システムへデータを渡したい時があります。

入力はsalesforceでPCとモバイルから行い、その後入力したデータを基幹システムや分析システムに連携することはよくあることだと思います。

 

その時に気をつけておくことのメモです。

  1. 大量データを連携する方法
  2. 日付/時間の形式
  3. 数値の形式

 1. 大量データを連携する方法

データの取得は基本的に、SalesforceAPI のqueryを使うことになります。

1度に、大量データを取得する時に気をつけることがあって、取得結果が大き過ぎるとクエリがタイムアウトする場合があります。

(微妙な件数だと一度目は失敗するけど、二度目は成功するなんてこともあります。Salesforceのキャッシュの制御次第だと思います)

そういった時には、一度に取得する件数を少なくするように条件をつけることで解決します。

例えば、直近1ヶ月に作成されたデータを連携する場合

Where CreatedDate = LAST_N_DAYS:30

の条件をつけることで取得できますが、これでタイムアウトする場合は、

Where CreatedDate = LAST_N_DAYS:30
  and CreatedDate < LAST_N_DAYS:29
  

Where CreatedDate = LAST_N_DAYS:29
  and CreatedDate < LAST_N_DAYS:28  

・・・・

Where CreatedDate = LAST_N_DAYS:2
  and CreatedDate < LAST_N_DAYS:1  

Where CreatedDate = LAST_N_DAYS:1

 

と取得結果を分割することで対処します。

 

このextractを処理を、データローダのコマンドラインモードで何度も実行することになります。

 

あまり問題にはなりませんが、通常のsfdc.usernameとsfdc.passwordで指定すると、extractを実行するたびログインを行います。そのためログイン回数が増えてしいまい、イベントログではデータ連携のログインばかりになってしまいますし、APIの消費もかさみます。

 

ログインを減らすには、データローダのパラメータでセッションIDとインスタンスURLを指定することで解決できます。(ログインは最初に1度行い、そこでインスタンスURLとセッションIDの取得しておく必要があります)

process-conf.xmlに次を指定します。

<entry key="sfdc.endpoint" value="インスタンスURL(例.https://ap2.salesforce.com)"/>
<entry key="sfdc.oauth.accesstoken" value="セッションID/>

sfdc.oauth.accesstokenを指定する場合、sfdc.usernameとsfdc.passwordは不要です。

これでデータローダ実行時に、ログイン処理はスキップされます。

2. 日付/時間の形式

APIで取得した時の日付/時間のフォーマットは選べず、日付/時間はGMTの出力となります。(yyyy-MM-dd'T'HH:mm:ss'.000Z')

この形式となるため、データローダで出力した日付/時間の値を、連携先で日本時間として取り込む場合、取り込む時に、日本時間へ変換してもらうことになります。

※ETLツールを使う場合や、CSVを変換するプログラムを作らない場合の話です。

3. 数値の形式

取得する値が大きいと指数形式となります。(1.E+10 のような値)

また、Salesforceで小数点以下が0の項目としていても、データとして小数点を持っている場合があります。(apexや、APIでセットうっかりセットしている時があります)

データローダで出力したcsvを、連携先で取り込む場合は、正しい数値へ変換してもらう必要があります。(連携先の取込ツールが対応しているか、文字として取り込んでから変換するかなどになります)

 

最後に

salesforceとの連携処理は、データローダを使うことにして簡単に済ます場合が多いと思いますが、上のようなことを考慮・定義しておく必要があります。

さすがに作成日を1日に限定しないとタイムアウトするほど件数がある環境はまれだと思いますが。。

CSVの変換処理は件数が多いと意外と大変なので、どちらで変換するかは決めて、変換する側はその処理を行う時間を見ておく必要があります。

TERADATA: TERADATAの後に、普通のDBに戻るのに時間がかかった思い出

しばらくTERADATAの開発を行った後に、通常のDB(業務用のSQL ServerOracle)に戻った時がありました。

 

TERADATAでは、極力まとめて処理を行うのがポイントで、1000万件くらいであれば処理は数秒で終わります。

※TERADATAにもキャパシティはあるので、そのTERADATAがどこまでまとめていけるかは把握しておく必要はあります。

 

CSVのロードや、テーブルからテーブルへのインサートなどストレスなく処理ができます。セカンダリインデックス(いわゆるB-Treeインデックスに近いもの)は、TERADATAでは効果が低くあまり使いません。

 

3,4年TERADATAの開発後、通常のRDBの開発に戻った時に処理の組み方を忘れていて大変でした。TERADATAだと500万件のマスタと5000万件のトランザクションくらいであれば普通に結合しますが、通常のRDBでは結合するとなかなか結果が返ってきません。(もしかすると永遠に返ってこないかも)

TERADATAでは、カーソルの制御はほぼ行わないのですが、通常のRDBではカーソルを回してRDBの処理内に収まるように設計することが必要です。(もちろん、処理の前に、主キー、結合キーはきちんとインデックスを作成し、統計をとっておく必要があります。)

 

通常のRDBで設計するときは、TERADATAで扱える件数の1/100くらいの件数が扱える件数としておきかえて設計するのがベストだと考えています。

TERADATAで1000万件くらいであれば、特に考慮はしないですが、TERADATAで10億件となると、さすがに慎重に設計するので、無理なSQLは実行しません。

これを踏まえると、通常のRDBではまとめて1万件処理するのは問題ないけど、100万件処理するときは慎重に設計しようとなります。(もしかすると、今時のサーバならもっと処理できるかもしれません。今書いてるのは、ちょうど5年ほど前の話になります。)

 

最後に

扱うアーキテクチャに合わせた設計というのはとても大切です。

TERADATAだったら楽勝なのになーと思う前に、用意されているものできちんと開発しないといけないんですよね。きちんと、PL/SQL や transact/SQL でカーソルを回して処理を行うのが必要です。 

 

まぁ、頭ではわかってるんです。生活水準を上げるとなかなか下げられないということを聞きますが、TERADATAならもっと楽なのになと考えてしまうのは、それに近いのかなと思いました。

 

Salesforce: Apex開発で気をつけていること

自分がApexでの開発で気をつけていることのメモです。

  1. Salesforce IDの項目はID型で定義する。
  2. トリガでの他オブジェクトの作成/更新処理を作り過ぎない
  3. 項目自動更新とトリガを混在させない
  4. そこでしか使わない処理は無名ブロックで囲み変数のスコープを限定する
  5. insert upsert update delete を try catch で制御するならロールバックは忘れずに行う
  6. トリガのテンプレートは必ず使う

1. Salesforce IDの項目はID型で定義する。

ID型とString型は互換性があり意識しなくても問題はないのですが、ID型で定義することで、後でプログラムを読む際にSalesforce IDが入る項目ということは明確になります。(特に関数の引数をID型で定義するとわかりやすくなります)
Stringで定義すると、Salesforce IDなのかそれとも別のユニークキーなのか明確にならないので、複数人数で開発する際に認識が違ってしまうこともあります。

 

気をつけたいのがID型で定義すると、文字列がIDとして正しくないとエラーになってしまいますが、後でエラーになるよりは最初でエラーとなった方が苦労しなくてすむと思います。


2. トリガでの他オブジェクトの作成/更新処理を作り過ぎない

トリガで、ある条件によって他のオブジェクトの作成/更新を行いすぎると、後々ガバナ制限の問題がでたり、データパッチを当てるときに注意が必要になったりします。
(気をつけて作成/更新をする分にはいいのですが、知らなくて勝手に他のオブジェクトが作成されたり、更新されたりすることで障害になったりするのが問題です)
利用者の理解が得られるなら、項目の作成/更新時に自動で動くのではなく、項目を更新後にボタンをクリックしてもらいボタンの処理として実装するのが安全だと考えています。
※もちろん時と場合によるので、その時その時で判断は必要ですが。


3. 項目自動更新とトリガを混在させない

トリガの作成が見えているオブジェクトならば、項目自動更新は設定せずに、トリガの中にロジックを全て持っていくのが良いと考えています。
というのは、新規作成時でも、項目自動更新で updateも走るので、トランザクションの中の処理量が増え、ガバナ制限や、性能への影響が出てくる可能性があります。
また、項目自動更新だと実行される順番が制御できないので、トリガの方が実装が簡単な場合があります。

 

4. そこでしか使わない処理は無名ブロックで囲み変数のスコープを限定する

一時的な変数を使って制御をしたい時があります。例えば次のようなコードがあります。

    List<Case> caselist = Trigger.New;
    Map<Id, Contact> contactMap = new Map<Id, Contact>();
    Set<Id> contactset = new Set<Id>();
    for(Case c : caselist){
      if( c.ContactId != null ){
        contactset.add(c.ContactId);
      }
    }
    contactMap = new MapMap<Id, Contact>([Select Id,Name,Email From  Contact where Id =:contactset]);

上記の「contactset」は「contactMap」を作成するためだけの一時的なSetで、後続では使わないものとします。
その場合、次のように無名ブロックで囲むのはどうでしょうか。

    List<Case> caselist = Trigger.New;
    Map<Id, Contact> contactMap = new Map<Id, Contact>();
    {
        Set<Id> contactset = new Set<Id>();
        for(Case c : caselist){
          if( c.ContactId != null ){
            contactset.add(c.ContactId);
          }
        }
        contactMap = new MapMap<Id, Contact>([Select Id,Name,Email From  Contact where Id =:contactset]);
    }

こうすることで、処理で使いたい変数と、一時的な変数のインデントが分かれるのと、ブロック内で変数のスコープが
限定されるので、後続の処理でうっかり「contactset」を使ってしまうことがなくなります。


5. insert upsert update delete を try catch で制御するならロールバックは忘れずに行う

処理の中で、2つ以上のオブジェクトに対してレコードを作成/更新することはよくありますが、その処理を
try catchで囲ってしまうと、最初の作成/更新は正常で、次の作成/更新でエラーの場合、最初の作成/更新が実行されてしまいます。そのため、きちんとcatchの中でロールバックする処理を忘れないようにします。
(結構よく忘れてしまうため備忘としてアウトプットしておこうと思います)

 

6. トリガのテンプレートは必ず使う

ちょっとしたトリガだとトリガ内にロジック書きたくなるのですが、一律テンプレートを使うことで統一した方が後々楽になります。

[トリガのテンプレートサンプル]

trigger SampleTrigger on Sample__c (
     before insert
    ,before update
//  ,before delete
    ,after insert
    ,after update
//  ,after delete
//  ,after undelete
){
    
    if( Trigger.isInsert ){

        if(Trigger.isBefore ){
            SampleTriggerHandler.OnBeforeInsert(Trigger.new);
        }
        
        if (Trigger.isUpdate) {
            SampleTriggerHandler.OnBeforeUpdate(Trigger.new, Trigger.old);
        } 
        
        //if (Trigger.isDelete) {
        //  SampleTriggerHandler.OnBeforeDelete(Trigger.old);
        //}
    
    } else if( Trigger.isAfter ){

        if(Trigger.isBefore ){
            SampleTriggerHandler.OnAfterInsert(Trigger.new);
        }
        
        if (Trigger.isUpdate) {
            SampleTriggerHandler.OnAfterUpdate(Trigger.new, Trigger.old);
        }
        
        //if (Trigger.isDelete) {
        //  SampleTriggerHandler.OnAfterDelete(Trigger.old);
        //}
        
        //if (Trigger.isUnDelete ){
        //  SampleTriggerHandler.OnUndelete(Trigger.new);
        //}
    }
}

1つつのオブジェクトに1つのトリガが推奨されていますが、1つのトリガに1つのハンドラクラスである必要はありません。例えば、トリガのロジックが膨大で複数人で開発する際には、最初に人数分のハンドラクラスを作れば効率はあがります。

 たくさんクラスを作ることでSOQLの発行回数が気になる場合は、static変数を使ってSOQLの発行回数を下げるやり方が効果的です。(開発を始める前にstatic変数でキャッシュするレコードを決め、あらかじめstaticメソッドを準備しておけば開発は楽になります)

 

static変数を使う方法は次の記事に書いています。

crmprogrammer38.hatenablog.com

 

Salesforce: Force.com移行ツール(Force.com Migration Tool)で指定するコンポーネント名をselect可能なSオブジェクト

件名がやたら長くなってしまいました。

オブジェクトのメタデータは、オブジェクトの情報をまとめて取得する方法と、カスタム項目、リストビュー、レコードタイプ、入力規則、カスタムボタンまたはカスタムリンクを個別で取得する方法があります。
個別で取得する際にコンポーネント名を指定しますが、コンポーネント名は対応するSオブジェクトから取得できるものがあります。

 

まず Force.com移行ツール(Force.com Migration Tool)での指定の方法は以下の通りです。

オブジェクトの情報を全て取得する

Force.com移行ツール(Force.com Migration Tool)で、オブジェクトのメタデータを取得する際にはpackage.xmlに次のように指定します。

    <types>
        <members>Sample__c</members>
        <name>CustomObject</name>
    </types>

 上記で指定すると、オブジェクトの情報が全て取得できます。

 

カスタム項目、リストビュー、レコードタイプ、入力規則、カスタムボタンまたはカスタムリンクを個別に取得する

個別に取得する際にはpackage.xmlにそれぞれ次のように指定します。

 

カスタム項目

    <types>
        <members>Sample__c.SampleField__c</members>
        <name>CustomField</name>
    </types>

リストビュー

    <types>
        <members>Sample__c.sampleview1</members>
        <name>ListView</name>
    </types>

レコードタイプ

    <types>
        <members>Sample__c.rectype1</members>
        <name>RecordType</name>
    </types>

入力規則

    <types>
        <members>Sample__c.rule1</members>
        <name>ValidationRule</name>
    </types>

カスタムボタンまたはカスタムリンク

    <types>
        <members>Sample__c.button1</members>
        <name>WebLink</name>
    </types>

上記のように個別に指定することにより必要なものだけ取得することができます。

 

個別に指定する際のコンポーネント名をSオブジェクトから取得する

Force.com移行ツール(Force.com Migration Tool)のpackage.xmlに記載するコンポーネント名は次のSオブジェクトから取得できます。一部取得できないコンポーネント名もあります。

ラベル 対応するSObject コメント
カスタム項目 項目定義(FieldDefinition) △次のselectで取得できますが、項目レベルセキュリティが外れていると結果に含まれません。

Select EntityDefinition.QualifiedApiName
 ,QualifiedApiName
From
  FieldDefinition
where EntityDefinition.QualifiedApiName = 'オブジェクトAPI'
リストビュー リストビュー(ListView) ○次のselectで取得できます。(ビューで「自分にのみ表示」にすると結果に含まれませんが、そもそも「自分にのみ表示」のビューはメタデータが取得できないので関係ありません)
Select 
 SobjectType
 ,DeveloperName
From
  ListView
レコードタイプ レコードタイプ(RecordType) ○書くまでもないと思います。
Select 
  SobjectType
 ,DeveloperName
From
  RecordType
入力規則 ×調べたのですがありませんでした。画面からコピーするしかないのかなと思います。
ボタンまたはリンク カスタムボタンまたはカスタムリンク(WebLink) ○次のselectで取得できます。(今回は関係ありませんが、WebLinkのURL項目にはJavaScriptも書いてあるので、ロジックを調査する時は便利です)
Select
  PageOrSobjectType
 ,Name
From
  WebLink

※SオブジェクトはAPIバージョン40で確認しました。古いデータローダでは取得できない可能性があります。 

最後に

環境間の移送は変更セットが便利なのですが、標準項目が選択できないので、標準項目とレコードタイプの組み合わせや、標準項目とプロファイルの組み合わせなどを移送したい時はForce.com移行ツール(Force.com Migration Tool)を使うと便利です。

package.xmlを書くのが多少しんどいですが。

 

Salesforce: テストクラスの作成での留意事項

SalesforceのApex開発を行う場合、本番環境にリリースするためにテストクラスを作成する必要があります。

 

設定とちょっとのトリガなどの開発であれば、テストクラスの作成はさほど大変ではないのですが、サイトやコミュニティで多くの画面を作りこむ場合、テストクラスの作成に多くの労力を必要とします。

さらに、モジュールのリリースの際のテストクラス実行中の待ち時間も意外とかかるのでテストクラスの作成の仕方は工夫が必要だと感じています。

 

個人的にテストクラスの作成で思うことは次になります。

  1. テストクラスでは最小メソッド単位の呼び出しをメインとする
  2. テストデータ登録時のトリガは極力スキップする仕組みを作る

1. テストクラスでは最小メソッド単位の呼び出しをメインとする。

テストクラスは大体以下のようになると思います。

  • カスタムWebServiceの場合メソッドを実行
  • トリガの場合、あらかじめ必要なマスタを用意した後、対象のオブジェクトへレコードを作成、更新、削除を実行
  • コントローラの場合、Visualforceページで必要なパラメータを指定後、コントローラのメソッドを実行

上記のテストメソッドは必要ですが、全てのテストメソッドを上記の書き方で書く必要はないと考えています。(労力もかかりますし、テストメソッドの実行時間も長くなります)

通常、上記のメソッドの中はさらに細かいメソッドで構成されています。テストクラスでは、その細かいメソッド単位で実行するようにします。

細かいメソッドは、そのクラス内でしかアクセスしないためprivate や protected としていると思いますが、Salesforceではテストクラスからのテスト用に テストビジブルアノテーション(@TestVisible)が用意されているので、これを使っていきます。

 

テストクラスでカバレージを上げるポイントとして、通常クラスの細かいメソッド内で業務ロジックを記載する場合、SOQLを発行しないようにしておきます。(SOQLを発行するメソッドはSOQLの発行だけの機能とします)

 

コントローラクラスの作成例は次の通りです。

public class SampleController {

    public Case caseObj {get; set;}
    String caseId;
    
    public APC_CUS_200_ShinchokuDetailCtrl(){
      caseId =  ApexPages.currentPage().getParameters().get('caseId');
    }
    
    //vf action
    public void doinit(){
      caseObj = [Select Id  ,Type ,Origin ,Subject ,Priority ,ParentId ,AccountId
                 From Case where id = :caseId ]; 
    }

    public PageReference doSomething() {
      Account acc        = getAccount();
      Case    parentcase = getParentCase();
      
      doCheck(acc, parentcase);
    }
    
    private Account getAccount(){
      if( caseObj.AccountId == nul ){
        return null;
      } else {
          return [Select  Id ,Name ,Industry 
From Account where ID =:caseObj.AccountId][0];
      }
    }

    private Case getParentCase(){
      if( caseObj.ParentId == nul ){
        return null;
      } else {
          return [Select  Id  ,Type ,Origin ,Subject ,Priority 
From Case where ID =:caseObj.ParentId][0];
      }
    }
    
    @TestVisible
    private void doCheck(Account acc,Case parentcase){
    
      if( caseObj.Origin != parentcase.Origin ){
        ・・・・・
      }
    
      if( acc.Industry == 'Education' && parentcase.Origin == 'Phone' ){
        ・・・・・
      }
    }
}

そして上記のdoCheckのメソッドのテストクラスは次のようにします。

@isTest
private class SampleControllerTest {

  static testMethod void testMethod01() {
    SampleController ctrl = new SampleController();
    
    Case caseobj    = new Case();
    Case parentcase = new Case();
    Account acc     = new Account();
//必要なマスタの値のセットをする。
    
    ctrl.caseObj = caseobj;
    ctrl.doCheck(acc, parentcase);
  }
}

こうすることで、特にマスタを事前に登録しておくことも不要となり、doCheckの業務ロジックを確実にテストすることができ、カバレージも確保できます。

 

カスタムWebサービスや、トリガでも同様に、カスタムWebサービスのクラスやトリガハンドラークラスの細かいメソッドを呼び出すことで対応します。

2. テストデータ登録時のトリガは極力スキップする仕組みを作る

上記1ではデータを作成せずにテストをすることで、効率をあげるということを書いていますが、テストクラスでテストデータの作成は避けて通ることができません。

ですが、テストクラス用のデータを作成するときにそのオブジェクトのトリガが邪魔になるときがあります。
そのトリガ処理のテストをしたいわけではないのに、トリガのエラーを解決するために時間がかかったりして効率が落ちます。

そこで、データ登録時のトリガをスキップする処理を用意しておくのはどうでしょうか。そこまで大した変更をせずにテストで使いたいデータを登録ができて、本来のテストに集中することができます。(もちろん全てのオブジェクトのトリガにスキップを入れる必要はありません、効果的な箇所だけ対応するのが良いと思います)
トリガのスキップについては以前に書いていますのでそちらを参照ください。

Salesforce: カスタムボタンのJavaScriptで注意すること

Salesforceでは、標準レイアウトにカスタムボタンを配置してアプリケーションを作成していきます。(Lightning Experience ではなくClassicを対象とした場合です)

 

自分がカスタムボタンの作成時に注意することのメモです。

  1. 環境依存の文字列はカスタム表示ラベルを使う
  2. オブジェクトの値を差し込む際は、改行や「"」や「'」が入る文字列は使わない
  3. カスタムWebServiceの戻り値ではJSON文字列を返却する
  4. 制御が複雑で、ネストが深くなる場合にはtry, catchをうまく使う

1. 環境依存の文字列はカスタム表示ラベルを使う

カスタムボタンでカスタム表示ラベル( {!$Label.Label1} など)は使えるので、環境依存する文字列、例えば、接続先のURLや、項目IDなどはカスタム表示ラベルにしておきます。※でも、項目IDについては、項目定義(FieldDefinition)のデュラブル ID(DurableId)から取得することをお奨めします。

2. オブジェクトの値を差し込む際は、改行や「"」や「'」が入る文字列は使わない

下記のようなId、日付項目、数値、チェックボックスは問題ないのですが、

( {!Opportunity.Id}、{!Opportunity.CloseDate}、{!Opportunity.Amount}、{!Opportunity.IsClosed} )
次のように、値に改行がはいったり、シングルクオテーション、ダブルクオテーションがはいる可能性がある項目は差し込むとJavaScriptがエラーとなる場合があります。
( {!Opportunity.Description} )

APIコール数が増えますが、カスタムWebServiceをコールしてApex内で処理するようにします。

3. カスタムWebServiceの戻り値ではJSON文字列を返却する

カスタムWebServiceでチェックをしたり、処理結果を戻したりする際には、JSON文字列で返却すると便利です。次のようなクラスを用意しておいて

public class ReturnSample {
  public Boolean IsSuccess;
  public Boolean IsWarning;
  public Boolean IsError;
  public String Message;
  public List<String> successIdList;
  public List<String> warningIdList;
  public List<String> errorIdList;
}

カスタムWebServiceで、上記インスタンスシリアライズして返却します。

ReturnSample retobj = new ReturnSample();
retobj.IsSuccess = true;
retobj.IsWarning = true;
retobj.IsError   = false;
retobj.Message   = '処理は完了しましたが、警告があります。XXXX';
retobj.successIdList = successlist;
retobj.warningIdList = null;
retobj.errorIdList   = warninglist;

return JSON.serialize(retobj);

そして、カスタムボタンではJSON文字列をデシリアライズして使います。

var retjson = sforce.apex.execute('SampleWebService','samplemethod', { arg1 : '{!Case.Id}' } ); 
var retobj  = JSON.parse(retjson);

多少込み入った処理を実装する場合でも、必要な情報は返却できると思います。

4. 制御が複雑で、ネストが深くなる場合にはtry, catchをうまく使う

カスタムボタンである程度ロジックを組む場合があると思います。 特にやっかいなのが、確認メッセージを出して"OK"なら次へ、"キャンセル"の場合は中断するという制御です。

この制御をifを使って実装すると、ネストが確認メッセージ分深くなっていきます。そこで、次のようにtry, catchを使ってジャンプさせてしまうのはどうでしょうか。


try{
  var retjson = sforce.apex.execute('SampleWebService','samplemethod', 
{ arg1 : '{!Case.Id}' } ); 
  var retobj  = JSON.parse(retjson);

  if( retobj.IsSuccess == true ) {
    if( retobj.IsWarning == true ){
      if( confirm('次の警告が発生しています。処理を続けますか? ' + retobj.Message) == false){
         throw { quietclose : true };
      }

      var retjson2 = sforce.apex.execute('SampleWebService','samplemethod2', 
{ arg1 : retobj.successIdList ,arg2 : retobj.warningIdList } ); 
      var retobj2  = JSON.parse(retjson2);

      if( retobj2.IsSuccess == true ){
        if( confirm('次の警告が発生しています。処理を続けますか? ' + retobj2.Message) == false){
           throw { quietclose : true };
        }

        var retjson3 = sforce.apex.execute('SampleWebService','samplemethod3',    { arg1 : retobj2.successIdList ,arg2 : retobj2.warningIdList } ); 
        var retobj3  = JSON.parse(retjson2);
        ・・・・・・・・・

      } else {
        throw retobj2.Message;
      }

    }
  } else {
    throw retobj.Message;
  }

} catch( ex ){
  if( ex.quietclose == true ){
    //何もせず終了する
  } else {
    alert( ex );
  }
}

ポイントは、メッセージ表示はcatchにまかせている点、catchにquietcloseプロパティをtrueにして投げると、何も表示せずに終わるようにしている点です。

上記のサンプルでも結構わかりずらいですが、これを全てifのブロックで実現した場合、ネストが相当深くなりメンテナンスが大変になっていきます。

 

最後に

カスタムボタンや、メールテンプレートではカスタム表示ラベルが使えるのに、差込項目として選べないんですよね。
本番にリリースするたびに、カスタムボタンなメールテンプレートの文字列を本番用に変更しているプロジェクトがあると大変そうだなと思います。カスタム表示ラベルではなく直に環境依存の文字列を書いていると予想できますが、一度決めた運用なので変更しずらいのだと思います。

 

後、この慣れ親しんだカスタムボタンもLightning Experienceを採用して使わなくなる日も近いのでしょうね。。 ガバナ制限を回避のため、カスタムボタンの中でカスタムWebServiceを細かく呼び出すような設計をすることがありますが、そういった部分がLightning Experienceでどうなるのか把握できていません。なかなかLightning Experienceへ切り替えができないでいます。。