はじめに
いきなりですが、こんなスクリプトをなんとなく書いていませんか?
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to == gs.getUserID()) {
// 何らかの処理
}
ServiceNowのスクリプティング言語はJavaScriptですが、この言語には多くの素晴らしい利点がある一方で、トリッキーな部分もあります。比較演算子の挙動なんて言うのはトリッキーさの筆頭に挙げられるものではないかという気がします。
上の等価演算子(==
)を厳密等価演算子(===
)に変えてみると。
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to === gs.getUserID()) {
// ここに到達することはない
}
この比較は決してtrueになることがないので、if文の中の処理は絶対に実行されません。ServiceNowのスクリプトで厳密等価演算子を使うことはほとんどないのかもしれませんが1、今日はこの辺の、理解があいまいになりそうな部分について自戒を込めておさらいしてみたいと思います。
最初の例の分析
最初の例の何が問題なのかを改めて考えてみましょう。まず、
current.assigned_to === gs.getUserID()
の厳密等価演算子の左右の式をそれぞれ考えます。
GlideSystemのgetUserID()
右辺のほうがわかりやすいので、こちらから行きますが、まずgs
というのは、GlideSystem
オブジェクトが格納されているグローバル変数ですので、この中身はGlideSystem
です。念のためドキュメントへのリンクを貼っておきますが、GlideSystem
のgetUserID()
メソッドは文字列を返します。つまり、右辺は現在のユーザーのシステムIDを文字列で返します。
GlideRecordからのドットウォーク
一方で左辺のcurrent.assigned_to
について、まず、意識したことのない方がいらっしゃるかもしれないので、念のために触れておきますと、まずcurrent
はGlideRecord
のオブジェクトです2。そして、GlideRecord
オブジェクトからフィールド名にドットウォークしたものであるcurrent.assigned_to
は、current.getElement('assigned_to')
と同じ意味になるので、GlideRecord
のgetElement()
メソッドの仕様に基づいて、GlideElement
のオブジェクトが返ってきます。つまり、current.assigned_to
で帰ってくるのは、システムIDと思っている人もいるかもしれませんが、実はGlideElement
オブジェクトです。
このあたりの仕様についてもドキュメントが詳しいです。(このドキュメントのgetElement()
の解説にはもう一つ大事なことが書いてありますが、それは後に回します)
最初に挙げた比較の式、
current.assigned_to === gs.getUserID()
がfalseに評価されるのは、この型の不一致が原因です。左辺はGlideElement
オブジェクトで、右辺は文字列なので合わないのです。では、なぜ等価演算子だと成功するのか、さらに掘り下げてみます。
等価演算子の動き
MDNのJavaScriptのドキュメントを見てみると、等価演算子の説明の中に以下の記述があります。
オペランドのうちの一方がオブジェクトで、もう一方が数値または文字列である場合は、そのオブジェクトの valueOf() および toString() メソッドを使用してプリミティブに変換を試みます。
冒頭のスクリプトはまさにこれに合致していて、片方が文字列、もう片方がGlideElement
オブジェクトなので、実行系は後者を文字列に変換するという動作をします。文字列への変換に際して、上のドキュメントによるとオブジェクトのvalueOf()
メソッドおよびtoString()
メソッドを用いると書かれています。これはGlideElement
においてどう作用するかを考えます。
改めてGlideElement
再度GlideElement
のドキュメントを見てみましょう。
GlideElement
にはvalueOf()
メソッドは用意されていませんが、toString()
メソッドは用意されています。メソッドの仕様として、フィールドの値を文字列で返すということが示されています。フィールドの値についてはそれ以上の細かいことは記載されていませんが、GlideElement
オブジェクトが参照フィールドを指している場合は、システムIDが返ってきます3。
したがって、current.assigned_to
は、厳密にはGlideElement
オブジェクトだけれども、文字列と等価演算子で比較するときなどに暗黙的に文字列に変換する処理が行われるため、結果としてシステムIDを表す文字列を評価できるという動作になります。
これがcurrent.assigned_to
とgs.getUserID()
の比較が、等価演算子だと成功し、厳密等価演算子だと成功しない理由になります。
修正例
いろいろと調べてきましたが、今や私たちはGlideRecord
周辺の型の扱いについて厳密な知識を得たので、より堅牢な書き方ができます4。
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to.toString() === gs.getUserID()) {
// ちゃんと処理できます。
}
ただ、それでは今後はGlideElement
からシステムIDを取得するときは必ず明示的に文字列にしましょうっていう話かというと、実はそうでもないと思います。ここまで調べてきてわかったとおり、下のスクリプトは十分に厳密であいまいさがないと言えるためです。
current.assigned_to == gs.getUserID()
一方で、こういうのは避けるべきです。
// だれかが作ったAPIを呼び出す。
new WildScriptInclude().wildMethod(current.assigned_to); // APIが引数をどう扱うかは不明
// オブジェクトに値を入れて呼び出し元に返す。
let result = {
assigned_to: current.assigned_to,
};
return result; // 呼び出し元は普通は文字列が入っていると考える
これらは、呼び出し先あるいは呼び出し元で、GlideElement
オブジェクトをどう使うかがわからないためです。後者などは、オブジェクトにセットされて返される値が文字列のシステムIDではなく生のGlideElement
オブジェクトであるとわかって呼び出してもらうというのはかなり不親切な設計だと思います。
GlideRecordのシステムID
一件落着なのですが、締める前に念のためもうひとつ確認しておきます。
なんとなく、こういうスクリプトを書いたことがある方はいませんか?(私はあります)
(function executeRule(current, previous /*null when async*/ ) {
// IDの重複をチェックする
var dept = new GlideRecord('cmn_department');
dept.addQuery('id', current.id);
dept.addQuery('sys_id', '!=', current.sys_id);
dept.setLimit(1);
dept.query();
if (dept.next()) {
// エラー処理
}
})(current, previous);
これは動作します。
先ほどGlideRecord
のgetElement()
メソッドのドキュメントを見たときに、レコードから値を取るためにGlideRecord
からドットウォークするなという記載があったのをご覧になったでしょうか。これが実はあまりお勧めではない作法で、フィールド値だけでなくオブジェクト全体を取得してメモリを浪費するというのが理由のようです。
フィールド値を取得するなら、getValue()
を、表示値を取得するならgetDisplayValue()
を、そしてシステムIDを取得するときはgetUniqueValue()
を利用するのが望ましい書き方です。
(function executeRule(current, previous /*null when async*/ ) {
// IDの重複をチェックする
var dept = new GlideRecord('cmn_department');
dept.addQuery('id', current.getValue('id')); // 推奨
dept.addQuery('sys_id', '!=', current.getUniqueValue()); // 推奨
dept.setLimit(1);
dept.query();
if (dept.next()) {
// エラー処理
}
})(current, previous);
まとめ
たかがシステムID、されどシステムID。適当なスクリプティングでもある程度それっぽく動くのですが、きちんと動く根拠を知っておくべきだなというのが今回の学びでした。
- 参照型フィールドへドットウォークしたときは、明示的に文字列に変換したほうが確実
-
GlideRecord
のシステムIDを取得するにはgetUniqueValue()
を使う
長くなったので今回はシステムIDだけで終了ですが、この話題はまだまだ尽きず、また改めて書いておきたいのは、システムIDを取るとき以上に気持ち悪い、True/Falseフィールドや選択肢フィールドの値を取得するときの動きについてです。また、文字列もブーリアンもプリミティブで返されるので安心してスクリプトが書けると評判(?)のGlideQuery
のことも調べていきたいと思います。
続編はこちら。
ServiceNowでレコードを参照するときのデータ型を意識する (2)
https://qiita.com/yujiarakitokyo/items/adc0e5644ebff5636ee9
-
OOTBでも使われてはいますが、どちらかというと少数派で、特にシステムIDを厳密比較演算子で判定する例はめったにありません。 ↩
-
Now Platformのスクリプティング環境においては、ビジネスルールで使える
previous
変数や、関係性(sys_relationship
)で使えるparent
などもGlideRecord
です。 ↩ -
OOTBの多くのスクリプトも依拠しているこの挙動が安易に変更になるとは思いませんが、システムIDが返ってくるというのは文書で公開された仕様ではなく、そのように動作するという観察結果に過ぎない点は注意が必要です。 ↩
-
toString()
を明示的に呼ばず、空文字列との連結処理によって強制的に変換するようなテクニックもよく行われています。そのほうが記述は短いですが、個人的には意味が明快なtoString()
のほうが良いと思います。 ↩