OutSystems11 OutSystems

[OutSystems] Aggregate で NullIdentifier() を使うときの注意点

OutSystems の Aggregate の Filters に条件式を書くとき、同一の評価結果となるべき条件式を If の中に書くか、外に書くかで実行結果が変わる、という問題に遭遇したのでメモ。

tl;dr

  • Aggregate の中で NullIdentifier() = 0 と思って使わない。
  • NullIdentifier() と比較する時、データ型が本当に Identifier 型であることをよく確認する。

問題の事象

まずは、Aggregate の2つの実行結果をご覧ください。

1つ目
img

2つ目
img

どちらも UserId = NullIdentifier() が評価されるため、同じ実行結果となる事を期待しますが、そうはなっていません。1つ目の実行結果は2件のレコードが返ってきていますが、2つ目は0件です。この2つの実装の違いは評価式が If の外にあるか中にあるか、のみです。

ちなみに UserId は LongInteger で値は変数の初期値のままです(つまり 0 です)。
OutSystems の NullIdentifier() Function のドキュメントを見ると、 NullIdentifier() = 0 と記載があることから、UserId = NullIdentifier()0 = 0 で true となるべきのため、レコードが返ってこない2つ目の結果は期待する動作と異なることになります。

原因と考察

ポイント1: 条件式を If の中に書くか外に書くか

問題の事象が起きた時の SQL を比べてみると次のようになっていました。

If の外に評価式を書いた場合は、その評価式が SQL に含まれていない事がわかります。

なぜ SQL に含まれていないかというと、恐らくこれは OutSystems による SQL の最適化によるものです。UserId = NullIdentifier() の UserId は単なる変数なので DBMS へ SQL を投げずとも事前に評価可能なため、評価して結果が true であれば SQL に含めないという事だろうと思います。

実際、評価式を UserId <> NullIdentifier() として必ず false となるようにして実行してみると、問題の事象の1つ目の画像にはなかった WHERE 句の条件が、次のように追加されました。

SELECT TOP (32) ENFoo.[ID] o0, ENFoo.[USERID] o1, ENFoo.[BAR] o2, ENFoo.[DATE] o3, ENFoo.[TIME] o4, ENFoo.[LONGINT] o5
FROM [JJUEOP012].DBO.[OSUSR_BYG_FOO] ENFoo
WHERE (@UserId IS NOT NULL)

まとめるとこのような感じです。

If の 事前の評価結果 SQL
true 出力されない
false 出力される
true 出力される
false 出力される

ここで重要な点は、事前に評価可能な条件式の場合、If の中に書く時と外に書く時で実行される SQL が変わるという事です。


追記

ここまで書いてから次の条件式で実行してみました。If を使っておらず、DBMS へ SQL を発行する前に評価可能なため、WHERE句には何も出力されないことを想像しましたが、実際は異なっていました。

UserId = NullIdentifier() and 1=1

実行結果
img

WHERE句に次のように出力されていますね…

WHERE ((@UserId = 0)
	AND (1 = 1))

ということなので、If の中/外に限らず、SQL が変わる場合があるとだけ頭の隅おいておけば良いと思います。

ポイント2: Aggregate の中の NullIdentifier()

OutSystems の NullIdentifier() Function のドキュメント を見ると NullIdentifier() = 0 ですが、いつ何時でもそうとは限らないようです。

上で UserId <> NullIdentifier() とした時の SQL が次のようになっていましたが、

WHERE (@UserId IS NOT NULL)

UserId の変数の型を LongInteger から User Identifier 型に変更すると次のように変わります。

WHERE (@UserId <> 0)

この SQL はまさしく期待する SQL のため、同じデータ型同士で比較するべきだ、という結論が頭をよりぎますが、問題点はそこではありません。

Aggregate の中で NullIdentifier() を使うと、比較するデータ型に応じて NULL として扱われたり、0 として扱われたりと一貫性がない事です。ポイント1で挙げたように DBMS に SQL を発行する前は true と評価されていたものが DBMS 側では False と評価されていることが、今回の問題事象の原因です。Aggregate 以外で使う場合、NullIdentifier() は一貫して 0 として扱われるため、Aggregate の場合だけ動作が異なるように見えます。

そのため、数値型と比較する場合は次のように 0 を Null として扱かってもいいように思います。

WHERE (@UserId IS NOT NULL or @UserId <> 0)

実際のところ、外部データベースを使わず、OutSystems のデフォルトのデータベースに Create/Update するだけならば、Null 値が入ることはないため、こちらの方が自然な動作のようにも感じます。

とはいえ開発者としては、この動作を変えることはできないため、受け入れて、Aggregate の中では NullIdentifier は 0 になったり NULL になったりする場合がある、と覚えておいたほうが良いでしょう。

対処方法・まとめ

  1. ポイント1で確認したように、Filters に書いた条件式がどこで(OutSystems 側/DBMS側のどちらか)で実行されているかは、時と場合によるため、うまく動かない時は Executed SQL をみて、意図した通りの SQL になっているか確認するのがベター。
  2. Aggregate の中で NullIdentifier() = 0 と思って使わない。
  3. NullIdentifier() と比較する時、データ型が本当に Identifier 型であることをよく確認する。