« 2014年7月 | メイン | 2014年9月 »

2014年8月に作成された記事

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です。
アクセス制御に関するテストはどうなるんだろ?とか追加の調査の余地はありますが、とりあえずは回避策があるので良しとしておきます。

それでは。

採用情報

株式会社フレクトでは、事業拡大のため、
・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