プログラマ38の日記

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

Salesforce: Communityの共有セットでのデータ共有で気をつけること

Communityで、Customer Communityというライセンスの時に気をつけることのメモです。(Customer Community Plus、Partner Communityは関係ありません)

 

  1. 共有ルールは使えず、共有セットを使う
  2. 1に伴い、Sharingのオブジェクトは使えない
  3. 1に伴い、共有セットの制限を越えたデータを見せるには、without sharing指定をしたVisualforce画面を作成する
  4. 1に伴い、共有セットの制限を越えた添付ファイルを見せるには、without sharing指定をしたVisualforce画面にbase64形式で出力する
  5. 所有者がCommunityユーザの場合、内部の共有ルールは適用されない

1. 共有ルールは使えず、共有セットを使う

SalesforceユーザとCommunityユーザのデータの共有は共有セットで行います。共有セットでは、ロール、公開グループ、ユーザを指定することができ、指定したロール、公開グループ、ユーザには、常にCommunityユーザのデータが見えます。(条件に応じて見せるということができません)

f:id:crmprogrammer38:20170821091018p:plain

2. 1に伴い、Sharingのオブジェクトは使えない

共有ルールが使えないので、Sharingのオブジェクトにレコードを作成することで共有させることができません。

3. 1に伴い、共有セットの制限を越えたデータを見せるには、without sharing指定をしたVisualforce画面を作成する

共有セットでは、最大で、ユーザから辿れる取引先と参照するレコードから辿れる取引先とが一致するレコードしか参照できません。

f:id:crmprogrammer38:20170821091621p:plain
上記を超えた範囲のデータを見せる業務の場合は、Visualforceで開発することになります。(例えば、基本はそれぞれの代理店内のデータを見せるように共有セットを指定しているが、代理店を統括する代理店には、統括下のデータを見せるなどが考えられます)

あえて、without sharingのキーワードを入れましたが、classに何も指定しなければwithout sharingがデフォルトです。そして、Visualforce画面ではデータのセキュリティを個別に実装する必要になります。仮にURLパラメータでIDを直打ちされたとしても、参照できるデータのみ見せるような制御が必要です。

4. 1に伴い、共有セットの制限を越えた添付ファイルを見せるには、without sharing指定をしたVisualforce画面にbase64形式で出力する

通常の"/servlet/servlet.FileDownload?file=AttachmentのID"を使うと共有セットの範囲を超えた添付ファイルでは参照権限のエラーとなります。

そこでコントローラ側で工夫が必要になります。例えば画像ファイルを見せたい場合は、コントローラ側で添付ファイルをbase64文字列に変換して、img タグに直接文字列を指定します
「<img src="data:image/png:base64,xxxxx..." />」
バイナリファイルをダウンロードさせたい場合は、コントローラ側で添付ファイルをbase64に変換した値をVisualforceに書き出しておき、JavaScriptのFileWriterなどで保存させるような作りにします。(これ以外のやり方だとコピーして参照可能なレコードを作成することになりますが、このコピーしたデータを定期的に削除するような仕組みが必要です)こちらも3と同様に仮にURLパラメータでIDを直打ちされたとしても、参照できるデータのみ見せるような制御が必要です。

5. 所有者がCommunityユーザの場合、内部の共有ルールは適用されない

Communityのデータと、内部の共有ルールを組み合わせて運用したい場合があります。例えば、Communityのデータで指定された選択リストの値に応じて参照できる部門を変えたい場合などがあると思います。

この場合は、所有者がCommunityユーザのままでは、共有ルールの仕組みが動かないため、データ作成時に所有者を変更することで解決します。
気をつける点として、所有者はデータ作成時に変更されてしまうため共有セットでの指定で所有者が使えません。ですので、別に取引先または取引先責任者の別項目を設けて共有セットはその別項目で指定します。そして、データ作成時に、その別項目にユーザの取引先や取引先責任者の情報を保持する処理が必要です。

Java: interfaceやabstract classが理解できた時の思い出

最初のプログラム言語はJavaでした。インスタンスやクラス、インターフェースや抽象クラス、キャストなど色々な横文字が大量に出てきて何が何やらわからず、苦労した思い出があります。

 

ある時なんとなくJavaが理解できた時の思い出です。

 

インスタンスとは

クラスをnew するとインスタンス(実体)になります。インスタンスは常にどのクラスのインスタンスなのかを持っています。例としてBigDecimalを使います。

f:id:crmprogrammer38:20170818092626p:plain

コードで書くと次になります。

    new BigDecimal(100);
    new BigDecimal(200);

 変数とは

Javaでは、継承:extends と実装: implements の仕組みがあります。
その仕組みの中で"インスタンスの参照"を格納できる変数の型に制限があります。
その制限は、インスタンスのクラスの定義で、自身のクラス以上が継承したクラスや、自身のクラス以上が実装したインタフェースのみインスタンスの参照を保持できるというものです。

BigDecimalが継承したクラスと実装したインタフェースは次の通りで、次のクラスとインタフェースがBigDecimalインスタンスを格納できます。
f:id:crmprogrammer38:20170818093725p:plain

クラスから作成したインスタンスを格納できる変数の関係は次の通りです。

f:id:crmprogrammer38:20170818094647p:plain
コードにすると次になります。

    BigDecimal decimal100 = new BigDecimal(100);
    Object obj100 = decimal100;
    Number num100 = decimal100;
    Serializable sel100 = decimal100;
    Comparable com100 = decimal100;
    
    BigDecimal decimal200 = new BigDecimal(200);
    Object obj200 = decimal200;
    Number num200 = decimal200;
    Serializable sel200 = decimal200;
    Comparable com200 = decimal200;

サンプルとしてNumber型の変数num100を例にするとnum100に格納されているのは、BigDecimalクラスのインスタンスなので、キャストをすればBigDecimal型に格納し直すこともできます。

    BigDecimal decimal100_c = (BigDecimal)num100;

 

抽象クラス

いまいちインターフェースとの使い分けがわからなかったのですが、抽象クラスは実装メソッドが持てて、そのメソッドの中で抽象メソッドも使えるというのが理解できてなんとなく使い方がわかりました。


public abstract class AbstractSample {
  
  protected void start(){
  }
  
//抽象メソッド
  abstract protected void execute();
  
  protected void end(){    
  }
  
//メソッド内で、抽象メソッドを使っておく。
  public void process(){
    start();
    execute();
    end();
  }
}

共通の部分はabstarct クラス側で実装しておいて、個別の処理はextendしたクラス側で実装を強制させることができます。似たような処理をクラスを分けて実現する場合、きれいに書けると思います。(現実は、1つのメソッドの中で分岐で実現することがほとんどだと思いますが。。)

 

終わりに

Webのフレームワークや、Javaのクライアント側のSwingなどは、抽象クラス、インタフェースを書いて覚えるにはちょうど良いと思います。
あまりやりすぎた継承や実装は好きではないですが、ちょっと工夫することで効率が大きく変わるのでオブジェクト指向は楽しいと思いました。

Salesforce: 重複ルールで作成・編集を許可している時の、標準画面以外からのデータの登録について

Salesforceで、重複ルールという機能があります。(前はなかったのですが、とても欲しかった機能でした。実装されてちょっと嬉しかったです)

 

重複ルールの設定次第では、重複レコードが他にあった際に警告を出しつつそのまま保存ができます。

この設定にした際の、API実装や、データローダやApexで気をつけないといけない点になります。

 

API実装で、重複レコードの場合に保存する方法

SOAPHeaderに、DuplicateRuleHeaderを指定する。DuplicateRuleHeaderではallowSaveをtrueにする。

  PartnerConnection pcon = Connector.newConnection(connectorConfig);
  pcon.setDuplicateRuleHeader(true, false, true);

 この指定をすることで、エラーとならずに保存ができます。
ETLツールでもSOAPHeaderの指定が可能であればこの設定ができると思います。
指定しない場合allowSaveの初期値はfalseなので、常にエラーとなります。

 

そして、APIには、事前に重複ルールに該当するか調べるメソッドPartnerConnection.findDuplicatesが用意されています。PartnerConnection.findDuplicatesでは、データ登録前に重複データあるか、ある場合の重複する他のデータを取得することができます。

  SObject sobj = new SObject();
  sobj.setType("Sample__c");
  sobj.setField("KeyField__c", "Value01");
  
  FindDuplicatesResult[] result = pcon.findDuplicates(new SObject[]{sobj});

 

データローダでは、重複レコードの場合は無条件で登録できない

データローダでは、DuplicateRuleHeaderが指定できない(APIVer40時点)ため、重複ルールで該当すると無条件でエラーとなります。

データローダではエラーではなく登録させたい場合は、重複ルールの条件に工夫が必要となります。

例えば、データローダを利用するユーザのプロファイルやロールを重複ルールの条件に追加することで、そのユーザでは登録ができるようにする対応を行います。

ApexではDMLOptionで指定する

Apexで重複ルールに該当しても、そのまま登録するにはDMLOptonの指定を行います。

Database.DMLOptions dml = new Database.DMLOptions();
dml.DuplicateRuleHeader.allowSave = true;
dml.DuplicateRuleHeader.runAsCurrentUser = true;

Account duplicateAccount = new Account(Name='duplicate');

Database.SaveResult sr = Database.insert(duplicateAccount, dml);

 

最後に

重複ルールを使う場合、標準画面のチェックが便利になった分、それ以外の考慮が必要になります。データ連携があるシステムでは、データ連携への影響を検討が必要ですし、そのオブジェクトを利用するVisualforceページを作成する場合は、そのVisualforceページの動きの確認が必要です。

Excel: セルの表をtableタグで出力するマクロをバージョンアップしました

以前、Excelの表をhtmlのtableタグに変換するマクロを作成しました。

 

crmprogrammer38.hatenablog.com

 

使っていくうちにやっぱりこうしたほうがいいかなとか考えていき、次のように改良しました。

  1. マクロの中でセルの値をValueで取得していた箇所をTextで取得するように修正
  2. 文字列の位置(上揃え、下揃え、中央揃え、右揃え)をタグに反映する

 

1は日付のフォーマットや数値の区切りはそのままhtmlになってくれた方が便利だなと思い対応しました。

2は文字の揃えをvalignやalignに反映されたほうが楽だなと思い対応しました。(あくまでExcel上で指定した設定が反映されます。例えば数値は指定しなくても表示上は右揃えですが、Excel上で設定していなければalignには反映されません)

 

ダウンロードはこちらからになります。

 

最後に

同じようなマクロやツールは、他に探せば他に色々なあったのですが、結局自分で使いやすいようにいじくれるというのが便利で、結局これを使ってます。
もっと、使いやすいツールがあると思うのですが、自分で創意工夫ができる余白がある方が楽しいんですよね。

Salesforce: APIの「renderEmailTemplate」「renderStoredEmailTemplate」を使ってみた

前回SalesforceAPIについて書きました。使ったことのないメソッドが増えているなーと思い、まず「renderEmailTemplate」「renderStoredEmailTemplate」を使ってみました。

「renderEmailTemplate」

	String endpoint = "https://login.salesforce.com/services/Soap/u/40.0";
	ConnectorConfig pconfig = new ConnectorConfig();
	pconfig.setAuthEndpoint(endpoint);
	pconfig.setServiceEndpoint(endpoint);
	pconfig.setUsername("sampleuser@sample.user");
	pconfig.setPassword("samplepassword");
	PartnerConnection pConnection = new PartnerConnection(pconfig);

	RenderEmailTemplateRequest req = new RenderEmailTemplateRequest();
	//ここでテンプレート文字列を指定する。
	req.setTemplateBodies(new String[] {"aaaaa {!Sample__c.Id} bbbb {!Receiving_User.Name}"});
	//レコードのID
	req.setWhatId("XXXXXXXXXXXXXXX");
	//受信ユーザ項目に指定するユーザID
	req.setWhoId("005XXXXXXXXXXXX");
	
	RenderEmailTemplateResult[] result = pConnection.renderEmailTemplate(new RenderEmailTemplateRequest[]{req});
	
	System.out.println(result[0].getBodyResults()[0].getMergedBody());

RenderEmailTemplateRequest.setTemplateBodiesは引数は文字列の配列で、件名や本文をまとめて指定できるようになっています。

「renderStoredEmailTemplate」

	String endpoint = "https://login.salesforce.com/services/Soap/u/40.0";
	ConnectorConfig pconfig = new ConnectorConfig();
	pconfig.setAuthEndpoint(endpoint);
	pconfig.setServiceEndpoint(endpoint);
	pconfig.setUsername("sampleuser@sample.user");
	pconfig.setPassword("samplepassword");
	PartnerConnection pConnection = new PartnerConnection(pconfig);

	RenderStoredEmailTemplateRequest req = new RenderStoredEmailTemplateRequest();
	//ここでメールテンプレートのIDを指定する。
	//メールテンプレート(EmailTemplate)オブジェクトからIDを取得する処理が必要	
	req.setTemplateId("00X0H000001V8keUAC");
	//レコードのID
	req.setWhatId("XXXXXXXXXXXXXXX");
	//受信ユーザ項目に指定するユーザID
	req.setWhoId("005XXXXXXXXXXXX");
	//この指定は必須
	req.setAttachmentRetrievalOption(AttachmentRetrievalOption.MetadataOnly); 
	
	RenderStoredEmailTemplateResult result = pConnection.renderStoredEmailTemplate(req);
	System.out.println( result.getRenderedEmail().getPlainTextBody() );
	System.out.println( result.getRenderedEmail().getSubject() );

メールテンプレート(EmailTemplate)をSOQLで検索し該当のテンプレートのIDを取得する処理が必要になります。(上の処理では書いていませんが。。)

終わりに

APIだけでなく、ApexClassでも同様の処理が用意されていて、「Messaging.renderEmailTemplate」「Messaging.renderStoredEmailTemplate」が対応します。

メールで送る文字列そのものが必要な時にはとても重宝すると思います。
ちなみに、メールテンプレートは、カスタム表示ラベルを使うことができますが、いまだに差し込み項目の種類で選べないですね。。

Salesforce: APIで用意されている機能について

SalesforceにはいくつかのAPIが用意されています。こういうことをやりたいときにこれを使えばいいというのがわかるようにまとめてみました。Rest API、Bulk APIは外していて、SOAP APIに限定しました。(APIVer40の時に書いています)

 

メソッド 処理内容 どのAPIの機能か
Enterprise/Partner
Metadata
Apex
Tooling
convertLead リードを取引先にする。      
changeOwnPassword 自身のパスワードを変更する。      
create レコードを作成する。      
delete レコードを削除する。      
describeGlobal 主に組織内のSobject情報をリストで取得する。      
describeSObject   Sobject情報を取得する。      
describeXXX その他の情報XXXを取得できる。      
emptyRecycleBin   ゴミ箱を空にする。      
executeListView   リストビューを実行し、結果を取得する。      
findDuplicates 登録する前に重複ルールに該当結果を取得する。      
performQuickActions   アクションを実行する。      
login ログインする。(セッションIDと、インスタンスURLを取得する)    
logout ログアウトする。(ログイン時のセッションIDを終了する。)    
merge    レコードをマージする。(リード、取引先、取引先責任者に限定された機能)      
process   承認プロセスの操作をする。      
query    SOQLを実行し、レコードを取得する。      
queryAll   SOQLを実行し、削除済みレコードを含めてレコードを取得する。      
queryMore    query/queryAllの次の結果を取得する。(検索結果の件数が多い場合、何度かにわけて結果を取得する)      
renderEmailTemplate   メールテンプレート文字列とレコードの値をマージする。      
renderStoredEmailTemplate    定義したメールテンプレートとレコードの値をマージする。      
resetPassword    指定したユーザのパスワードをリセットし、リセット後のパスワードを取得する。(通知メールを送るわけではない)      
setPassword    指定したユーザに指定したパスワードをセットする(ログイン後、パスワードの変更画面が表示される。)      
retrieve   SalesforceIdを指定してレコードを取得する。      
search グローバル検索のようにレコードを検索し、結果を取得する。      
sendEmail    指定した宛先、件名、本文、添付ファイルでメールを送信する。      
sendEmailMessage   Sobject「メールメッセージ(EmailMessage)」のドラフトメールを送信する。      
undelete   ゴミ箱から復元する。      
update レコードを更新する。      
upsert レコードをアップサートする。      
getServerTimestamp Salesforceからサーバ時刻を取得する。    
getUpdated 指定されたオブジェクトに対して指定された期間内に更新されたレコードを取得する。      
getDeleted 指定されたオブジェクトに対して指定された期間内に削除されたレコードを取得する。      
getUserInfo ログインしたユーザ情報を取得する。    
invalidateSessions 指定されたセッションIDを終了する。    
deploy メタデータをデプロイする。      
checkDeployStatus デプロイの状況を取得する。(deployは非同期のため定期的に状況をチェックする。)      
cancelDeploy デプロイをキャンセルする。      
deployRecentValidation クイックリリースする。      
retrieve メタデータを取得する。      
checkRetrieveStatus メタデータの取得状況を取得する。(メタデータの取得は非同期のため定期的に状況をチェックする。)      
describeMetadata メタデータタイプの一覧を取得する。      
listMetadata 指定したメタデータタイプのコンポーネント一覧を取得する。      
createMetadata メタデータコンポーネントを作成する。      
upsertMetadata メタデータコンポーネントをアップサートする。      
updateMetadata メタデータコンポーネントを更新する。      
deleteMetadata メタデータコンポーネントを削除する。      
readMetadata メタデータコンポーネントを取得する。      
renameMetadata メタデータコンポーネントをリネームする。      
compileClasses クラスを作成する。(開発環境用)      
compileTriggers トリガを作成する。(開発環境用)      
compileAndTest クラスを作成しテストする。(本番環境用)      
runTests テストを実行する。    
executeAnonymous AnonymousApexを実行する。    
create Tooling API objectsを作成する。      
update Tooling API objectsを更新する。      
upsert Tooling API objectsをアップサートする。      
delete Tooling API objectsを削除する。      
search グローバル検索のようにTooling API objectsを検索し、取得する。      
query SOQLのようにTooling Api Objectsを検索し、取得する。      
queryAll 削除されたTooling API objectsを含めてSOQLのようにTooling API objectsを検索し、取得する。      
queryMore query/queryAllの次の結果を取得する。(検索結果の件数が多い場合、何度かにわけて結果を取得する)      
describeGlobal Tooling API objectsの一覧を取得する。      
describeSObject 指定したTooling API objectsの詳細を取得する。      
describeXXX その他のTooling API objectsの情報を取得する。      
retrieve Idを指定してTooling API objectsを取得する。      
getUpdated 指定された期間内に更新されたTooling API objectsを取得する。      
getDeleted 指定された期間内に削除されたTooling API objectsを取得する。      
runTestsAsynchronous 非同期でテストを実行する。      

 ※APIメソッドはすべてではありません。

 

まとめていく中で、Tooling APIの使い道がピンときませんでした。開発者コンソールの一部では使っていると思いますが、自分の中ではこういう所で使えそうというイメージが持てませんでした。

Enterprise/Partner APIと同じ名前のメソッドが、Tooling APIで定義されていますが、取得するのがTooling API Objectsというメタデータ寄りの定義情報になります。

メタデータの作成や取得をするならMetadata APIの方を使うことになりますし、Tooling APIだけが持っているメソッドはほぼありません。

Tooling API Objectsがいまいち理解できていないのだと思いますが、今は使う場面がありません。

 

あと、APIを使う上での注意事項は次になります。

  • 基本logoutは使いません。データ連携などで、同じユーザが同じタイミングで複数の処理を動作させている可能性があります。1つの処理でlogoutすると、同時に動いているほかの処理でセッションIDが無効となりエラーとなってしまいます。
  • APIのコール数の上限や、送信メール数の制限を超えないようにします。 
  • sendEmailでメールを送信する時は、エラーとなる宛先があるなどSalesforceに負荷がかかると処理が中断される場合があります。
  • レコードの作成、更新などでもSalesforceに負荷がかかると処理が中断される場合があります。

 

SQL: Window関数を理解できた時の思い出2

前の続きです。

crmprogrammer38.hatenablog.com

 

Window関数では、Windowの中で並び替えをした順番に各行で計算する仕組みがあります。累計の計算を例にします。(BIツールで、ランニングサムの名称がついていたりします)

以下の年別累計の項目は、年毎のWindowの中で年月の古い順に出費を足した値となります。

年月 出費 年別累計
2016 201611 27,000 27,000
2016 201612 24,000 51,000
2017 201701 18,000 18,000
2017 201702 3,000 21,000
2017 201703 15,000 36,000
2017 201704 27,000 63,000

これを出力するSQLは次になります。(rows betweenを省略しないで書いています)

select
  年 
 ,年月 
 ,出費 
 ,sum(出費) over(partition by 年 order by 年月 
                 rows between unbounded preceding and current row ) as 年別累計
from データ;

このSQLでrows between~の記載がありますが、ここで行を計算する際に、Windowのどこからどこまでを計算対象とするかの指定をしています。

英語の通りなのですが、Window内の、先頭行(unbounded preceding)から現在行(current row)までをsumするという指定になります。

※補足ですが、sumやcountなどのWindow関数でorder byを指定した場合は、"rows between unbounded preceding and current row"がデフォルトなので省略ができます。

 

rows betweenで次を指定できます。

unbounded preceding 先頭行
N preceding  N行前の行
current row 現在行
N following N行後の行
unbounded following 最終行

 

上の指定を組み合わせると以下のような結果がとれます。

  • 1行前の出費は、1段下がったように値が表示されます。
  • 1行後の出費は、1段上がったように値が表示されます。
  • 現在の行数は、年単位に行毎に数が増えています(row_numberと同じ結果です)
  • 残り行数は、そのWindowで現在行を含めずに何行残っているが表示されます。
年月 出費 1行前の出費 1行後の出費 現在の行数 残り行数
2016 201611 27,000   24000 1 1
2016 201612 24,000 27,000   2 0
2017 201701 18,000   3000 1 3
2017 201702 3,000 18,000 15000 2 2
2017 201703 15,000 3,000 27000 3 1
2017 201704 27,000 15,000   4 0

 これを出力するSQLは次になります。

select
  年 
 ,年月 
 ,出費 
 ,sum(出費) over(partition by 年 order by 年月 
                 rows between 1 preceding and 1 preceding ) as "1行前の出費"
 ,sum(出費) over(partition by 年 order by 年月 
                 rows between 1 following and 1 following ) as "1行後の出費"
 ,count(*)  over(partition by 年 order by 年月 
                 rows between unbounded preceding and current row) as "現在の行数"
 ,count(*)  over(partition by 年 order by 年月 
                 rows between 1 following and unbounded following ) as "残り行数"
from データ;

rows between で範囲をどこからどこまでにするかで欲しい値が取得できます。

 

最後に

rows between以外にもrange betweenというのもありますが触れないでおこうと思います。

 

Window関数があると便利だなと思っているのは次です。

キー 開始日 その他属性・・
KEY001 2016-01-01 ・・
KEY001 2016-08-15 ・・
KEY001 2017-03-01 ・・
KEY002 2016-04-10 ・・
KEY002 2017-02-01 ・・

 上記は、いわゆる履歴管理されているマスタですが、開始日だけ持っています。

結合条件で使うため終了日を計算する場合があります。終了日は同じキーの次の開始日の1日前、最終行の場合は固定で2100-12-31とします。

その時にWindow関数を使うと次のようなデータが作れます。

キー 開始日 終了日 その他属性・・
KEY001 2016-01-01 2016-08-14 ・・
KEY001 2016-08-15 2017-02-28 ・・
KEY001 2017-03-01 2100-12-31 ・・
KEY002 2016-04-10 2017-01-31 ・・
KEY002 2017-02-01 2100-12-31 ・・

この結果となるSQLは次の通りです。(Oracle用のため日付の計算はDB毎で変更が必要です。)

Select
   キー
  ,開始日
  ,coalesce( 
     max( 開始日 ) over( partition by キー order by 開始日 
                         rows between 1 following and 1 following ) -1  
  , to_date('21001231','YYYYMMDD') ) as 終了日
  ,その他属性
From
  履歴を持つマスタ

DWHなどを作っていると使う時も多いかなと思います。