1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ServiceNowでレコードを参照するときのデータ型を意識する (1)

Last updated at Posted at 2024-02-26

はじめに

いきなりですが、こんなスクリプトをなんとなく書いていませんか?

よくあるし期待通りに動作するけど微妙な例
// 実行ユーザーが担当者にアサインされているとき
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です。念のためドキュメントへのリンクを貼っておきますが、GlideSystemgetUserID()メソッドは文字列を返します。つまり、右辺は現在のユーザーのシステムIDを文字列で返します

GlideRecordからのドットウォーク

一方で左辺のcurrent.assigned_toについて、まず、意識したことのない方がいらっしゃるかもしれないので、念のために触れておきますと、まずcurrentGlideRecordのオブジェクトです2。そして、GlideRecordオブジェクトからフィールド名にドットウォークしたものであるcurrent.assigned_toは、current.getElement('assigned_to')と同じ意味になるので、GlideRecordgetElement()メソッドの仕様に基づいて、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_togs.getUserID()の比較が、等価演算子だと成功し、厳密等価演算子だと成功しない理由になります。

修正例

いろいろと調べてきましたが、今や私たちはGlideRecord周辺の型の扱いについて厳密な知識を得たので、より堅牢な書き方ができます4

本稿的に良い例
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to.toString() === gs.getUserID()) {
    // ちゃんと処理できます。
}

ただ、それでは今後はGlideElementからシステムIDを取得するときは必ず明示的に文字列にしましょうっていう話かというと、実はそうでもないと思います。ここまで調べてきてわかったとおり、下のスクリプトは十分に厳密であいまいさがないと言えるためです。

current.assigned_to == gs.getUserID()

一方で、こういうのは避けるべきです。

避けたい例1
// だれかが作ったAPIを呼び出す。
new WildScriptInclude().wildMethod(current.assigned_to);  // APIが引数をどう扱うかは不明
避けたい例2
// オブジェクトに値を入れて呼び出し元に返す。
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);

これは動作します。

先ほどGlideRecordgetElement()メソッドのドキュメントを見たときに、レコードから値を取るために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

  1. OOTBでも使われてはいますが、どちらかというと少数派で、特にシステムIDを厳密比較演算子で判定する例はめったにありません。

  2. Now Platformのスクリプティング環境においては、ビジネスルールで使えるprevious変数や、関係性(sys_relationship)で使えるparentなどもGlideRecordです。

  3. OOTBの多くのスクリプトも依拠しているこの挙動が安易に変更になるとは思いませんが、システムIDが返ってくるというのは文書で公開された仕様ではなく、そのように動作するという観察結果に過ぎない点は注意が必要です。

  4. toString()を明示的に呼ばず、空文字列との連結処理によって強制的に変換するようなテクニックもよく行われています。そのほうが記述は短いですが、個人的には意味が明快なtoString()のほうが良いと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?