XPath 基礎 (1)

みなさんもご存知の通り、Web ページは HTML で書かれています。

ですので、スクレイピングしたりクライアントサイドで動的なページを作ったりするときには、自分が処理したいその部分を何らかの方法で指定する必要があります。

jQuery 使いの方に馴染みが深いのは CSS セレクタかと思います。

.block {
  width: 100px;
}

.block がセレクタですね。 「class 属性に block が指定されている要素」を指定するものです。

CSS セレクタでもかなり複雑な指定ができますが、XPath を使用するとさらに複雑な指定ができるようになります。

CSS セレクタでは、

  • タグ名で指定した要素 (div, p)
  • id 属性や class 属性に、指定した文字列が設定されている要素 (#hoge, .hoge)
  • 任意の属性の値が任意の文字列と一致する要素 (div[class="hoge"])
  • 指定した要素の下位、直下、兄弟などの要素 (div p a, div > p)

といった指定の仕方が基本となります。

XPath でも同じように、タグ名、構造、属性値などで要素を指定することができます。

XPath では、ロケーションパスという表現を使って要素を指定します。例えば、

//div

これは全部の div タグを指定することになります。

XPath では、ディレクトリ構造を指定するように、/ (スラッシュ) で要素を区切って指定します。

//div/p/a

上記であれば、「すべての div タグの直下にある p タグの直下にある a タグ」ということになります。

最初の / はルートノードを意味します。ルートノードとはなんぞやということですが、HTML 文書は入れ子の構造であり、つまりツリー構造になっています。
その一番上の頂点がルートノードです。ですので、

/div

としてしまうと、それは頂点の直下の div ということになってしまいますので、これに合致する要素はありません。

// は、「自身および子孫」を表します。なので、//div はすべての div タグを意味するというわけです。

//div//p

上記のようにすれば、「すべての div タグのそれぞれの子孫にある p タグ」ということになります。

<div id="d1">
  <p>...</p>
  <div id="d1-1">
    <p>...</p>
    <section>
      <p>...</p> <!-- この p は div/p ではマッチしない -->
    </section>
  </div>
</div>
</html>




Salesforce で You have uncommitted work pending エラー

Salesforce の Apex コードでは、外部のWebサービスを利用することができます。つまり HTTP リクエストを送信することができます。 これを Salesforce ではコールアウトと呼んでいます。


通常の Apex コードではコールアウトを行うことはできず、callout=true を付与した future メソッドなどの、個別のスレッドを使用する仕組みを用いなければコールアウトを行うことはできません。


future メソッドを実装すること自体は、単に @future アノテーションを使用すればよいだけなので、特段難しいことはありません。
ただし、以下のような点には注意が必要です。

  • future メソッドの引数にはプリミティブ型かプリミティブ型のコレクションしか指定できない (つまりオブジェクトをそのまま渡すことはできないし、独自のクラスのインスタンスを渡すこともできない)
  • future メソッドは別のトランザクションになる - static な変数を共有することはできない
  • future メソッドは非同期である - いつ実行されるのかは不定である (ただし単体テストでは Test.stopTest の後には完了している)
  • future メソッドから future メソッドを呼ぶことはできない
  • future メソッドは、ガバナ制限により1つのトランザクションでは50回までしかコールできない


上記ののような future メソッドに関する注意点には気をつけていたのですが、先日初めてのエラーに遭遇しました。
こんな処理がありました。

トリガ発動 → コールアウト実行 → 結果をDMLで書き込み


コールアウトを行う箇所は future メソッドになっていて、コールアウト実行後、その結果を UPDATE するというよくあるような処理です。

当初は問題なく動作していました。


その後、複数レコードに未対応であることが発覚し、以下のように修正しました。

トリガ発動 → (コールアウト実行 → 結果をDMLで書き込み)をレコード数分繰り返し


ところがここで、標題の「System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out」が発生してしまいました。


この原因はメッセージの通りで、未コミットのデータベース変更がある状態ではコールアウトは実行できないというものです。

コールアウト実行 → 結果をDMLで書き込み(ここまではOK) → コールアウト実行(ここでエラー) というわけです。


なんのための制限かわかりませんが、(コールアウト実行 → 結果をリストやマップなどに保持)を繰り返す → レコード更新 とする必要があります。



Salesforce はこの手の落とし穴のようなところが少なくありませんので、気をつけたいところです。


MySQL をコマンドラインで使う

自動化するならコマンドラインですね。ということで、最近使った MySQLコマンドラインを記録しておきます。

データベースを作成する

mysql -u<ユーザID> -p<パスワード> -e "CREATE DATABASE <データベース名> CHARACTER SET utf8mb4"

※ -u と -p の後ろは空けない

そのデータベースに全権を持つユーザを作成する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "GRANT ALL ON <データベース名>.* TO <新しいユーザ名>@localhost"


その作成したユーザにパスワードを設定する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "SET PASSWORD FOR <ユーザ名>@localhost=password('<セットするパスワード>')"


そのデータベースにデータをインポートする

zcat <gzip圧縮されたSQL文> | mysql -u<ユーザID> -p<パスワード> <データベース名>


UPDATE文を発行してインポートしたデータを修正する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "UPDATE wp_options SET option_value=<変更後の値> WHERE option_name='siteurl'"

※ここでは Wordpress の wp_options を直す例



上記はバッチ/スクリプトにできるので、例えば新しくデータベースを作成して、そのデータベースと同名のユーザにそのデータベースへの全権を付与して、何らかのデータをインポートする といったことであれば、以下のようにできます。

#!/bin/bash

mysql -u<ユーザID> -p<パスワード> -e "CREATE DATABASE $1 CHARACTER SET utf8mb4"
mysql -u<ユーザID> -p<パスワード> $1 -e "GRANT ALL ON $1.* TO $1@localhost"
mysql -u<ユーザID> -p<パスワード> $1 -e "SET PASSWORD FOR $1@localhost=password('$1')"
wget http://<どこかのサーバに置いてあるデータ>/data.sql.gz
zcat data.sql.gz | mysql -u<ユーザID> -p<パスワード> $1
rm -f data.sql.gz

この例では可変なのはデータベース名(ユーザ名)のみですが、可変にしたい箇所は引数として受け取るようにすればよいでしょう。

これを c.sh として保存したすると、使うときは、

sh c.sh <データベース名>

となります。


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 でアクセスできるようになります。