ServiceNowのテーブルで重複レコードを避ける方法

## はじめに

ServiceNowのデータベースのレコードにはほぼ必ず[^1]ユニークなシステムID（通称Sys ID）が付与されます。この仕組みのせいでマスター情報の維持管理が難しい[^2]などという悩みはありますが、一方でそれ以外のフィールドでは重複に対しておおらかな態度でいられるのは、その恩恵とも言えます。

そのような状況なので、ServiceNowのテーブルでは、ユーザーが利用する項目においてはわざわざ重複を避けることはしないことも多いですが、厳密な管理業務を追い求めると自ずと重複チェックの重要性は増してきます。

今回は簡単な話題ですが、ServiceNowのテーブルで重複レコードを避ける方法を考えてみたいと思います。

## 実装方法検討
### いちばん手っ取り早い方法

いちばん手っ取り早い方法は、辞書エントリテーブル（`sys_dictionary`）で、重複チェックしたいフィールドのレコード上で「一意（`unique`）」フィールドにチェックをつけることです。ちなみにこのフィールドはOOTBでは表示されていないので、フォームを修正して表示させる必要があります。

![辞書エントリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/3b11f931-d03b-a6d4-1e52-b78ee015a973.png)

こうすると、チェックをつけたフィールドに重複する値を入れることはできなくなります。これは非常に強い制約で、既存のレコードにすでに重複がある場合は有効にできませんし、ビジネスルールとも無関係に効いてきます。（推測でしかありませんが、おそらくはバックエンドのデータベースに対して、実際に一意制約がかかっています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/3ac229bc-93f2-cea0-2c05-098f09436077.png)

これは極めて強力な機能ですが、業務的には少し使いづらいところがあります。

* 単一フィールドの一意制約しか作れない
* いかにもデータベース層っぽいエラーメッセージをユーザーに見せることになる

というわけで、絶対に一意にしたいときには有効ですが、利用する局面は多くないように思います。実はOOTBでもほとんど使われていません。（有名なところだと、ユーザーテーブル（`sys_user`）のユーザーIDフィールドが挙げられます）

### より普通の方法

結局ビジネスルールでチェックするのが最も一般的であるように思います。この場合はこのようなビジネスルールを書くことになるでしょう。

```javascript:ビジネスルール「Prevent duplicate」
(function executeRule(current, previous /*null when async*/ ) {
    if (new global.GlideQuery('cmn_department')
        .where('sys_id', '!=', current.getUniqueValue())
        .where('id', current.getValue('id'))
        .selectOne()
        .isPresent()) {
        gs.addErrorMessage(gs.getMessage('Department ID {0} already exists.', current.getValue('id')));
        current.setAbortAction(true);
    }
})(current, previous);
```
GlideQueryを使っているのは半分趣味ですが、この話題もどこかで語りたいと思います。[^3]

この処理を含むビジネスルールをテーブルのBefore-InsertとBefore-Updateで動かしておけばとりあえず重複は防げます。あと、このケースの場合IDフィールドを必須にしておくことも必要でしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/9dcc2e9a-5ba7-9ee3-1ec3-dd94a63e971e.png)

この方法はOOTBでも広範に使われている方法で（ビジネスルールテーブルで「Duplicate」などで検索すればたくさん出てきます。）、複合一意制約にしたい場合はそういうビジネスルールにすればいいですし、やり方次第でいくらでも複雑にできます。また、ユーザーを意識したメッセージを自由に設定できるのも大きなメリットです。

一方で難点としては以下のようなことが挙げられます。

* あくまで更新時と挿入時のチェックなので、たとえばEasy Importを使ったときにビジネスルールを無効にしてインポートしたとき[^4]など、重複するデータを運用上発生させてしまう可能性があること
* テーブルのレコード件数が多くなると、挿入と更新のたびに全件検索が走るので、パフォーマンスへの影響が無視できなくなること

とはいえ、OOTBで多く使われる運用なので、まずはファーストチョイスにすべき実装方法だと思います。

### とっておきの方法

3番目は複合キーにも一意制約をかけられるデータベース層の方法です。該当のテーブルの設定画面を開き、関連リスト「データベースインデックス」から新規インデックスを作成します。つまり、テーブルに対して複合キーインデックスを作ることで、複数のフィールドに一意制約をかけます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/36455aaa-c6a5-49c3-8e40-e250138ae9ba.png)

このケースでは、事業部と部門名称の複合キーということで、異なる事業部配下であれば同じ部門名を許容するというルールにしてみたいと思います。

インデックス作成はバックグラウンドで時間をかけて実施されるので、Create Indexボタンをクリックすると完了時の連絡先を尋ねられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/98be9d3e-d7f5-c6f8-be0f-25b329daeedd.png)

この方法は非常に強力で、最初の方法と同じくデータベースのエラーメッセージが出てしまう難点はありますが、複合一意制約を性能よくかけることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/4ddcd02c-4df0-61e2-248a-7aa681bc1f63.png)

最大の難点は、ServiceNowにおいてはインデックスは一度作成するとユーザーの権限では削除できず、Now Supportからの問い合わせをしなければ修正できないということです。重複することが業務上必要なフィールドに対して間違えて作成してしまうと、必要なデータは格納できないわ、自分ではすぐに復元できないわで踏んだり蹴ったりになりますので、よく気をつけて実施する必要があります。

（この節の画面イメージが英語なのは、取り返しがつかない作業のためにPDIインスタンスを急遽用意しているためです。お見苦しい点ご容赦ください。）

## おわりに
ServiceNowのテーブルで重複レコードを避けるための方法ということで、一意制約をかける実装について調べてみました。通常は、ビジネスルールで実装するのが良いと思います。ただ、ビジネスルール実行の所要時間が業務上無視できないレベルになってきた場合は、その他の方法は大いに検討の余地があります。ただ、取り返しのつかない方法もあるので十分に気をつけて実施したいものです。

今回の内容は、Communityにあったこの記事の内容を参考にしています。

https://www.servicenow.com/community/developer-articles/how-to-enforce-a-unique-constraint-across-multiple-columns-in-a/ta-p/2385611


[^1]: 例外はたまにあります。ビュー（`sys_ui_view`）テーブルにおける、Default Viewのレコードなど。

[^2]: あるレコードを参照している別のレコードがあったとき、参照先レコードを削除して新規に作り直してしまうと、内容が同じだったとしても元のレコードとは異なるシステムIDになります。そのため参照元レコードからの参照は切れてしまいます。参照用のテーブルを最新化するときに逐一更新するのではなく、一括で「洗い替え」する運用は、システム運用の現場では伝統的に行われているプラクティスですが、ServiceNowで実施する場合は一工夫必要になります。

[^3]: もう一つ、メッセージの使い方があまり褒められたものではないのですが、これもそのうち取り上げたいと思います。

[^4]: Easy Import利用時のプロパティ。ビジネスルールの実行は停止できます。 https://docs.servicenow.com/csh?topicname=easy-import-properties.html&version=latest