Tooling API を使ってログを設定する

Salesforce には Tooling API という非常に強力な API があります。
どのようなことができるかというと...

  • Apex クラスおよびトリガと Visualforce ページおよびコンポーネントの作業コピーを管理する。
  • 静的リソースファイルの作業コピーを管理する。
  • Apex クラスおよびトリガと Visualforce ページおよびコンポーネントの作業コピーに更新やエラーがないかチェックし、変更を組織にコミットする。
  • ヒープダンプマーカーを設定する。
  • Apex 実行時に Apex コードまたは SOQL ステートメントをフロート表示する。
  • Apex を匿名実行する。
  • チェックポイントを設定して自分または他のユーザ用にログファイルを生成する。
  • デバッグログおよびヒープダンプファイルにアクセスする。
  • カスタムオブジェクトのカスタム項目を管理する。
  • コードカバー率の結果にアクセスする。

https://developer.salesforce.com/docs/atlas.ja-jp.salesforce1api.meta/salesforce1api/apis_tooling_introducing.htm より

動的に生成されたApexクラスを追加するなど、そんなことが出来ていいのかというようなこともできてしまうようです。

この中で最近使用したのが、デバッグログへのアクセスです。
私の知る限りログ設定の有効期限は24時間が最大のため、24時間を超えてログを出力させたい場合は都度設定しなければなりません。
しかも、デバッグログはユーザごとに設定が必要なため、なにかの現象の再現待ちを行うなどで全ユーザにログを設定したいとなった場合には非常に手間がかかります。

そこで、以下のようなコードでデバッグログの設定を行いました。

Map<String, String> record = new Map<String, String>();
record.put('TracedEntityId', <ユーザのID>);
record.put('LogType', 'USER_DEBUG');
record.put('DebugLevelId', <デバッグレベルのID>);
record.put('ExpirationDate', DateTime.now().addDays(1).addHours(-9).format('yyyy-MM-dd\'T\'HH:mm:ss'));

Http h = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');
req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v43.0/tooling/sobjects/TraceFlag/');
req.setMethod('POST');
req.setBody(JSON.serialize(record));
HttpResponse resp = h.send(req);


API を使用するときは認証をどうするかという問題がありますが、開発者コンソールの匿名ブロックで実行すれば現在のユーザのセッションIDが使用できます。
使用するオブジェクトは TraceLog です。デバッグログの設定はこのオブジェクトのレコードとして格納されています。

ExpirationDate で -9h しているのは、UTC で指定する必要があるためです。

なお実際のコードはもう少し複雑で、実際のコードでは、ユーザの一覧を取得してすでにそのユーザのデバッグログ設定があれば、ExpirationDate を更新するということも行っています。




Salesforce のデバッグログ

Salesforce の開発にあたっては、デバッグログ というログ機能が備わっておりこれがデバッグの助けになってくれます。
このデバッグログについて、最近、知らなかったことや勘違いしていたこと、新たに学んだことがあったので備忘録として残します。 正確な情報はでないかもしれませんので、参照される方はご注意ください。

デバッグログを見る

デバッグログは、いくつかの場所で見ることができます。

  • 設定 - 環境 - ログ - デバッグログ
  • 開発者コンソール - Logs
  • Tooling API


設定 - 環境 - ログ - デバッグログ

設定画面から見ることができます。ログはブラウザ上で見ることも、個々のログをファイルとしてダウンロードすることもできます。
不要になったログを削除することもできます。
一度に表示される件数が決まっていてそれほど多くないということと、フィルタ機能がないため、ログが多いときは目的のログにたどり着くのが難しいことがあります。


開発者コンソール - Logs

開発者コンソールを開いて、下の方にある Logs タブでログを見ることができます。個々のログをファイルとしてダウンロードすることもできます。
デフォルトでは自分以外のログしか見れませんが、メニューの Debug - Show My Current Logs Only のチェックを外すと他のユーザのログも見れるようになります。

開発者コンソールで見る場合は、Logs タブの下部に Filter というチェックボックスとテキストボックスがあり、これを使用すると特定のユーザのログのみ表示する、などのフィルタを行うことができます。
さらに、ログを開いて下部の Debug Only にチェックを入れると System.debug で出したログだけを見ることもできます。

なお、Logs タブにはリアルタイムでログのエントリが追加されていくためか、ログが多いと開発者コンソール自体が非常に重くなっていきますので、大量にログがある場合は注意が必要です。


Tooling API

API を使用してログを取得することができます。
ただ API を使って「見る」ことはあまり使わないかと思いますので、詳細は割愛します。


デバッグログを設定する

デバッグログを出力するためには、出力するように設定を行わなければいけません。設定も、上述の3箇所で行うことができます。

基本的に、デバッグログは「どのユーザのログを出すか」「どのくらいの期間」「どのくらい詳細に出すか」の組み合わせ=追跡フラグを設定します。

最も一般的には、以下のようになるかと思います。

  • デバッグログ画面で新規ボタンをクリック
  • 追跡対象エンティティ種別 に ユーザ を指定
  • 追跡対象エンティティ名 でログを出力したいユーザを指定
  • 開始日と有効期限を指定 (最大24時間までしか指定できない)
  • デバッグレベルを指定 デフォルトの SFDC_DevConsole でもたいていは十分

ここで有効期限には最大24時間までしか指定できないため、24時間を超えてログを取得したい場合は、1日1回などこれを更新する必要があります。

デバッグレベルは「どのくらい詳細に出すか」の指定で、ワークフローの情報だけ欲しい だとか コールアウトの情報だけ欲しい といった場合には自分でそのあたりを調整することができます。

追跡対象エンティティ種別 では、ユーザ、Apexクラス、Apexトリガなどを選択することができます。
ここで、 追跡対象エンティティ種別にApexクラスを選択してログを取りたいクラスを選択したら、そのクラスだけのログが取れる と勘違いしそうになりますが、そうではありません。
公式のドキュメントにも、以下のように記述されています。

自分自身を含む特定ユーザ、クラス、およびトリガのデバッグログを保持および管理できます。クラスおよびトリガの追跡フラグを設定してもログの生成や保存は行われません。 クラスおよびトリガの追跡フラグによって他のログレベル (ユーザ追跡フラグによって設定されたログレベルなど) が上書きされますが、ログが記録されることはありません。クラスまたはトリガが実行されたときにログ記録が有効であれば、実行時にログが生成されます。

https://help.salesforce.com/articleView?id=code_add_users_debug_log.htm より

追跡対象エンティティ種別にApexクラス/トリガを選択した追跡フラグを追加しても、それだけではログは出力されませんので、注意が必要です。



Salesforce の自動更新系の順序

Salesforce では、自動化の一環として以下のようなものが使用できます。

  • フロー
  • ワークフロールール
  • プロセスビルダー
  • 項目自動更新
  • トリガ

これらは便利なものですが、何も考えずに使用するとDLLヘルよろしくの地獄を見るかもしれません。

これらの実行順序や特性を理解していないと、意図しない挙動になる恐れがあります。
以下は、公式による説明です。

下記が、レコードに適用される salesforce ロジックの順序です。

  • 古いレコードをデータベースからロード(または、新しい挿入の初期化)
  • 新しいレコードの値で古い値を上書き
  • システムの入力規則(商談商品を挿入する場合、システムの入力規則に加えてカスタム入力規則が実行されます)
  • すべての before トリガを実行(EE / UE のみ)
  • カスタム入力規則
  • レコードをデータベースに保存(しかし、コミットされていない)
  • レコードをデータベースから再ロード
  • すべての after トリガを実行(EE / UE のみ)
  • 割り当てルール
  • 自動応答ルール
  • ワークフロー ルール
  • プロセス
  • エスカレーション ルール
  • 積み上げ集計数式の値の更新(存在する場合)
  • データベースのコミット
  • コミット後のロジック(メールの送信)

自動化ルール、および、Apex トリガーはどのような順番で処理されますか? より



気をつけたいのは、更新系の組み合わせです。

例えばあるオブジェクトの項目を、ワークフロールールでもトリガでも更新するような場合、意図しない動きにならないように気をつけなければなりません。

ワークフロールールの項目自動更新が項目を変更する場合、もう一度トリガが動くという点も考慮する必要があります。

積み上げ集計項目の更新によって、親側のワークフロールールやトリガが動作するということも忘れてはいけません。


たいていのワークフロールールやトリガでは、「YYYがXXXだったら」というような条件をもとに動作させることが多いかと思います。

ワークフロールールやトリガが増えていくと、何か問題が起こっても再現させることすら困難なこともあるため、できる限りシンプルな状態に保ちたいものです。




Wordpress の API でカスタムフィールドにアクセスする

Wordpress を利用するにあたっては、何らかのプラグインを導入することが常ではないでしょうか。
プラグインでは、そのプラグインで利用する独自のデータをカスタムフィールドで持つことが多いかと思います。

あるいはまた、標準で用意した項目では足りず、自前で何らかのカスタムフィールドを利用することもあるかと思います。

そういった運用をしている Wordpress サイトに API でアクセスする場合、API でもそれらのカスタムフィールドにアクセスしたいことがあります。 しかし、デフォルトではそれらのカスタムフィールドにアクセスすることはできません。

カスタムフィールドにアクセスするためには、以下のいずれかの対応が必要になります。

register_rest_field する

例えばカスタムフィールドのキーが some_key だったとしたら、次のようになります。

// Wordpress のバージョンによっては違うかも
add_action('rest_api_init', function() {
    register_rest_field(
        'post',
        'some_key',
        array(
            'get_callback' => function($post, $field_name, $request) {
                return get_post_meta($post['id'], $field_name, true);
            },
            'update_callback' => function($value, $post, $field_name) {
                return update_post_meta($post->ID, $field_name, $value);
            },
            'schema' => null,
        )
    );
});
// なお get_callback の方の $post は連想配列で、update_callback の $post は WP_Post オブジェクト
// なぜそのようになっているのかは謎だが、注意が必要


これを function.php に入れておくことで、投稿データに some_key というキーでアクセスできるようになります。


register_meta する

でも実は、カスタムフィールドは投稿のメタデータとして格納されるので、次の register_meta だけでもアクセスできるようになります。

register_meta('post', 'some_key', ['show_in_rest'=>true, 'single'=>true]);


この場合は、投稿データの meta の子供の some_key でアクセスできるようになります。


MariaDB(MySQL) の不思議な挙動

DBMSMariaDB 5.5.52。
最近、不思議な挙動に遭遇しました。

「データベースに登録されている企業の一覧を表示し、その企業の大分類を表示する」という機能の実装を行いました。
この企業データは以下のようなものです。

company テーブル

id name f1 f2 f3 f4 f5
1 株式会社A 1 null null null null
2 株式会社B null 1 null null null
3 株式会社C 1 1 null 1 null


分類には大分類と中分類があり、company テーブルではその企業がどの中分類が該当するのかというフラグだけをその中分類ごとにカラムを分けて f1~f5 で持ち、設定されている場合は 1、未設定の場合は null を設定するという構造になっていました。

大分類と中分類のマスタは別テーブルにあります。その大分類/中分類の名称は、そのマスタでコードから名称へ翻訳する必要がありました。

code テーブル

code name
fa 大分類A
fb 大分類B
f1 中分類A1
f2 中分類A2
f3 中分類B1
f4 中分類B2
f5 中分類B3


ここで、中分類 f1~3 は大分類A、f4 と f5 は大分類B です。

ですので、画面上の表示としては、以下を表示したいということになります。
株式会社A → 大分類A
株式会社B → 大分類A
株式会社C → 大分類Aと大分類B


そこで、以下のようなSQL文を考えました。

SELECT co.*
  ,CONCAT_WS(', '
     ,(CASE WHEN COALESCE(co.f1, co.f2) IS NOT NULL THEN (SELECT name FROM code WHERE code='fa') ELSE NULL END)
     ,(CASE WHEN COALESCE(co.f3, co.f4, co.f5) IS NOT NULL THEN (SELECT name FROM code WHERE code='fb') ELSE NULL END)
   )
FROM company co


このSQL文では、以下を意図していました。

1.「カラム f1 または f2 が NULL でない」を COALESCE(co.f1, co.f2) IS NOT NULL で示す (大分類Bについても f3, f4, f5 を使って同様に)
2. 1が真(つまり f1 または f2 が NULL でない)なら SELECT name FROM code WHERE code='fa' を、そうでないなら NULL を取得する
3. その結果を CONCAT_WS でつなぐ (NULL は無視される)


これで正しく結果を取得できる想定だったのですが、どうにも結果が予期したものになりませんでした。 株式会社B は「大分類A」と取得したいのに、「大分類A, 大分類B」と取得されてしまうのです。

不思議なのは、

  • WHERE句を指定すると結果が正常になる場合がある
  • 取得するカラムに co.f3, co.f4, co.f5 を追加しても、idが2のレコードではそれぞれ null である
  • COALESCE(co.f3, co.f4, co.f5) だけ取得しても、idが2のレコードでは null である

ということでした。

CONCAT_WS、CASE、COALESCE を組み合わせることによって何かツボを突いてしまったしまったかのようでした。
文法エラーにはなりませんでしたが、CONCAT_WS にサブクエリを指定したのが良くなかったのでしょうか。


この件は、以下のようにSQL文を書き直すことで対応することができました。

SELECT co.*
  ,CONCAT_WS(', '
     ,(SELECT name FROM code WHERE code='fa' AND COALESCE(co.f1, co.f2) IS NOT NULL)
     ,(SELECT name FROM code WHERE code='fb' AND COALESCE(co.f3, co.f4, co.f5) IS NOT NULL)
   )
FROM company co





正規表現のおさらい (5)

正規表現のおさらいシリーズ
その1:http://developer.venture-net.co.jp/entry/2018/06/22/174206
その2:http://developer.venture-net.co.jp/entry/2018/06/29/192116
その3:http://developer.venture-net.co.jp/entry/2018/07/06/185719
その4:http://developer.venture-net.co.jp/entry/2018/07/13/185653

アンカー

例えばこんなファイルがあったとします。 f:id:venturenet:20180810183218p:plain
よくあるのが、「先頭がXXXで始まるパターンを抜き出したい」というケースです。

単に 127\.0\.0\.1 という正規表現では、途中にあらわれる 127.0.0.1 にもマッチしてしまいます。

こんなときに使うのが ^$ です。これはこれまでに登場したメタ文字と異なり、特定の文字にマッチするのではなく位置にマッチします。
それぞれ以下の通りです。

メタ文字 意味
^ テキストの先頭にマッチする
$ テキストの末尾にマッチする


^127\.0\.0\.1 と書けば先頭が 127.0.0.1 で始まっているもののみにマッチしてくれるというわけです。




商談のコピーでは見えていない項目はコピーされない

最近知ったので備忘録。

Salesforce の商談にはコピー機能があります。

f:id:venturenet:20180810181146p:plain

これをコピーすると、

f:id:venturenet:20180810181305p:plain

こうなって、確度や売上月01、数量などの項目がコピーされていることがわかります。

商談のコピーについては、商談をコピーする場合の考慮事項 にて、以下のように説明されています。

メモ 項目に対するアクセス権が「参照のみ」の場合、その項目の値はコピーしたレコードには引き継がれません。

商談をコピーする場合の考慮事項 より

それで、上記から項目へのアクセス権さえあれば、例え画面上に表示されていなくともコピーされるものと思っていたのですが、ページレイアウトに追加されていない項目はコピーされませんでした。

当たり前といえば当たり前のような気もしますが、あるプロジェクトでは商談オブジェクトに様々なフラグをカスタム項目で持たせており、それらは主にトリガなどから参照/更新するもので画面上には表示していないものでした。

あるとき商談をコピーしたときの動作がおかしかったため、調査したところ上記が原因だったというわけです。