プログラマ38の日記

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

Salesforce: 主従関係 主オブジェクト削除時の従オブジェクトのトリガの動き

主従関係にすると主オブジェクト削除時に従オブジェクトは自動で削除されますが、主オブジェクト削除した際は、従オブジェクトのトリガ(before delete, after delete)は動きません。

 

主従関係ではないですが、オブジェクトに関連付けた活動も同じ動きとなり、オブジェクト削除時に関連付けた活動は自動で削除されますが、活動のトリガ(before delete, after delete)は動きません。

 

活動データを他のオブジェクトにコピーするといった制御をトリガで実装しようとするとデータの削除をうまくひろえないタイミングがあるため注意が必要です。(こんな要求ないかもしれませんが・・)

 

 

Salesforce: リードの取引の開始をApexで行う時の注意事項

リードの取引の開始をまとめて行う場合に、Apex(もしくはAPIの)を使う場合があります。その際の注意事項を書いておこうと思います。

 

ApexでDatabase.convertLead(APIでconvertLead、以下Apex前提で記載します)を行う場合次の2点の考慮が必要です。

  1. リードの所有者がキューか
  2. 既存の取引先責任者、既存の商談に関連付けを行うか

1のリードの所有者がキューの場合、そのままDatabase.convertLeadを行うとエラーとなるので、Database.convertLeadを行う前に、所有者をユーザへ変更しておく必要があります。(所有者がユーザであれば、たとえそのユーザが無効ユーザであってもエラーになりません・・・)

2の既存の取引先責任者、既存の商談に関連付けする場合、LeadConvertクラスで

  • setAccountId(accountId)
  • setContactId(contactId)
  • setOpportunityId(opportunityId)

を指定しますが、setAccountIdで指定した取引先と、setContactIdで指定する取引先責任者の取引先は同じである必要があります。

同様に、setAccountIdで指定した取引先とsetOpportunityIdで指定する商談の取引先も同じである必要があります。
なので、既存の取引先責任者、商談に関連付けを行う場合はあらかじめ取引先をそろえてからDatabase.convertLeadをするか、Database.convertLeadをする前に取引先が同じかの判定をいれておく必要があります。


最後に

取引の開始では項目の対応付けの設定を使います。項目の対応付けは、Winter'20まではメタデータでしかリリースできず、メタデータの「LeadConvertSettings」で取得し、別環境へデプロイをしていました。
ですが、Spring'20で変更セットでリリースできるようになりました。

Salesforce: ディスクストレージの考慮事項

1レコード2KBだとずっと認識していました。大枠は正しいのですが、細かな部分が違っていたのであらためてまとめておこうと思います。
内容は以下のヘルプに記載の通りで、基本1レコード2KBですが、キャンペーンや記事などが例外となっています。(キャンペーンが8KB、キャンペーンメンバーが1KB、記事が4KB、メールメッセージはメールのサイズに応じて変化)

 

Salesforce ヘルプ [各種レコードサイズを教えてください]
https://help.salesforce.com/articleView?id=000318951&language=ja&type=1&mode=1

Salesforceヘルプ [データおよびストレージリソースの監視]

https://help.salesforce.com/articleView?id=admin_monitorresources.htm&type=5


そして、見落としがちなのが [データおよびストレージリソースの監視]の一番下に記載されている
有効またはアーカイブ済みの商品、価格表、価格表エントリ、および納入商品は、ストレージ使用量には含まれません。
という記載です。

 

商品、価格表、価格表エントリは商品情報を登録するものでそこまでストレージに影響はしませんが、納入商品は万能なオブジェクトなので使いやすくストレージへの貢献度を大きくあげることができます。最初から取引先、取引先責任者、商品への参照関係項目が用意されていて納入商品の中で階層構造になるように親、および階層のルートへの参照関係項目があります。

パレートの法則はストレージの容量にも当てはまり(例外もあると思いますが)、「使用ストレージの8割は利用オブジェクトの2割で占めている」となります。
利用オブジェクトの2割の中の1オブジェクトを納入商品にすることで大きくストレージの消費を抑えることができます。1オブジェクトだけ突出して件数が多い場合、ストレージを考慮して納入商品を使うメリットは最大化されます。
ただし、納入商品ではプロファイルの「レコードの作成時に監査項目を設定」をチェックしても作成者、作成日、更新者、更新日は設定できない/主従関係は作成できない/共有設定は取引先で設定する(納入商品設定:納入商品の共有の有効化をチェックいれることにより所有者での制御が可能になります)など、他オブジェクトとの違いはあるので使い方がはまるかは事前の検証は必要です。(逆に個人的にはこれ以外で他オブジェクトとの違いは把握していませんが・・)

 

最後に

ストレージを消費しない標準オブジェクトでは、他に商談商品(商談の子オブジェクト)、品目名スケジュール(商談商品の子オブジェクト)、注文商品(注文の子オブジェクト)があり階層を持つオブジェクトでは子オブジェクトはストレージを消費しない傾向があります。

ただし見積の子オブジェクトの見積品目名はストレージを消費しますので都度調査は必要です。見積だけ特殊なのかもしれませんが。

 

雑記: ブラウザをVivaldiに変えたらSalesforce開発が楽しくなった

まず、最初にブラウザのVivaldiはサポートされていないことはわかっています。
ですが、Vivaldiを使ってちょっと便利になってSalesforce開発が楽しくなったので設定や使い方を書いておこうと思います。

 

1.タブスタッキングが便利過ぎる

f:id:crmprogrammer38:20200224184251p:plain
タブをグループで管理できます。私はChromeなどで画面上部でタブが多くなってくるとすぐに探せなくなってしまうためこの機能があるととても助かります。
Salesforceで開発を行う際には、開発中は設定画面、開発機能の動作検証用画面、ヘルプサイトやその他の情報検索の画面など数多くの画面をタブで開くので、用途に分けてタブをグループしています。
そして、Salesforceのデプロイを行う際は複数のSandboxを扱うことが多くなり、その際には環境毎でタブをグループ化しています。こうすることで環境毎の設定の確認などやりやすくなりました。
タブスタッキングに名前を付けるにはツール>設定>タブから「タブスタックの名前の変更を許可する」の設定が必要です。

f:id:crmprogrammer38:20200224185753p:plain

2.開いているウィンドウの一覧が便利

f:id:crmprogrammer38:20200224185210p:plain


この画像は1のタブグループのタブをウィンドウ表示しています。画面が小さい場合、常にウィンドウを表示させておくのは難しいかもしれませんが、ある程度余裕があれば常にウィンドウ表示をしておくことで自分の使いたいタブを探す時間が短縮できます。

3.開発者コンソールが別ウィンドウにならなくて便利

Chromeだと開発者コンソールが別ウィンドウになります。これはこれで便利だという人はいると思いますが、個人的に別ウィンドウは非常に煩わしいと感じていました。
Vivaldeではツール>設定>外観から「タブでポップアップを開く」の設定をすることで別ウィンドウではなく同一ウィンドウでの表示となります。

f:id:crmprogrammer38:20200224192141p:plain

この設定をすると、例えばタブスタイルやClassicのルックアップ画面などのポップアップもタブでの表示となりますが特に気にはなりませんでした。

最後に

今まではChromeを使っていましたが、個人的にタブが多すぎた時の対処に困っていました。(拡張機能などで対処可能なのだと思いますが、色々試しても自分に合う拡張機能が見つけられませんでした)
冒頭にも書いていますが、Vivaldiの動作はサポートはされていないので最終的な動作はChromeなどで実施することになりますが、開発中くらいは好きなブラウザを使って開発作業を楽しみたいと思います。

Salesforce: アプリケーションメニューから、コミュニティを非表示にする

結論としては、アプリケーションに表示されているコミュニティは、プロファイルのアプリケーション設定ではなくアプリケーションメニューから非表示にできます。

 

以下詳細です。

コミュニティのメンバーとなったプロファイルや権限セットを持つユーザではアプリケーションでそのコミュニティが表示されます。

[コミュニティのメンバー]

f:id:crmprogrammer38:20200224115655p:plain

[アプリケーションの表示]
コミュニティ1、コミュニティ2、コミュニティ3がログインユーザがメンバーとなっているコミュニティ

f:id:crmprogrammer38:20200224115912p:plain

 

アプリケーションが不要の場合には、設定>アプリケーションメニューで非表示にできます。

f:id:crmprogrammer38:20200224120505p:plain


そうするとアプリケーションからコミュニティを非表示にできます。範囲は組織全体となります。

f:id:crmprogrammer38:20200224120808p:plain

アプリケーションメニューの設定は、
Spring'20時点ではメタデータとして設定が取得できないようです。

Salesforce:Lightning showToastイベント(e.force:showToast)のメモ(メッセージの改行など)

データ保存時にメッセージを表示するのに、showToastイベント(e.force:showToast)を使うことがあります。

showToastイベントについて調べたことの備忘メモです。

1.mode (画面の表示の仕方)  は 3 種類

dismissible : ×クリックで消える。durationで指定した時間経過で消える。

f:id:crmprogrammer38:20190903193244p:plain
sticky : ×クリックで消える。時間経過では消えない。

f:id:crmprogrammer38:20190903193255p:plain
pester : ×クリックできない(表示がない)。durationで指定した時間経過で消える。

f:id:crmprogrammer38:20190903193316p:plain

2.type (色やアイコン) は 5 種類

f:id:crmprogrammer38:20190903194049p:plain : info

f:id:crmprogrammer38:20190903194107p:plain : success

f:id:crmprogrammer38:20190903194121p:plain  : warning

f:id:crmprogrammer38:20190903194131p:plain : error

 f:id:crmprogrammer38:20190903194138p:plain : other

3.一度に表示できるのは 3個 のメッセージ

連続してshowToastイベントを実行しても一度の画面表示は3つまでのようです。
同時に3つ以上実行しても、表示されているメッセージが消えてから次のメッセージを表示するという動きをします。

f:id:crmprogrammer38:20190903194603p:plain

4.タイトルを指定すると、メッセージのフォントが小さくなる

f:id:crmprogrammer38:20190903194919p:plain

5.メッセージを改行するには別途スタイルシートが必要

次のような改行区切りのメッセージをToastで表示しようとすると、うまく改行されません。
メッセージ内容:
 表示メッセージ1
 表示メッセージ2
 表示メッセージ3

画面表示: 

f:id:crmprogrammer38:20190911122550p:plain

Toast内で改行する場合は、スタイルシートの指定が必要です。

1.次のスタイルシートを静的リソースに登録します。

.toastMessage.forceActionsText{
     white-space : pre-line !important;
}

2.Lightning Componentのcmpで、外部CSS リソースを読み込みます。

<ltng:require styles="{!$Resource.MultiLineToast}" />

 ※静的リソースには「MultiLineToast」として登録しました。


上記1,2を実施後の画面表示は次の通りとなります。

f:id:crmprogrammer38:20190911123514p:plain

 

最後に

この調査をするにあたって作成したLightning Componentのサンプルは次になります。

cmp:

<aura:component implements="lightning:isUrlAddressable,force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" access="global" >
  <ltng:require styles="{!$Resource.MultiLineToast}" />
    
    <lightning:layout multipleRows="true">
        <lightning:layoutItem  size="4" padding="around-small" largeDeviceSize="4" smallDeviceSize="12">

    <lightning:card title="Toastイベントサンプル">
        <table class="slds-table slds-table_bordered slds-table_cell-buffer">
            <thead>
              <th>#</th>
              <th>mode</th>
              <th>type</th>
              <th></th>
              
            </thead>
            <tbody>
                <tr><td>1</td><td>dismissible</td><td>info</td><td><lightning:button aura:id="dismissible info" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>2</td><td>dismissible</td><td>success</td><td><lightning:button aura:id="dismissible success" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>3</td><td>dismissible</td><td>warning</td><td><lightning:button aura:id="dismissible warning" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>4</td><td>dismissible</td><td>error</td><td><lightning:button aura:id="dismissible error" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>5</td><td>dismissible</td><td>other</td><td><lightning:button aura:id="dismissible other" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>

                <tr><td>6</td><td>sticky</td><td>info</td><td><lightning:button aura:id="sticky info" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>7</td><td>sticky</td><td>success</td><td><lightning:button aura:id="sticky success" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>8</td><td>sticky</td><td>warning</td><td><lightning:button aura:id="sticky warning" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>9</td><td>sticky</td><td>error</td><td><lightning:button aura:id="sticky error" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>10</td><td>sticky</td><td>other</td><td><lightning:button aura:id="sticky other" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>

                <tr><td>11</td><td>pester</td><td>info</td><td><lightning:button aura:id="pester info" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>12</td><td>pester</td><td>success</td><td><lightning:button aura:id="pester success" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>13</td><td>pester</td><td>warning</td><td><lightning:button aura:id="pester warning" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>14</td><td>pester</td><td>error</td><td><lightning:button aura:id="pester error" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
                <tr><td>15</td><td>pester</td><td>other</td><td><lightning:button aura:id="pester other" variant="brand" label="showToast" onclick="{!c.showMyToast}" /></td></tr>
            </tbody>
        </table>
    
    </lightning:card>
            </lightning:layoutItem>
        </lightning:layout>
</aura:component>

 js:

({
  showMyToast : function(component, event, helper) {
        
        var modestr = event.getSource().getLocalId().split(' ')[0];
        var typestr = event.getSource().getLocalId().split(' ')[1];
        
        
        var toastEvent = $A.get("e.force:showToast");
        toastEvent.setParams({
            mode: modestr,
            type : typestr,
            //title: '表示タイトル',
            message: '表示メッセージ1\n表示メッセージ2\n表示メッセージ3'
        });
        toastEvent.fire();
  }
})

 

Salesforce:Lightning Promiseを使ってサーバコールを順番に処理する

Lightning Componentのクライアントサイドコントローラで業務ロジックを書く時に、サーバサイドコントローラのメソッドを順次呼び出したり、全部の処理が完了した後にメッセージ表示をしたりしたい時があります。

例えば、次のようなhelperとcontrollerを用意したとします。

//helper.js    
    invokeApex1 : function(arg, component, event, helper) {
        var serveraction = component.get('c.sampleCall1');
        serveraction.setParams({
            "value": arg
        });
        
        serveraction.setCallback(this, function(response){
            var state = response.getState();
            if (state === "SUCCESS") {
                //ロジック
            }
        });
        $A.enqueueAction(serveraction);
    }
    ,
    invokeApex2 : function(arg, component, event, helper) {
        var serveraction = component.get('c.sampleCall2');
        serveraction.setParams({
            "value": arg
        });
        
        serveraction.setCallback(this, function(response){
            var state = response.getState();
            if (state === "SUCCESS") {
                //ロジック
            }
        });
        $A.enqueueAction(serveraction);
    }    
//controller.js    
    handleClick : function(component, event, helper) {
        helper.invokeApex1('invoke01',component, event, helper);
        helper.invokeApex2('invoke02',component, event, helper);
		
        var toastEvent = $A.get("e.force:showToast");
        toastEvent.setParams({
            "title": "Success!",
            "message": "処理が完了しました。"
        });
        toastEvent.fire();
    }

 上記controllerの handleClick 実行時には、helper.invokeApex1 helper.invokeApex2 が完了していなくても、"処理が完了しました。" と表示されます。
これは、$A.enqueueAction が非同期のためです。

 

 上の処理をPromiseを使って書き直すと

//helper.js   
invokeApex1 : function(arg, component, event, helper) {
        return new Promise(function(resolve, reject){
            var action = component.get('c.sampleCall1');
            action.setParams({
                "value": arg
            });
            
            action.setCallback(this, function(response){
                var state = response.getState();
                if (state === "SUCCESS") {
                    //業務ロジック
                    resolve(response.getReturnValue());
                } else {
                    reject(new Error(response.getError()));
                }
            });
            $A.enqueueAction(action);            
        });
    }    
    ,
    invokeApex2 : function(arg, component, event, helper) {
        return new Promise(function(resolve, reject){
            var action = component.get('c.sampleCall2');
            action.setParams({
                "value": arg
            });
            
            action.setCallback(this, function(response){
                var state = response.getState();
                if (state === "SUCCESS") {
                    //業務ロジック
                    resolve(response.getReturnValue());
                } else {
                    reject(new Error(response.getError()));
                }
            });
            $A.enqueueAction(action);
            
        });
    }    
//controller.js   
    handleClick : function(component, event, helper) {
        
        const invokeApex1 = helper.invokeApex1('invoke01' ,component, event, helper);
        
        invokeApex1.then(function (response){
            //response は、invokeApex1のresolveの引数なので、invokeApex1の実行結果が使えます
            return helper.invokeApex2('invoke02' ,component, event, helper);
        }).then(function (response){
            //response は、invokeApex2のresolveの引数なので、invokeApex2の実行結果が使えます
            var toastEvent = $A.get("e.force:showToast");
            toastEvent.setParams({
                "title": "Success!",
                "message": "処理が完了しました。"
            });
            toastEvent.fire();                
        } , function (error){
            //エラー発生時の制御
        });
    }

 上記のように書き換えることで、非同期処理でも順番に実行することができます。

最後に

次のようにサーバサイドコントローラのメソッドをコールする場合、$A.enqueueAction(action); を一度に実行されます。そして、例えばSOQLの発行回数などはまとめて実行したメソッド全体で加算されていきます。

helper.invokeApex1('invoke01',component, event, helper); 
helper.invokeApex2('invoke02',component, event, helper);

特に、PromiseやsetCallback の中で次のactionを順番に制御しなくてもいい場合でも、まとめて実行してしまうと予想外のガバナ制限エラーが発生してしまうので注意です。