はじめに
ビジネスルールというのは、おそらくServiceNowでいちばんよく使う自動処理だと思います。いつからか、ドキュメント上の名称が「クラシックビジネスルール」になっていて、古いものであることを殊更に主張していますが(代わりにつかうべきはフローデザイナーであるということでしょう)、ServiceNowのストアアプリ製品でフローデザイナーよりも遥かに多用されている関係で、まったく新規のアプリ開発をする時でもないとなかなかフローデザイナーに振り切って実装するのは難しいです。
そんなビジネスルールについて、実行タイミングについての正確な理解を深めようということで、今回はドキュメントを読み直しつつ、そこから論理的に導かれる「こんな処理をつくるときはこのタイミングで実行」というガイドラインを考えてみたいと思います。
改めてビジネスルールを理解する
まずはドキュメントを確認しましょう。
ポイントは二つあり、ビジネスルール実行のきっかけになるレコード操作は何か、もう一つは実行されるのはそのレコード操作に対してどのタイミングなのか、ということです。ドキュメント掲載順と逆ですが、まずはレコード操作から。
ビジネスルールが適用されるレコード操作を理解する
以下の4つの操作を行ったときにビジネスルールを起動することができます。この4つはそれぞれに有効にしたり無効にしたりできるので、例えば「挿入と更新のときに動作するビジネスルール」なども作れます。ただし、クエリと他の3つにまたがるルールはあまり意味のある処理にはならないのでそれはやめたほうが良いです。
挿入
新規レコードをテーブルに追加するときに起動します。
更新
既存のレコードを更新するときに起動します。挿入も更新に含まれるかというと、含まれないことには注意してください。
削除
テーブルのレコードを削除したときに起動します。
クエリ
これは全く異なるルールです。データベースにクエリをかける前に起動されます。これを使うことによって、クエリに条件を追加することができます。主な用途はアクセス制御ですが、これについては他と全く違う話になって、かつ長くなるので別の機会に取り上げたいと思います。
実行のタイミングを理解する
次に実行のタイミングです。これらは前のレコード操作と異なり、どれか一つを排他的に選択する必要があります。ドキュメントを引用しながら話を進めます。
前(Before)
ユーザーがフォームを送信した後で、データベース内のレコードに対してアクションが実行される前。
アクション実行の前に動きます。このことが意味することで最も重要なのは、ビジネスルールの実行結果によってアクション実行を中止したいのであれば、そのルールは必ずBeforeで実行する必要があるということです。後ほど詳しく述べます。
後(After)
ユーザーがフォームを送信した後で、さらにデータベース内のレコードに対してアクションが実行された後。
アクション実行後に動きます。先ほどの話と逆で、この種類のビジネスルールはアクションを差し止めることはできません。アクション自体は行われたものとして、それ以外のアクションを付随して実施したいときに使います。
非同期(Async)
ユーザーがフォームを送信した後と、ビジネスルールから作成されたスケジュール設定済みジョブがスケジューラーによって実行された後。ユーザーがフォームを送信してから、データベース内のレコードに対して何らかのアクションが実行されるまでに、システムがビジネスルールからスケジュール設定済みジョブを作成します。
とくに一文目が誤訳というか、わかりづらいので英語原文も見ておきましょう。
After the user submits the form and after the scheduler runs the scheduled job created from the business rule.
'and'を「と」と訳しているので、独立な2つの条件のどちらかに合致したときという条件も読める日本語ドキュメントですが、このandはむしろ「かつ」のことでしょう。1
したがって、「ユーザーがフォームを送信した後で、スケジューラーがビジネスルールから作られたスケジュールジョブを実行した後」ということになります。
小難しい表現なのですが、ポイントは以下です。
- アクションの後で実行される(その点ではAfterと同じ)。
- 非同期実行される。Afterだと、ビジネスルールの処理が終わって初めてユーザーは次の操作をすることができますが、Asyncの場合、ルールの処理の実行前にユーザーに操作を渡します。そのあとでバックグラウンドでビジネスルールの処理を行います。
例えば、ビジネスルールのトリガーが、フォーム上のUIアクションボタンを押下することだったとすると、Afterビジネスルールの場合はボタン押下後すぐに処理が始まり、ユーザーは処理完了後に操作ができるようになります。同じ処理をAsyncで実行するとボタンを押すとすぐにユーザーは次の操作をできます。処理はバックグランドで行われます。
表示(Display)
これも今回は割愛。クライアントスクリプトとサーバーサイドスクリプトの連携のための機能で、独立した大きなテーマです。
ビジネスルール実行のガイドライン
Beforeを選ぶべきか、Afterを選ぶべきか、はたまたAsyncを選ぶべきかを判断する基準はいくつかあります。完全に包括的な基準にはなかなか難しいですが、こういう時はこれ、というのがはっきりしているものを覚えておくだけでも悩みは減るのではないでしょうか。
まずはタイミング以前の問題の絶対的ルール。
current.update()
を実行しない
ServiceNowのビジネスルールにおける禁忌事項として有名ですし、上のドキュメントにも書いてあるのでご存じの方も多かろうと思いますが、current.update()
という処理はしてはいけません。
これをやると、その更新処理から再びビジネスルールがトリガーされてしまい、大量の無駄な処理を行ってしまうことになります。とりわけ、自分自身が更新時のビジネスルールなのであれば(多くの場合はそうでしょう)、その中でcurrent.update()
を行うことで無限ループに入ります。
Now Platformにはこういう状況を検出して無限ループを防ぐ安全機構があるので、直ちに無限ループになってサーバーの異常を起こすわけではありませんが、そのような挙動があることを前提にした機能実装はすべきではありませんし、する意味もありません。そのようなことを行う必要がある状況がありません。
Beforeビジネスルールはその中で更新処理をしなくても、どのみちすべてのBeforeビジネスルールが完了したあとで更新されます2。
では、OOTBでも1か所もないのかというと、実はときどきいます。どうやっているかというと、current.setWorkflow(false)
と併用して、後続のビジネスルールの起動を抑制したうえで実行しています。
であれば実行していいじゃないか、という話ではないです。日本企業のITでよく見かけるのは、この実装例を根拠に「すべてのビジネスルールにおいては、current.setWorkflow(false)
を行い、しかる後にcurrent.update()
を行うことで、確実に更新されるようにする」みたいなコーディングルールを書いてしまうような現場ですが、お願いですからやめてください。
そもそも業務要件としてcurrent.update()
がないと成り立たない状況というのがほとんどありません。current.setWorkflow(false)
は本来必要なチェックもバイパスして更新を実行する方法です。防げたはずのデータの不整合まで防げなくなります。害だけがあってメリット皆無です。
current
に変更を加えるルールはBefore
これもほぼ例外のない絶対的なルールです。current
の内容を変更するルールはBeforeです。理由は簡単で、Afterでcurrent
に対して変更を行うためには、current.update()
をしないといけないので、前のルールに思いっきり抵触するためです。
チェックを行うビジネスルールならBefore
例えば、データベースの内容の整合性を確認するようなビジネスルール(重複チェックなど)は、実際にデータベースにデータが書き込まれる前に実施しないと意味がありません。実際にデータベースに反映されてしまった後で動いても手遅れです。 なので、データに対して何らかのルールを強制するビジネスルール(ビジネスルールという名前から考えたら、それが本来のビジネスルールです)は、Beforeで動かすことにより、データベース反映の前に問題を検知するという原則が導かれます。
current
ではないデータを操作するルールはBeforeで実行しない
先ほどの基準の逆で、current
ではないデータ、つまり同一テーブルの別のレコード、あるいは他テーブルのレコードの操作を行うルールは先ほどとは逆に、Beforeで実行してはダメで、AfterかAsyncで動かす必要があります。
これは実例を交えて説明しましょう。例えば、このフォーム。
Reviewerフィールドに追加したユーザーを、専用のグループに追加したいとします。そのためには、このフォームを保存したときのビジネスルールで、グループメンバーテーブル(sys_user_grmember
)にレコードを追加する必要があります。なのでこんな実装になるでしょう。
(function executeRule(current, previous /*null when async*/) {
// レビューワーグループを取得する。
const grp = new GlideRecord('sys_user_group');
grp.addQuery('name', 'Qiita Task Reviewers');
grp.query();
if (grp.next()) {
// レビューワーグループに現レコードのReviewerがすでにいないか確かめる。
const grmember = new GlideRecord('sys_user_grmember');
grmember.addQuery('user', current.getValue('reviewer'));
grmember.addQuery('group', grp.getUniqueValue());
grmember.setLimit(1);
grmember.query();
if (!grmember.next()) {
// レビューワーグループに現レコードのReviewerを追加する。
grmember.initialize();
grmember.setValue('user', current.getValue('reviewer'));
grmember.setValue('group', grp.getUniqueValue());
grmember.insert();
}
}
})(current, previous);
正しく動くと、このレビューワーグループにこのようにユーザーが追加されます。
なのですが、例えば同時にこんなルールもあったとしましょう。レビューワーと担当者が同じ人間であってはならないという(Segregation of Dutiesの観点からは妥当な)ルールです。
このとき、もし同じ人間を2つのフィールドにセットして保存するとチェックによりエラーになります。
しかしあろうことか、Abel Tutorはレビューワーグループには入ってしまうのです。
これは2つのルール(レビューワーと担当者が同じであってはいけないルールと、レビューワーをグループに追加するルール)を両方Beforeで動かしてしまったことが原因で、実行の順序は実は関係ありません。どちらを先に動かしても同じ現象が起きます。
ビジネスルールにおけるAbort Action(あるいはsetAbortAction(true)
メソッドの利用)は、即時中止ではなく、アクションの前に行われる予定だったビジネスルールは全て実行されます。そのため、Beforeでデータ書き込みをしてしまうと、処理を中断したときに書く必要のなかったレコードが書かれてしまうのです。
処理件数が事前に予測できない更新はAsyncか別の方法を考える
BeforeやAfterのビジネスルールについて、ユーザーがその契機になる操作を行うと、実際にビジネスルールの処理が終わるまでユーザーは操作ができなくなります。したがって、ここであまりに重い処理を挟んでしまうとユーザビリティを損ないます。
これは絶対的なルールでもないのですが、個人的には、実際に更新する件数の上限が特定できるときのみ、BeforeあるいはAfterのビジネスルールにするようにしています。不特定多数の更新が入るビジネスルールはAsyncで動かすか、あるいはビジネスルールをやめてスクリプトアクションかスケジュールスクリプトでの処理にします。
例えば、子レコードを更新したときにparent
フィールドで参照する親レコードに更新をかけるビジネスルールの場合、そのルールが更新するレコードは親レコード1件だけです。一方で、親レコードを更新したときに、そのレコードを参照している子レコードすべてに更新をかけるという処理は、子レコードが何個あるかわからないので不特定多数のレコードへの更新になります。子レコードが5件とか10件という世界なら気にしなくてよいかもしれませんが、親レコードを参照してよいという子レコードの数には制限がありません。10,000件更新になってしまうと、BeforeやAfterのビジネスルールでやると不便な処理になる可能性があります。それでは厳しいので、こういうときはAsyncか別の方法3にするということです。
まとめ
長くなってしまったのでここで一回切ります。
いくつかのルールが登場しましたが、基本的にはアクションの前で実行するのか、アクションの後で実行するのか、非同期で実行するのかについては、「こうでなければならない」という説明をロジカルにできるケースが意外とあります。なんでもいいというわけではないということです。
ただ、いくつか書きそびれたこともあるので、また続編として書きたいと思います。