プログラマ38の日記

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

Salesforce/SQL: 検索条件でのnullの扱いの違いについて

Salesforceではデータの取得でSOQLを使います。
一般的なRDBSQLと似ていますが、結合処理や、検索の指定がSQLと異なります。

検索条件でのnullの指定と検索結果について、SOQLとSQLの違いがあるのでメモです。

 

次のデータをサンプルとしてSOQLとSQLで検索結果の違いを見ていこうと思います。

# 項目 備考
1 valueA  
2 valueB  
3   null値です。
4 valueC  

 

nullの条件の指定 

SOQLのnullの条件は、 「項目 = null」 または 「項目 = ''」 です。
Salesforceには空文字でデータ登録はできないのでSOQLでは、nullと空文字で区別はありません。
※Apexではnullと空文字は異なります。
「項目 = null」の条件に対して結果は次です。

# 項目 備考
3   null値です。

 

SQLのnullの条件は「項目 is null」です。この条件での結果は次です。

# 項目 備考
3   null値です。

 

in句でのnullの条件の指定

SOQLで、in句でnullを指定可能です。 逆にin内でnullが入っていると検索結果に含まれてしまいます。
「項目 in ('valueA',null,'ValueC')」の条件に対して結果は次です。

# 項目 備考
1 valueA  
3   null値です。
4 valueC  

 

 SQLで、in句にnullを指定しても結果には含まれません。
「項目 in ('valueA',null,'ValueC')」の条件に対して結果は次です。

# 項目 備考
1 valueA  
4 valueC  

 

XX以外の条件でnullの扱い

SOQLでは、例えば 「項目 != 'valueA'」のように、XX以外を指定した場合、
指定された項目がnullの値は検索結果に含まれます。
「項目 != 'valueA'」の条件に対して結果は次です。

# 項目 備考
2 valueB  
3   null値です。
4 valueC  

 

 SOQLでは、例えば 「項目 != 'valueA'」のように、XX以外を指定した場合、
指定された項目がnullの値は検索結果に含まれません。
「項目 != 'valueA'」の条件に対して結果は次です。

# 項目 備考
2 valueB  
4 valueC  

 

 最後に

プログラムで、nullの結果が含まれる、含まれないで動きがだいぶ違うため
扱いに注意が必要です。私はSQLに慣れ過ぎていて、Salesforceの動きで大分戸惑いましたが。。

Salesforce: 最終更新日(LastModifiedDate)とSystem Modstamp(SystemModstamp)の違い

最終更新日(LastModifiedDate)とSystem Modstamp(SystemModstamp)は、今まで特に意識もせず、同じタイムスタンプが入っているなーぐらいの認識でした。

 

ですが、「レコードの作成時に監査項目を設定」を有効化して、最終更新日(LastModifiedDate)に直接タイムスタンプを指定した時に違いがでるということを知りました。

 

例えば、監査項目を設定を有効にして次のデータを登録します。

NAME CREATEDDATE LASTMODIFIEDDATE
sample01 2017-07-27T00:44:38.000Z 2017-07-27T00:44:38.000Z
sample02 2017-07-27T00:23:48.000Z 2017-07-27T00:23:48.000Z
sample03 2017-07-27T00:31:56.000Z 2017-07-27T23:38:28.000Z

 すると登録後は、System Modstamp(SystemModstamp)は、登録時刻がセットされます。

NAME CREATEDDATE LASTMODIFIEDDATE SYSTEMMODSTAMP
sample01 2017-07-27T00:44:38.000Z 2017-07-27T00:44:38.000Z 2017-08-21T23:36:34.000Z
sample02 2017-07-27T00:23:48.000Z 2017-07-27T00:23:48.000Z 2017-08-21T23:36:34.000Z
sample03 2017-07-27T00:31:56.000Z 2017-07-27T23:38:28.000Z 2017-08-21T23:36:34.000Z

 

最終更新日(LastModifiedDate)で判定すると、直接、最終更新日(LastModifiedDate)に値を指定されると対象から漏れてしまうかもしれません。
Apexプログラムや、APIの中で、この項目の値の違いを理解して設計しておいた方がいいのだと思います。

 

最後に

監査項目に値を指定するために、以前はSalesforceの内部設定を変更してもらう必要があったのですが、Winter16から画面の設定でできるように変更になっていたんですね。やり方は次の通りでした。

設定>ビルド>ユーザインタフェース
「レコードの作成時に監査項目を設定」および「無効な所有者のレコードを更新」ユーザ権限を有効化 にチェック

f:id:crmprogrammer38:20170822090544p:plain
プロファイルで「レコードの作成時に監査項目を設定」をチェック

f:id:crmprogrammer38:20170822090758p:plain
さらに、プロファイルで「無効な所有者のレコードを更新」をチェックすると所有者に無効ユーザが指定できます

f:id:crmprogrammer38:20170822091032p:plain



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」が対応します。

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