この記事は以下の記事の続編です。
ServiceNowでレコードを参照するときのデータ型を意識する (1)
https://qiita.com/yujiarakitokyo/items/0b1d7c483087d289d998
はじめに
前回、主にGlideRecordから参照型フィールドをドットウォークで取得したときの挙動について調査しました。
結果として、ドットウォークで取得されるのは参照型フィールドが持つシステムIDの文字列表現ではなく、GlideElementオブジェクトであること、とはいえGlideElementから文字列への変換は多くの場合自動で行われるので、そのまま後続処理に渡しても多くの場合は問題なく処理できることがわかりました。ただ、この文字列への変換は暗黙的に行われるものなので、注意が必要というのが前回の一応の結論でした。
今回はその続編です。いろいろ実験してみましたが、中にはかなり難解な挙動を行うものもあります。
検証
選択肢を取得するときの型
意外と自信を持って言えない(私は言えないので調べています)ケースその1は、選択肢フィールドの値です。今回、このような選択肢フィールドを用意いたしました(バカみたいな例であることは承知しています)。これを使って検証してみたいと思います。
これで選択肢1を選んで次のスクリプトを流します。あえてスケジュールスクリプトで実行するのは、実行契機を「オンデマンド」にすることで、ボタン押せばすぐ実行できて検証に適しているためです。保存しておけるし、保存しても更新セットにキャプチャーされないし、という都合のよさもあります。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
if (qtask.sample_choice.toString() === '1') {
gs.info('Value of Sample Choice is string value "1"');
}
}
})();
一応思惑通りにログが出ています。選択肢型をドットウォークして文字列に変換すると、選択肢の値が文字列で返ってくるというある意味当たり前の話です。もちろん、値であり、選択肢フィールドのほかのカラム(例えばラベル)でないことはとても大事です。
同じフィールドを今度はgetValue()
で取得してみると。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
if (qtask.getValue('sample_choice') === '1') {
gs.info('Value of Sample Choice is string value "1"');
}
}
})();
やはりログが同じように出ています。何も面白くない例ですが、ここでわかることは 「数値っぽい値を選択肢の定義には入れているが、getValue()
はあくまで文字列で値を返してくる」 ということです。1
数値型を取得するときの型
今度は適当な数値型フィールドを追加してみましょう。
我ながらバカみたいな例ですが、このフィールドに数値42を入れてみます。
こちらをスクリプトで読み取ります。分岐が付いた時点でお察しなのですが。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
if (qtask.getValue('sample_integer') === 42) {
gs.info('Value of Sample Integer is numeric value 42');
} else if (qtask.getValue('sample_integer') === '42') {
gs.info('Value of Sample Integer is string value "42"');
}
}
})();
結果としては、これも文字列として返ってきます。
ことプログラミングの観点からいうと、ServiceNowというのはかなり文字列主導の社会で、あまり文字列以外のことを信用しない仕組みでできています。データベースとしては数値で格納したつもりでも、読み取った値は基本的に文字列です。数値的な演算に使うためには必ず数値に変換して、数値であることを確認して処理する必要があります。
例えばこの処理を行いますと......
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
gs.info(`42 + 1 = ${qtask.getValue('sample_integer') + 1}`);
}
})();
この結果を返します。データベース上の定義が数値型であることと全く直感的には一致しませんが仕様です。これには十分な注意が必要です。
数値に変換する方法としてはJavaScript標準のNumber()
でよいと思います。ていうか、中途半端に知ったかぶりをして標準APIのGlideStringUtil.getNumeric()
などを使っても、数値にはなりません。2
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
gs.info(`42 + 1 = ${Number(qtask.getValue('sample_integer')) + 1}`);
}
})();
真偽値型を取得するときの型
同じようなノリで、True/False型のフィールドも試してみます。
そして、スクリプトで読み取ってみます。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
if (qtask.getValue('sample_boolean')) {
gs.info(`Sample Boolean is checked.`);
} else {
gs.info(`Sample Boolean is not checked.`);
}
}
})();
これが直感に反する結果になるのですが、このコードはサンプル真偽値フィールドにチェックを入れても入れなくても内側のif
がtrue
に評価されて、Checkedのほうのログを出力します。
そんなアホなと思われるかも知れませんが、実は、True/Falseフィールドに対してgetValue()
で値を取りに行くと、チェックが入っているときは文字列の「1」が、入っていないときは文字列の「0」が返ってきます。
JavaScriptにはFalsyな値という概念があり、真偽値として評価したときにfalse
に評価される値が決まっています。
文字列の「0」はここに入っていないので、つまりtrue
になります。(数値の「0」はfalse
です)
これではまともな処理が書けないので、解決策を考えます。いくつかの方法がありますが、OOTBで広く行われているのは、ドットウォークしたGlideElementをそのまま評価することです。前回の結論は何だったのでしょう。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
if (qtask.sample_boolean) {
gs.info(`Sample Boolean is checked.`);
} else {
gs.info(`Sample Boolean is not checked.`);
}
}
})();
これで正しく動作します。しかし、このコードのqtask.sample_boolean
をqtask.sample_boolean.toString()
にすると、またチェックの有無に関わらずtrue
に評価されるようになります。
真偽値を含むGlideElementの謎
この挙動はかなり不可解なものですので、今度はこんなコードでテストしてみます。ちなみにいま「サンプル真偽値」にはチェックは入っていません。
(function() {
const qtask = new GlideRecord('x_snc_qiita_sample_qiita_task');
if (qtask.get('number', 'QTASK0001001')) {
gs.info(`Test1: ${qtask.sample_boolean}`);
gs.info(`Test2: ${typeof qtask.sample_boolean}`);
gs.info(`Test3: ${Boolean(qtask.sample_boolean)}`);
gs.info(`Test4: ${qtask.sample_boolean.toString()}`);
gs.info(`Test5: ${Boolean(qtask.sample_boolean.toString())}`);
gs.info(`Test6: ${qtask.getValue('sample_boolean')}`);
gs.info(`Test7: ${Boolean(qtask.getValue('sample_boolean'))}`);
}
})();
結果はこうなります。ちなみにドットウォークを省略せずにqtask.getElement('sample_boolean')
という書き方にしても同じ動きになります。
これが示唆するのは以下の事項です。
- チェックが入っていないTrue/Falseフィールドの値を
GlideRecord
で照会し、ドットウォークすると(つまり、GlideElement
を取得すると)、オブジェクトが返ってくる。(上記テスト2) - そのオブジェクトはFalsyである。(上記テスト3)
- そのオブジェクトを文字列に変換すると、文字列「false」に変換される。(上記テスト1と4)
- フィールドの値をgetValue()で取得すると文字列「0」が返ってくる。(上記テスト6)
- 文字列「false」は当然Falsyではない。(上記テスト5)
- 文字列「0」も当然Falsyではない。(上記テスト7)
下の二つはJavaScriptの仕様なので別にいいのですが、2番目などはかなり異様な挙動です。さきに引用したMDNのドキュメントによれば、「JavaScript で唯一の偽値のオブジェクトは、組み込みのdocument.all
です」とされているにも関わらず、ここにも偽値のオブジェクトがあるためです。ベンダーの実装なのだから標準と違うことだってあるだろうさと達観してももちろん問題ないのですが、であるからこそ、実装に関わるときは正確に理解しておきたいものです。
結果的にこの挙動のために、True/Falseフィールドを参照するコードはシンプルに書けます。ドットウォークしてそのまま評価すればいいからです。ただし、チェックが入っていない場合にFalsyであるこのオブジェクトに余計なことをすると途端にFalsyではない値になり、プログラムの挙動を大いに混乱させることになります。
まとめ
今回は少し難しい結果になりました。この挙動はよく理解しておくべきだと思います。
- 選択肢フィールドの値をドットウォークで取得して文字列に変換するときと、
GlideRecord.getValue()
で取得するときはいずれも、選択肢の「値」が文字列で返ってくる。値が数値によるコード値でも文字列になる。 - 整数フィールドの値についても同様に、必ず文字列として返ってくる。よってテーブルから検索した値を数値計算したいときは変換の必要がある。
- 真偽値フィールドの値については、フィールドがチェックされているときにドットウォークするとオブジェクトが返ってきて、文字列に変換すると文字列「true」になる。一方で、チェックされていないときにドットウォークすると「Falsyなオブジェクト」が返ってくる。このオブジェクトを文字列に変換すると、文字列「false」が返ってくる。
-
GlideRecord.getValue()
で値を取得すると、チェックされているときは文字列「1」が、されていないときは文字列「0」が返ってくる。先の文字列「false」含め、空でない文字列はいずれもFalsyではないので、そのまま条件判定に使うと予期せぬ動作をする。
またこのテーマは扱うことになると思います。次に調べないといけないと思っているのはクライアントサイドでの挙動でしょうか。
-
はいそうですね、という話なのですが、これは設計上の判断には影響がある話です。つまり、選択肢(
sys_choice
)の値フィールドに数値で入れたとしても、どのみちスクリプトでは文字列としか取得できないということですので、例えば選択肢の値を数値的に評価するためにはどうせ数値へのキャストを求められることになり、あまり効率的な処理にはなりません。であるならばいっそのこと、コードよりも意味が分かる文字列を値にするほうが良いのではないかという議論が出てきます。 ↩ -
検証しながら「なんでやねん」と思ったのですが、このメソッドは文字列を数値にするのではなく、文字列から数字を抜き出して数字だけの文字列にしてくれるメソッドです。 https://developer.servicenow.com/dev.do#!/reference/api/vancouver/server/no-namespace/GlideStringUtilScopedAPI#GSUS-getNumeric_S ↩