カテゴリ「Apex」の記事

2014年11月 3日 (月)

Queueableインタフェースを使った非同期処理実行

エンジニアの谷隈です。

Winter'15から導入された新機能Queueableインタフェースについて書きます。


Queueableインタフェースはfutureメソッドと機能的にはすごく似ています。
どちらも非同期に処理を実行する仕組みです。
ドキュメントで謳われている両者の違い、Queueableインタフェースを利用するメリットは次の2点です。

・ジョブIDを取得して、進行状況を監視できる
・ジョブの連鎖が可能(Winter'15時点では2連鎖まで。Spring'15で無制限になる予定。)

次のコードはQueueableインタフェースを実装したクラスの簡単なサンプルです。
非同期に実行したい処理をexecuteメソッドの中に書きます。

public class QueueableExample implements Queueable {
    public void execute(QueueableContext context) {
		String n = 'Queueable_' + DateTime.now().getTime();
        Account a = new Account(Name=n,Phone='(03) 1234-1234');
        insert a;        
    }
}

処理のキューへの登録にはsystem.enqueueJobメソッドを使用します。
キューに入れられた処理の開始タイミングは呼び出し側のトランザクション完了後の模様。

ID jobID = System.enqueueJob(new QueueableExample());
System.debug('jobID = '+ jobID);

1トランザクション中でキューに登録できるジョブ数は50。
ガバナ制限を超えてキューにジョブを登録しようとした場合は、例外が発生します。

System.LimitException: Too many queueable jobs added to the queue: 51

ジョブの進行状況の取得はAsyncApexJobレコードを検索して取得できます。

AsyncApexJob jobInfo 
	= [SELECT Status,NumberOfErrors FROM AsyncApexJob WHERE Id=:jobID];

futureメソッドとの違いで、ちょっと面白いと思ったのはバッチジョブ内からもキューへの登録/実行が可能な点です。(バッチからfutureメソッドを呼び出した場合は例外が発生)
ただ、現時点では制限により1つしかキューにジョブを追加できないので使えるケースはかなり限られそうです。

最近の派手系追加機能と比べるといかんせん地味なQueueableですが、その内容はfutureメソッドからの順当な進化系なので抑えておいて損はない機能だと思います。


リリースノート
http://docs.releasenotes.salesforce.com/ja-JP/winter15/release-notes/rn_apex_queueing_jobs.htm

The New Apex Queueable Interface
https://developer.salesforce.com/blogs/engineering/2014/10/new-apex-queueable-interface.html

2014年9月30日 (火)

意外と知らないSalesforce Tips (8~10)

玉澤です。
意外と知らないSalesforce Tipsの残り3つを紹介します。


8. Salesforce標準のログイン画面をブランディングする

Salesforceの標準のログイン画面を変えたいお客様の会社のイメージでブランディングしたいときに役立つTipsです。

下記の手順で、Salesforce標準のログイン画面をカスタマイズできます。

まず、[管理]→[ドメイン管理]→[私のドメイン]から組織のサブドメインを登録します。
※組織のサブドメインは一度しか登録できないため、慎重に選択してください。

20140930_81

登録したら、↓のような画面になるので、「こちらをクリックしてログインしてください」を押下して、
ドメインのテストを実施します。

20140930_82
テスト後、↓のような画面になるので、「ユーザにリリース」を押下して、リリースします。
※新しいドメイン名をリリースした後は、元に戻せません。リリース後は、すべてのユーザが新しいドメインにリダイレクトされるので、注意してください。

20140930_83
リリース後、ログインページのブランド設定が表示されるので、ロゴ、背景色、右フレームのURLを表示します。

20140930_84
設定後、リリースしたドメインにアクセスすると、↓のように、ログイン画面がカスタマイズされて表示されるようになります。

20140930_85

9. タブのデフォルト表示を「最近使った○○」ではなくする

タブをクリックしたときに、「最近使った○○」(※○○はオブジェクト名)というビューではなく、通常のビュー画面をデフォルト表示したい場合に役立つTipsです。

20140930_91 まず、通常のビュー画面を表示するVisualforceページを作成します。↓のコードを書けばOKです。

<apex:page tabStyle="Rsv2__c"> ・・・オブジェクト名を指定
  <apex:enhancedList type="Rsv2__c" ・・・オブジェクト名を指定
             customizable="false" 
             height="600" 
             rowsPerPage="50" />
</apex:page>


次に、タブのデフォルト表示を変更したいオブジェクトの設定画面を開き、「ボタン、リンク、およびアクション」セクションで、タブの設定編集リンクをクリックします。

20140930_92
タブの設定編集画面で、上書き手段で「Visualforceページ」を選択し、先程作成したVisualforceページを選択します。

20140930_93
すると、タブをクリックしたときに、「最近使った○○」というビューではなく、通常のビュー画面が表示されるようになります。

20140930_94
10. レコード詳細画面にグラフを表示する

レコードの詳細画面にグラフを表示したいときに役立つTipsです。対象のレコードに関連するレポート結果だけをグラフ表示できます。

ここでは、取引先レコードの詳細画面で、その取引先の商談をフェーズ毎に集計したグラフを表示する方法を例に説明します。

まず、レポートをグラフ付きで作成します(今回の例では、標準の商談レポートタイプで、フェーズ毎に集計したサマリレポートを作成します)。

20140930_101

20140930_102
次にグラフを表示したいオブジェクトのページレイアウトの設定画面を表示します。レポートグラフのセクションに、先程作成したレポートが表示されているので、ドラッグ&ドロップでレイアウトに追加します(今回の例では、取引先のページレイアウトを表示し、先程作成したレポートグラフを追加します)。

20140930_103
追加後、プロパティ設定で、表示サイズ、タイトル表示の有無、絞り込み条件を設定します。
グラフは、元々のレポートの集計結果から、更に絞り込み条件で指定した項目で絞り込んだ結果が表示されます(今回の例では、取引先IDを指定して、その取引先の商談だけが表示されるようにします)。

20140930_104設定すると、↓のように、レコード詳細画面でグラフが表示されるようになります(今回の例では、取引先レコードの詳細画面で、その取引先の商談をフェーズ毎に集計したグラフが表示されます)。

20140930_105

2014年8月20日 (水)

意外と知らないSalesforce Tips (6~7)

またもやこのシリーズです。
今回は6つめと7つめをお送りします。

6. VisualforceをRead-Onlyモードで実行する

Apexの実行には実はRead-Onlyモードというものがあります。
これ、何か良いことがあるのかというとガバナ制限が緩和されるというメリットがあります。

どうしても大量のデータを扱いたいといったケースなどには、特別な細工をせずに扱えるデータ件数が増えるので利用価値がありそうです。

Read-Onlyモードの設定の仕方には2通りの方法があります。

(1) Visualforceページ全体をRead-Onlyモードにする
(2) コントローラークラスのメソッドをRead-Onlyモードにする

どちらの方法でモード設定するかによって緩和される制限に差があります。

具体的な緩和されるガバナ制限の内容は下記になります。

・1トランザクションで取得可能なレコード数上限が緩和される

50,000件 → 1,000,000件

・繰り返しコンポーネントでの使用可能コレクションサイズの上限が緩和される

1,000件 → 10,000件
対象の繰り返しコンポーネントは<apex:repeat><apex:dataTable><apex:dataList>の3つ
※Visualforceページ全体をRead-Onlyモードにした時のみ適用されます。

ここからはRead-Onlyモードの設定の仕方について書いていきます。

まずは、Visualforceページ全体をRead-Onlyモードにする方法から。
これはすごく簡単で、<apex:page>タグにパラメータ「readOnly="true"」を追加するだけです。

使用例: Visualforce側

<apex:page readOnly="true" controller="ReadOnlyController" action="{!init}" >
    <apex:repeat value="{!accountList}" var="ac">
        <apex:outputField value="{!ac.Id}"/>
        <apex:outputField value="{!ac.Name}"/>
        <hr />
    </apex:repeat>
    <apex:form>
        <apex:commandButton action="{!save}" value="Save"/>
    </apex:form>
</apex:page>

使用例: コントローラー側

global class ReadOnlyController {

    public List<Account> accountList {get; set;}
    
    public PageReference init(){
        accountList = [SELECT Id, Name FROM Account];
        return null;
    }
    
    // Read-Onlyモードだとこの処理は失敗する
    public PageReference save(){
        update [SELECT Id, Name FROM Account LIMIT 1];
        return null;
    }
}

コントローラー側は特に何もする必要はありません。
ちなみに、Read-Onlyモードの時にコントローラー側でデータの更新をしようとすると「Too many DML statements」の例外が発生します。

次はコントローラーのメソッド単位でRead-Onlyモードを設定する方法です。
コントローラーのメソッドにRead-Onlyモードを設定するには「@ReadOnly」あのテーションを付与します。
ただ、どんなメソッドにもアノテーションを付けられる訳ではなく、次の条件を満たしている必要があります。

・ global もしくは public である
・ static である
・ @RemoteAction アノテーションが付いている

ちなみにVisualforceということを抜きにすれば「@ReadOnly」自体はwebserviceなどでも使用できます。
詳細はDeveloper's Guideを見てください。

Force.com Apex Code Developer's Guide - ReadOnly Annotation
https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_classes_annotation_ReadOnly.htm

Visualforce Developer's Guide - Working with Large Sets of Data
https://www.salesforce.com/us/developer/docs/pages/Content/pages_controller_readonly_context.htm

では、実際の使用例です。

使用例: Visualforce側

<apex:page controller="ReadOnlyController2">
    <apex:includeScript value="//code.jquery.com/jquery-1.11.0.min.js" />
    <apex:includeScript value="//code.jquery.com/jquery-migrate-1.2.1.min.js" />
    <apex:includeScript value="//cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js" />

    <div data-bind="foreach: accountList">
        <span data-bind="text: Id" /> / <span data-bind="text: Name" />
        <hr/>
    </div>
    
    <button data-bind="click:save1">Save1</button><br/>
    <button data-bind="click:save2">Save2</button><br/>
    
    <apex:form>
        <apex:commandButton action="{!save3}" value="Save3"/>
    </apex:form>

    <script>
        jQuery.noConflict();    
        jQuery(document).ready(function($){
            var viewModel = new (function(){
                var self = this;
                self.accountList = ko.observableArray();
                
                self.find = function(){
                    Visualforce.remoting.Manager.invokeAction(
                        '{!$RemoteAction.ReadOnlyController2.find}',
                        function(result, e){
                            if(!e.status){
                                alert('error:' + ko.toJSON(e));
                                return;
                            }
                            self.accountList(ko.utils.arrayMap(result, function(o){
                                return {
                                    Id: ko.observable(o.Id),
                                    Name : ko.observable(o.Name)
                                };
                            }));
                        },
                        {escape :false}
                    );
                };
                
                self.save1 = function(){
                    Visualforce.remoting.Manager.invokeAction(
                        '{!$RemoteAction.ReadOnlyController2.save1}',
                        function(result, e){
                            alert((e.status?"success":("failed"+ko.toJSON(e))));
                        },
                        {escape :false}
                    );
                };
                
                self.save2 = function(){
                    Visualforce.remoting.Manager.invokeAction(
                        '{!$RemoteAction.ReadOnlyController2.save2}',
                        function(result, e){
                            alert((e.status?"success":("failed"+ko.toJSON(e))));
                        },
                        {escape :false}
                    );
                };
                
                //initialize
                self.find();
            })();
            ko.applyBindings(viewModel);
        });
    </script>
</apex:page>

使用例: コントローラー側

global class ReadOnlyController2 {
   
    @ReadOnly @RemoteAction global static List<Account> find() {
        return [SELECT Id, Name FROM Account];
    }
    
    // Read-Onlyモードなのでこの処理は失敗する
    @ReadOnly @RemoteAction global static void save1() {
        update [SELECT Id, Name FROM Account LIMIT 1];
    }
    
    @RemoteAction global static void save2() {
        update [SELECT Id, Name FROM Account LIMIT 1];
    }
    
    public PageReference save3(){
        update [SELECT Id, Name FROM Account LIMIT 1];
        return null;
    }
}

ページ全体がRead-Onlyモードになるわけではないので通常のメソッドを呼び出してデータを更新することもできます。

いかがだったでしょうか。
機会があったら活用してみてください。

7. 変更セットへ楽に項目を追加する

変更セット使ってますか。
変更セットは便利といえば便利なのですが、そのUIはお世辞にも使い易いとは言えません。
特にカスタム項目数が数千件ある時に、そのなかから必要なものを選択する場合は一苦労です。

こんなときに次の手順を踏むと少しだけ項目を選ぶのが楽になります。

(1) 一覧で「▼増やす」のリンクをクリック

(2) URLに追加されるパラメータ「rowsperpage」を1000に書き換えたURLに移動

これで一気に一覧の表示件数を上限の1000件に変更することができます。
あとはソートするなり、ページ内検索するなりすればほんの少し楽に項目追加ができるようになります。

さらに手動でURLを書き換えるのすら面倒だという方のために一覧表示を1000件にするブックマークレットを用意してみました。
(やっていることは表示しているURLにパラメータ「rowperpage」があれば値を1000に変更し、なければ値を1000で追加して遷移させているだけです。)

javascript:(function(){var l=location;var h=l.href;if(!(/entityType=/i).test(h)){h+=('&entityType='+document.getElementById('entityType').value);} l.href=((/rowsperpage=[0-9]+/i).test(h))?h.replace(/rowsperpage=[0-9]+/ig,'rowsperpage=1000'):(h+'&rowsperpage=1000');})();

一応、Chromeで動作確認してあります。
もし良かったら使ってやってください。

それでは。

2014年8月18日 (月)

意外と知らないSalesforce Tips (4~5)

前回からの続きです。
今回は4つめと5つめをお送りします。

4. オブジェクトでトピックを利用する

Spring'14からChatterのトピック機能がオブジェクトでも利用できる様になりました。
トピックはTwitterで言うところのハッシュタグみたいなもので、レコードの整理に使うことができます。

・使うための準備
オブジェクトに対してトピック機能を有効化する必要があります。
Spring'14以降に作成した組織の場合はトピックを使用可能なすべての標準オブジェクトは、デフォルトで機能が有効になっている様です。

トピックの有効化は「カスタマイズ>トピック>オブジェクトのトピック」から行います。

Mame20140818_01

オブジェクトのトピックが有効になると、そのオブジェクト種別のレコードで公開タグが無効になるので、公開タグを使っている場合は注意が必要です。

・使ってみる
トピックをレコードに追加するには詳細画面を開いて、タイトルの下にある「クリックしてトピックを追加」リンクをクリックします。
(すでにトピックが追加済みの場合はリンク名が「トピック」になります)

Mame20140818_02

トピック名を入力して「完了」をクリックするとトピックが追加されます。

Mame20140818_03

追加されたトピック名をクリックすると、対象のトピックが付与されているレコードが一覧で表示されます。

Mame20140818_04

Mame20140818_05

また、トピックはビューの絞り込み条件に指定することもできます。
トピックを絞り込み条件に指定する場合は、絞り込みの項目で「トピック」を選びます。

Mame20140818_06

次はApexからトピックを操作してみます。
ConnectApiネームスペースのTopicsクラスがこの辺りの処理の為のメソッドを持っています。

トピックを追加するレコードのIDとトピック名を指定してトピックを追加します。
(第1引数はコミュニティのIDでコミュニティに対してトピックを追加する時でなければ、nullを指定します。)

String communityId = null;
String recordId = 'a04U000000Jj29h';
String topicName = 'トピックテスト';
ConnectApi.Topic objTopic = ConnectApi.Topics.assignTopicByName(communityId, recordId, topicName);

トピックのIDを指定して削除します。
(第1引数のコミュニティIDの扱いは追加の時と同じです。)

String communityId = null;
String topicId = '0TOU00000008ePB';
ConnectApi.Topics.deleteTopic(communityId, topicId);

トピックが追加されているレコードの一覧はTopicAsssignmentオブジェクトから取得できます。

Select Id, EntityId, TopicId FROM TopicAssignment WHERE TopicId = '0TOU00000008eP6OAI'

EntityIdにレコードのID、TopicIdにトピックのIDが入っています。
(組織でコミュニティが有効化されている場合はオブジェクトにNetworkIdカラムが追加され、ここにコミュニティのIDが入ります。)

・権限
トピック機能を扱うにはトピックの権限とレコードに対する権限が必要です。

一般ユーザ権限説明
トピックを割り当てる レコードへのトピックの追加・削除に必要です。
トピックを作成 トピックそのものを作成するのに必要です。
トピックを削除 トピックそのものを削除するのに必要です。
トピックを編集 トピックそのものを編集するのに必要です。

トピックを割り当てるには対象のレコードを更新できる必要があります。
参照権限のみの場合は、参照はできますが、割り当てはできません。

トピック機能は種類の異なるオブジェクトを関連づける事もできるので、うまく使って効果的にレコードを整理してみてください。

5. レコードの閲覧日時と参照日時を参照する

最近使ったXXX的な一覧を作ってみたくなったことはありませんか。
まさにそのためだけに作られたような項目が Summer'13 から各オブジェクトに追加されています。

「LastReferencedDate」と「LastViewedDate」がその項目です。

この2つの項目はどちらも読み取り専用の項目で、違いは項目の値が更新されるタイミングです。
詳細は次の表にまとめましたのでご覧ください。

 LastReferencedDateLastViewedDate
レコードの詳細画面を見たとき 更新される 更新される
レコードを画面からルックアップして参照項目に設定したとき 更新される 更新されない

このように画面で操作したときは上のタイミングで項目が更新されるのですが、Apexからの操作の場合は何もしなければこの2項目は更新されません。
Apexから更新したい場合は次のオプションを付けたSELECT文を発行することでこれらの項目を更新することができます。

詳細画面を見たときと同じ更新を行う
  ・・・ 「FOR VIEW」オプションを付与します。

SELECT Id,Name,AccountId,LastViewedDate,LastReferencedDate FROM Contact
WHERE ID='003U00000024dJgIAI' FOR VIEW

レコードを画面からルックアップして参照項目に設定したときと同じ更新を行う
  ・・・ 「FOR REFERENCE」オプションを付与します。

SELECT Id,Name,AccountId,LastViewedDate,LastReferencedDate FROM Contact WHERE
ID='003U00000024dJgIAI' FOR REFERENCE

機会があったら使ってみてください。

それでは。

2014年8月 8日 (金)

意外と知らないSalesforce Tips

こんにちは。エンジニアの若葉です。

先日、社内で「意外と知らないSalesforce Tipes 10撰」という題目で
ネタを持ち寄ったので、とりあえず3つ程ご紹介しますね。

1. 標準ボタンによる画面リロード

まず、標準のリストビューにビューの選択状態を保ったままリロードする方法をご紹介します。

例えば、申請→処理中→完了のような状態が遷移するオブジェクトがある場合、

①各ステータスに応じたリストビューを設定してステータスごとにデータを管理する

②一括で状態を更新するApexを書いて、リストビューにカスタムボタンを配置

③カスタムボタンを実行したら、更新ボタンでリフレッシュして、現在のビューから当該のデータを追い出す

なんてケースが良くあるかと思います。


ただ、実行と更新が2段階に分かれていているので、処理件数が多い場合には、
更新ボタンを押し忘れて、処理済みのデータを対象に再度更新処理を実行してしまう恐れが残ります。
そこで、カスタムボタンに記述するJavascriptに以下のコードを追加しておくと、

var views = ListViewport.instances, i;
for (i in views) {
    if (views.hasOwnProperty(i)) {
// 現在のリストビューを保持したままリロード views[i].refreshList(); return; } }

現在のビューを保ったままリロードができます。
事故の防止、操作の簡易化が図れてとても便利ですね。

2. Sitesで強制SSL

次ですが、Sitesで公開WEBフォームを作る際、住所情報などを登録するので
SSLアクセスが必須となるケースが良くあります。

設定によってSSLアクセスを強制できる助かるのですが、残念ながらその設定は無いようです。
そこで、

①フォームを表示するリクエストをチェックして、

②httpだったらhttpsにしてリダイレクトする

という感じの機能を実装するのですが、URL関連のAPIがわかりにくく、
毎回リファレンスを見直しているので、この際整理してみました。

使用するAPI

プロトコルの確認 URL.getProtcol()
現在のセキュアなドメインの取得 Site.getBaseSecureUrl()
現在のPathの取得 URL.getPath()

Sitesのドメインはsand/prod間でドメインが異なるので、環境差分を吸収するのに一工夫必要です。このAPIを知るまでは、この環境違いへ対応するのにカスタム表示ラベルに切り出すなど、悲しい対応をしていました。。

また、Pathの取得はちょっと注意が必要で、取得できるPathは実際のURL中に現れない「/apex」が含まれてしまうので、リダイレクトさせるには、これを除外する必要あります。

使用例

// 現在のURL情報
public class CurrentUrl {
    public Boolean isSecure() {
        return (URL.getCurrentRequestUrl().getProtocol() == 'https');
    }
    public Boolean isInSecure() {
        return !isSecure();
    }
    
    public String getSecureSiteUrl() {
        String baseUrl = Site.getBaseSecureUrl();
        String path = URL.getCurrentRequestUrl().getPath();
        Integer pos = path.lastIndexof('/');
            return baseUrl + path.substring(pos, path.length());
        }
    }
    

こんな感じで、現在のURLを取り扱うクラスを定義しておくと、
コントローラからシンプルに呼び出せて、使いまわしが簡単になります。

    // 呼び出し側
    CurrentUrl cu = new CurrentUrl();
    if (cu.isInSecure()) {
        // httpsでリダイレクト
        PageReference pr = new PageReference(cu.getSecureSiteUrl());
        pr.setRedirect(true);
        return pr;
    }
    return null;
}

3. MIXED_DML_EXCEPTIONの避け方

最後です。テストコードが失敗する場合に、

MIXED_DML_OPERATION, 非設定オブジェクトを更新した後の設定オブジェクト上の DML操作 (またはその逆) は、許可されていません

この例外が原因となっているケースがたまにあります。


サンプル

@isTest
public static void test() {
    User u = getUser();
    insert u;//・・・(1) 設定オブジェクト

    MixedDmlSample md = new MixedDmlSample();
    Id accountId = md.createAccount(u.Id);
    System.assertEquals('flect', [SELECT Name FROM Account WHERE Id=:accountId][0].Name);
}

// プロダクトコード
public class MixedDmlSample {
    public Id createAccount(String userId) {
        Account a = new Account(Name='flect');
        insert a;//・・・(2) 非設定オブジェクト
        User u = [SELECT LastName FROM User WHERE Id=:userId];
        Contact c = new Contact(LastName=u.LastName, Account=a);
        insert c;//・・・(3) 非設定オブジェクト
        return a.Id; 
    }
}

これは、例えば上記のサンプルでいうと、User(1)とAccount(2)は、同一トランザクションでInsert/Updateできない、というような制限です。

詳細は以下のリンクから確認できますが、制限の理由は、UserもAccountも、アクセスする側とされる側、としてレコードの共有情報を更新する必要があり、それは各々のDMLを直列に確定させないと正しく処理できない可能性があるためのようです。

http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_dml_non_mix_sobjects.htm

上記のサンプルのように、プロダクトコードのみでは該当しないけど、テストデータとしてUserを事前にInsertすることは多いので、この制限はテストコードでよく遭遇します。

制限だから仕方いないかと思いきや、そこはちゃんとテストコード用に回避策が用意されています。

https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_dml_non_mix_sobjects_test_methods.htm

回避例

@isTest
public static void test() {
    User thisUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
    System.runAs (thisUser) {//・・★ これで囲うだけ
    User u = getUser();
    insert u;

    MixedDmlSample md = new MixedDmlSample();
    Id accountId = md.createAccount(u.Id);
    System.assertEquals('flect', [SELECT Name FROM Account WHERE Id=:accountId][0].Name);
    }
}


System.runAs ブロックで囲ってあげれば、それだけでOKです。
アクセス制御に関するテストはどうなるんだろ?とか追加の調査の余地はありますが、とりあえずは回避策があるので良しとしておきます。

それでは。

2014年7月 8日 (火)

Apex一括メール送信の制限

お久しぶりです、こころです。

Apexからの一括メール送信には

”1 組織あたり 1 日に合計 1,000 個の外部メールアドレスまで” 

つまり

”アドレス重複を含んで1日1000通まで”

というガバナ制限があります。

1日で閾値ギリギリまでメール配信されそうな要件があり、閾値を超えるとどうなるのか試してみました。

例えば1回に75通Apexからメールを送るとして、
13回送信すると975通になります。
14回目にどうなるのでしょうか?

①エラーになってメールは1通も配信されない
②25通まで配信され1000に達した後は配信されない
③なんと1000通超えて配信完了する

①であって欲しいと思いながらメールの一括送信用メソッドMassEmailMessageのレファレンスを確認すると、

sendEmail メソッドで送信される一括メールメッセージは、送信する組織の 1 日の一括メール制限にカウントされます。この制限値に達すると、MassEmailMessage を使用する sendEmail メソッドへのコールは拒否され、ユーザは MASS_MAIL_LIMIT_EXCEEDED エラーコードを受信します。

とあり、MASS_MAIL_LIMIT_EXCEEDEDの解釈が問題となりそうです。

DeveloperEdition(配信制限数は10通/日)でテストしてみました。

Apexクラス

public class sendMassEmail {
    public List contactIds = new List();

    public static void sendMass() {
    List conObj = [SELECT id, name FROM Contact LIMIT 3];//配信対象件数
    List contactIds = new List();
    for(Integer i=0; i < conObj.size(); i++){
        contactIds.add(conObj[i].Id);
    }
    Messaging.MassEmailMessage mail = new Messaging.MassEmailMessage();
    mail.setTargetObjectIds(contactIds);
    mail.setTemplateID('00X10000000NzIC'); //送信メールのテンプレートのID
    Messaging.sendEmail(new Messaging.MassEmailMessage[] {mail}); 
    }
}

ページ

<apex:page controller="sendMassEmail">
    <apex:form >
        <apex:commandButton action="{!sendMass}" value="一括送信"/>
    </apex:form>
</apex:page>

ボタンを1回押すと3通の一括送信メールが送られます。DeveloperEdition組織の一括メール送信制限は10通です。3回ボタン押下で9通配信されますが、4回目で何が起こるでしょう。

※一括メール送信はログインユーザで実行されます。

20140708_14439_2

3回クリックして4回目は、、

20140707_234827_2

キタ!! System.EmailException: SendEmail failed. First exception on row 0; first error: MASS_MAIL_LIMIT_EXCEEDED, Failed to send email: []

のエラーになりました。

デバッグログを確認すると

00:47:08.033 (33768787)|EXCEPTION_THROWN|[42]|System.EmailException: SendEmail failed. First exception on row 0; first error: MASS_MAIL_LIMIT_EXCEEDED, Failed to send email: []
00:47:08.034 (34189036)|SYSTEM_METHOD_EXIT|[42]|Messaging.sendEmail(LIST<Messaging.Email>)
00:47:08.034 (34251510)|FATAL_ERROR|System.EmailException: SendEmail failed. First exception on row 0; first error: MASS_MAIL_LIMIT_EXCEEDED, Failed to send email: []


4回目の一括配信メールは失敗して送信されませんでした。
(もちろん最初の9通は届きましたが、それ以降は1通も届いていません。)

設定>ログ>メールログファイル よりメールログをリクエストして確認すると、やはり9通です。

1

というわけで予想通り①でした。

実は検証を仕込んでいる間に、

SendEmailResult オブジェクトで返されるすべてのエラーは、メールが送信されなかったことを表します。

の文字をApexレファレンス中(P294)に見つけてしまって、検証するまでもなく①な気が限りなくしていたのですが、念のため確認してみました。

Sandboxで1000通の制限に対してのテストをしてみましたが、同様に一括メール件数が閾値を超えるリクエストの場合は配信はされません^^

Apexからの一括メール配信を設計する上で参考にしてみてください。

2012年2月27日 (月)

Apex共有で所有者と同じロールへレコードを共有する方法

Salesforceでは、組織の共有設定が「非公開」に設定されている場合、
同じロールに属するユーザが所有するレコードは参照できません。
営業部ロールにAさんとBさんが属していた場合、Aさんが所有するレコードをBさんは参照できない

一方で、「同じロールに属するユーザが所有するレコードは参照できるようにしたい
営業部の誰かが所有するレコードは、営業部全員で参照できるようにしたい)」
という要望を頂くことは結構多いような気がします。

この要望は、↓の共有ルールを作成すれば実現できます。
所有者の所属:「営業部ロール」
共有先:「営業部ロール」
アクセス権:「参照のみ」

但し、この方法だと、このルールを適用するロールの数だけ共有ルールを作成しなければなりません。また、ロールの追加や変更の度に、共有ルールを追加・変更しなければなりません。

大規模な組織などで、ロールの数が多い場合や、ロールの追加・変更が多い場合は、
下記で紹介する方法で、トリガからApex共有を使って、自動的に対応するロールにレコードを共有すると良いでしょう。

トリガでApex共有を使って、レコード共有する方法

1. トリガを作成するオブジェクトに、Apex共有の理由を作成

理由表示ラベルと理由名を設定(どちらも任意の名称でOK)して、Apex共有の理由を作成します。

ここでは、
理由表示ラベル:所有者のロールへの共有
理由名:OwnerRoleSharingReason
と設定します。

20120227_1

2. トリガ作成

オブジェクトに所有者のロールへレコードを共有するトリガを作成します。
ここでは、Item__cというカスタムオブジェクトに作成する場合のソースコードを記載します。

Trigger ShareItemTrigger on Item__c (after insert, after update) {
  // レコードIDリストの作成
  // 所有者IDリストの作成
  List<ID> recordIdList = new List<ID>();
  List<ID> ownerIdList = new List<ID>();
  for(Item__c item : Trigger.new){
    recordIdList.add(item.Id);
    ownerIdList.add(item.OwnerId);
  }

  // 共有レコード追加リストの作成の
  // 共有先ユーザ/グループの設定のところで、
  // 所有者IDに対応するグループIDを取得したいので、
  // 「所有者ID-ロールID マップ」と
  // 「ロールID-グループID マップ」を作成する
  //
  // 所有者ID-ロールID マップの作成
  Map<Id, Id> ownerIdMap = new Map<Id, Id>();
  for(User u : [Select Id, UserRoleId From User 
                Where Id = :ownerIdList]) {
    ownerIdMap.put(u.Id, u.UserRoleId);
  }

  // ロールID-グループID マップの作成
  Map<Id, Id> roleIdMap = new Map<Id, Id>();
  for(Group grp : 
    [Select Id, RelatedId From Group 
     Where RelatedId = :ownerIdMap.values() 
     and Type = 'Role']) {
     roleIdMap.put(grp.RelatedId, grp.Id);
  }

  // 共有レコード追加リストの作成
  // (共有オブジェクトに追加するレコードを作成する)
  List<Item__Share> addShareList 
    = new List<Item__Share>();
  for(Item__c item : Trigger.new) {
    Item__Share shareObj = new Item__Share();

    // 共有レコードID
    shareObj.ParentId = item.Id;

    // 共有先ユーザ/グループ
    // (所有者のロールに対応するグループIDを設定)
    shareObj.UserOrGroupId = 
      roleIdMap.get(ownerIdMap.get(item.OwnerId)); 

    // アクセス権限
    // 参照のみ : read
    // 参照・更新 : edit
    shareObj.AccessLevel = 'read';
    
    // 共有の理由
    shareObj.RowCause = 
      Schema.Item__Share.RowCause.OwnerRoleSharingReason__c;

    addShareList.add(shareObj);
  }

  // 共有レコード削除リストの作成
  List<Item__Share> delShareList =
    [Select Id From Item__Share 
     Where ParentId = :recordIdList 
     and RowCause = 
     :Schema.Item__Share.RowCause.OwnerRoleSharingReason__c];

  try {
    // 共有の理由が「所有者のロールへの共有」である
    // 共有レコードを削除してから、追加する
    delete delShareList;
    insert addShareList;
  }
  catch(System.DmlException e) {  
  }
}

トリガ作成後に、レコードを追加・更新すると、所有者のロールにレコードが共有されるようになります。

レコードの詳細画面で「共有」を押下すると、↓のように、所有者のロールへ共有されていることが分かります。

20120227_2

2011年4月12日 (火)

beforeトリガとafterトリガの違いは?

beforeトリガafterトリガの特徴をまとめてみました。

○beforeトリガ
レコードがDBに保存される前に起動されます。
レコードがDBに保存される前のため、Trigger.newを直接変更することができます
(before insert、before updateトリガ)。

○afterトリガ
レコードがDBに保存された後に起動されます。
但し、まだコミットはされていません(重要)。

DBに保存された後のため、
・Id
・自動採番
・作成者
・作成日時
・更新者
・更新日時
などのDBで設定された項目の値にアクセスできます

DBに保存された後のため、Trigger.newを直接変更することはできませんが、
DML操作で元レコードの更新・削除を行うことができます
(after insert、after updateトリガ)。

○実行順序
beforeトリガ、afterトリガ、必須、入力規則のチェックは、
↓の順番で実行されます。

1. beforeトリガ
2. 必須、入力規則のチェック
3. afterトリガ

○使い分け

開発時には、
・入力規則のチェックより前に実施したい場合には、beforeトリガを使う
・DBで設定された値にアクセスしたい場合には、afterトリガを使う
など、両者の特徴を生かして、使い分けると良いと思います。

最後に、特徴をまとめた表を載せておきます。

beforeトリガ afterトリガ
起動タイミング レコードがDBに保存される前 レコードがDBに保存された後
DBで設定する項目へのアクセス 不可
Trigger.newの項目の値の変更 可 ※1 不可 ※2
元レコードのDML操作(更新・削除) 不可 ※1 可 ※2
実行順序 必須、入力規則のチェックの前 必須、入力規則のチェックの後

※1 before insert、before updateの場合
※2 after insert、after updateの場合

2010年11月 9日 (火)

with sharingキーワードとwithout sharingキーワード

Apexはデフォルトでシステムモードで動作するので、全オブジェクトに対して全レコードの編集が可能です。

そのため、例えば、ユーザがVisualforceページでレコードを検索する場合、ユーザが参照権限を持たないレコードについても、検索できてしまいます。

ユーザが参照可能なレコードのみを検索できるようにするには、Apexクラスにwith sharingキーワードを付与します。
これによって、ユーザに適用されているレコードレベルの共有ルールを強制実行することができますユーザモードでの動作)。

public with sharing class SharingClass {

}

ここで注意しなくてはならないのは、with sharingキーワードを付与しても、オブジェクトのCRUD権限、項目のアクセス権限は、システムモードのままということです。ユーザがアクセスできないはずのオブジェクトや項目にアクセスできてしまいます。

オブジェクトのCRUD権限、項目のアクセス権限は、Apexでは制御できないので、開発者が、ユーザから隠されているデータを公開しないように注意してApexコードを書く必要があります。

逆に、ユーザに適用されている共有ルールを強制実行されないようにするには、without sharingキーワードを付与します。

public without sharing class NoSharingClass {

}

with sharingキーワードもwithout sharingキーワードも付与しない場合は、呼び出し元のApexクラスの設定が有効となります。
例えば、with sharingキーワードが付与されたApexクラスから、キーワードなしのApexクラスを呼び出した場合、ユーザに適用されている共有ルールが強制実行されます。呼び出し位置が最上位レベルの場合(最初に呼ばれるApexクラスの場合)は、システムモードでの動作となります。

動作モードを表にまとめるとこうなります。

呼び出し位置 キーワード
なし without sharing with sharing
最上位レベル システムモード システムモード ユーザモード
最上位レベル以外 呼び出し元のモードと同じ システムモード ユーザモード

ちなみに、トリガはシステムモードの動作となりますが(トリガにはキーワードを付けることができない)、トリガ内部でwith sharingキーワード付きのApexクラスを呼び出すことで、ユーザモードでの動作となります。

また、匿名ブロックは、ユーザモードでの動作となります。

2010年11月 1日 (月)

Apexのトランザクション制御

前回、ApexのDMLステートメントと失敗時の挙動の説明で、ロールバックの話が出たので、今回はApexのトランザクション制御の特性を紹介します。

トランザクションの開始

Apexスクリプトの開始と同時にトランザクションが開始されます明示的にトランザクションを開始することはできないので、注意が必要です。

コミット

Apexスクリプトが正常終了すると、コミットされます。これも明示的にコミットすることはできないので、注意が必要です。

セーブポイント

Database.setSavePoint()をコールして、セーブポイントを設定できます。

ロールバック

Apexスクリプトが異常終了すると、ロールバックされます
明示的にロールバックするには、Database.rollback(savepoint)をコールします(セーブポイントへのロールバックしかできません)。

以上のように、明示的に制御できるのは、セーブポイントの設定とロールバックだけで、他はApexが正常終了したか異常終了したかによって、自動的に行われます。きめ細かく制御できないので、この特性を理解した上でプログラムを組む必要があります。

最後に、トランザクション中での行ロックの取得方法と、Force.comプラットフォームでのトランザクションの分離レベルについて、紹介しておきます。

行ロックの取得

Apexコード中で実行するSOQLクエリに「For Update」キーワードを付けて、行ロックを取得することができます。行ロックはトランザクションがコミットまたはロールバックされるまで持続されます

例) Select Id From Account For Update

トランザクションの分離レベル

Force.comプラットフォームのトランザクションの分離レベルは、コミット済み読み取り(READ COMMITTED)で、変更することはできません。

更新したがまだコミットされていないデータは、他のトランザクションからは読み取られません。あるトランザクションが更新中のレコードを他のトランザクションが読み取ろうとしたときは、更新中トランザクションが終了するまで一定時間待ちます。一定時間を過ぎた場合は待っている方のトランザクションが失敗します。

※トランザクションの分離レベルについては、ここを参照のこと

採用情報

株式会社フレクトでは、事業拡大のため、
・Salesforce/Force.comのアプリケーション開発
・HerokuやAWSなどのクラウドプラットフォーム上
でのWebアプリケーション開発
エンジニア、マネージャーを募集中です。

未経験でも、これからクラウドをやってみたい方、
是非ご応募下さい。

フレクト採用ページへ

会社紹介

  • 株式会社フレクトは、
      ・認定コンサルタント
      ・認定上級デベロッパー
      ・認定デベロッパー
    が在籍している、セールスフォースパートナーです。
    また、heroku partnersにも登録されています。
    herokuパートナー
    株式会社フレクト
    Salesforce/Force.com
    導入支援サービス
    弊社の認定プロフェッショナルが支援致します。
    ・Visualforce/Apexによるアプリ開発
    ・Salesforceと連携するWebアプリ開発
    も承っております。
    セールスフォースご検討の際は、
    お気軽にお問合せください。

Twitter

リファレンス

■Developer's Guide(リファレンス)
・Apex  HTML | PDF | 日本語PDF | ガバナ制限
・Visualforce  HTML | PDF
・Web Services API  HTML | PDF | 日本語PDF
・Bulk API  HTML | PDF
・REST API  HTML | PDF | 日本語PDF
・Metadata API  HTML | PDF
・Migration Tool  HTML | PDF
・AJAX Toolkit  HTML | PDF
・Data Loader PDF | 日本語PDF

■早見表 (日本語)
数式
Apex
Visualforce
Web Services API
Chatter