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

## はじめに

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

```JavaScript:よくあるし期待通りに動作するけど微妙な例
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to == gs.getUserID()) {
    // 何らかの処理
}
```

ServiceNowのスクリプティング言語はJavaScriptですが、この言語には多くの素晴らしい利点がある一方で、トリッキーな部分もあります。比較演算子の挙動なんて言うのはトリッキーさの筆頭に挙げられるものではないかという気がします。

上の等価演算子（`==`）を厳密等価演算子（`===`）に変えてみると。

```JavaScript:期待通りには動かない例
// 実行ユーザーが担当者にアサインされているとき
if (current.assigned_to === gs.getUserID()) {
    // ここに到達することはない
}
```

この比較は決してtrueになることがないので、if文の中の処理は絶対に実行されません。ServiceNowのスクリプトで厳密等価演算子を使うことはほとんどないのかもしれませんが[^1]、今日はこの辺の、理解があいまいになりそうな部分について自戒を込めておさらいしてみたいと思います。


## 最初の例の分析

最初の例の何が問題なのかを改めて考えてみましょう。まず、

```JavaScript
current.assigned_to === gs.getUserID()
```

の厳密等価演算子の左右の式をそれぞれ考えます。

### GlideSystemのgetUserID()

右辺のほうがわかりやすいので、こちらから行きますが、まず`gs`というのは、`GlideSystem`オブジェクトが格納されているグローバル変数ですので、この中身は`GlideSystem`です。念のためドキュメントへのリンクを貼っておきますが、`GlideSystem`の`getUserID()`メソッドは文字列を返します。つまり、右辺は**現在のユーザーのシステムIDを文字列で返します**。

https://developer.servicenow.com/dev.do#!/reference/api/vancouver/server/no-namespace/c_GlideSystemScopedAPI#r_ScopedGlideSystemGetUserID

### GlideRecordからのドットウォーク

一方で左辺の`current.assigned_to`について、まず、意識したことのない方がいらっしゃるかもしれないので、念のために触れておきますと、まず`current`は`GlideRecord`のオブジェクトです[^2]。そして、`GlideRecord`オブジェクトからフィールド名にドットウォークしたものである`current.assigned_to`は、`current.getElement('assigned_to')`と同じ意味になるので、`GlideRecord`の`getElement()`メソッドの仕様に基づいて、`GlideElement`のオブジェクトが返ってきます。つまり、`current.assigned_to`で帰ってくるのは、**システムIDと思っている人もいるかもしれませんが、実は`GlideElement`オブジェクト**です。

このあたりの仕様についてもドキュメントが詳しいです。（このドキュメントの`getElement()`の解説にはもう一つ大事なことが書いてありますが、それは後に回します）

https://developer.servicenow.com/dev.do#!/reference/api/vancouver/server/no-namespace/c_GlideRecordScopedAPI#r_ScopedGlideRecordGetElement_String

最初に挙げた比較の式、

```JavaScript
current.assigned_to === gs.getUserID()
```

がfalseに評価されるのは、この型の不一致が原因です。左辺は`GlideElement`オブジェクトで、右辺は文字列なので合わないのです。では、なぜ等価演算子だと成功するのか、さらに掘り下げてみます。

### 等価演算子の動き

MDNのJavaScriptのドキュメントを見てみると、等価演算子の説明の中に以下の記述があります。

> オペランドのうちの一方がオブジェクトで、もう一方が数値または文字列である場合は、そのオブジェクトの valueOf() および toString() メソッドを使用してプリミティブに変換を試みます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Equality

冒頭のスクリプトはまさにこれに合致していて、片方が文字列、もう片方が`GlideElement`オブジェクトなので、実行系は後者を文字列に変換するという動作をします。文字列への変換に際して、上のドキュメントによるとオブジェクトの`valueOf()`メソッドおよび`toString()`メソッドを用いると書かれています。これは`GlideElement`においてどう作用するかを考えます。

### 改めてGlideElement

再度`GlideElement`のドキュメントを見てみましょう。

https://developer.servicenow.com/dev.do#!/reference/api/vancouver/server/no-namespace/c_GlideElementScopedAPI#r_ScopedGlideElementToString

`GlideElement`には`valueOf()`メソッドは用意されていませんが、`toString()`メソッドは用意されています。メソッドの仕様として、フィールドの値を文字列で返すということが示されています。フィールドの値についてはそれ以上の細かいことは記載されていませんが、`GlideElement`オブジェクトが参照フィールドを指している場合は、システムIDが返ってきます[^3]。

したがって、`current.assigned_to`は、厳密には`GlideElement`オブジェクトだけれども、文字列と等価演算子で比較するときなどに暗黙的に文字列に変換する処理が行われるため、結果としてシステムIDを表す文字列を評価できるという動作になります。

これが`current.assigned_to`と`gs.getUserID()`の比較が、等価演算子だと成功し、厳密等価演算子だと成功しない理由になります。

### 修正例

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

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

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

```JavaScript
current.assigned_to == gs.getUserID()
```

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

```JavaScript:避けたい例1
// だれかが作ったAPIを呼び出す。
new WildScriptInclude().wildMethod(current.assigned_to);  // APIが引数をどう扱うかは不明
```

```JavaScript:避けたい例2
// オブジェクトに値を入れて呼び出し元に返す。
let result = {
    assigned_to: current.assigned_to,
};
return result; // 呼び出し元は普通は文字列が入っていると考える
```

これらは、呼び出し先あるいは呼び出し元で、`GlideElement`オブジェクトをどう使うかがわからないためです。後者などは、オブジェクトにセットされて返される値が文字列のシステムIDではなく生の`GlideElement`オブジェクトであるとわかって呼び出してもらうというのはかなり不親切な設計だと思います。


## GlideRecordのシステムID

一件落着なのですが、締める前に念のためもうひとつ確認しておきます。

なんとなく、こういうスクリプトを書いたことがある方はいませんか？（私はあります）

```JavaScript:動作するけど実は結構微妙な例
(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()`を利用するのが望ましい書き方です。

```JavaScript:改善した例
(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`のことも調べていきたいと思います。

:::note
続編はこちら。
**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()`のほうが良いと思います。