メニューボタン
IBMi海外記事2021.10.27

ループの代わりにSETを使用する

Ted Holt 著

Tedさん、こんにちは。
お送りしたのは、ある照会プログラムに入れていたRPGソース コードです。コードを読んで、ご意見を聞かせていただきたいと思います。SETLL、DOW、およびREADE命令コードを使用して、選択されたレコードのグループをループする代わりに、SQLのSETステートメントを使用しました。こうした方が、他の人が見ても、ずっと理解しやすいと思います。これは良いコーディング手法と言えるでしょうか。
-- Mikeより

受信トレイの中にMikeさんからのメールを見つけて、いつものことながら、大変、嬉しく思いました。Mikeさんと直接お会いしたのは、数年前の RPG & DB2 Summit だったと思います。それ以来、彼とのやり取りは私の楽しみになっています。Mikeさんがレコードレベル アクセスからSQLへ移行しつつあるようで、喜ばしく思います。プログラムでSQLを使用したいものの、何らかの問題にぶつかったという読者の方々から、かなりのメールを頂いています。いよいよ、そのトピックに取り組む時が来たようです。

まず、説明用に小さなデータベース表が必要です。

create table Sales
  ( Order    dec(5)   not null,
    Line     dec(3)   not null,
    Deleted  char(1)  not null with default,
    Qty      dec(3)   not null with default,
    Price    dec(5,2) not null with default,
    primary key (Order, Line)
    );

では、Mikeさんが今後はもう書かないようにしたいと言っていたタイプのコードを見てみましょう。

FSales     if   e           k disk    rename(Sales: SalesRec)

D ctOrderLines    s              3p 0
D sumOrderLines   s              7p 2
D SearchOrder     s              5p 0

C                   EVAL      ctOrderLines = *zero
C                   EVAL      sumOrderLines = *zero
C     SearchOrder   SETLL     Sales
C     SearchOrder   READE     SalesRec
C                   DOW       not %eof(Sales)
C                   IF        Deleted <> 'Y'
C                   EVAL      ctOrderLines += 1
C                   EVAL      sumOrderLines += (Qty * Price)
C                   ENDIF
C     SearchOrder   READE     SalesRec
C                   ENDDO

そして、以下が、Mikeさんが送ってくれたソース コードに、少し手を加えたものです。

exec sql
   set (:ctOrderLines, :sumOrderLines) =
      (select count(*), sum(Qty * Price)
         from Sales
        where Order = :SearchOrder
          and Deleted <> 'Y');

これら2つのコードが実現することは同じでしょうか。まったく同じではありません。所定のオーダーに対してアクティブな(すなわち、削除されていない)行が1つ以上ある場合は、答えはイエスで、2つのコードは同じ結果を生成します。けれども、アクティブな行がない場合、2つ目の列はnullを返します(しかし、1つ目の列ではありません。count関数は、null値を返しません)。これは、coalesce関数で簡単に修正されました。

exec sql
   set (:ctOrderLines, :sumOrderLines) =
      (select count(*), coalesce(sum(Qty * Price), 0)
         from Sales
        where Order = :SearchOrder
          and Deleted <> 'Y');

これで、これら2つのコードは等価となりました。簡単だったのではないでしょうか。

話は変わるのですが、興味をそそられることがありました。Mikeさんが、SETステートメントを使用していたことです。長年、RPGプログラムにSQLを組み込む癖が染み付いているので、私ならSELECT INTOを使用したと思います。

exec sql
   select count(*), coalesce(sum(Qty * Price), 0)
     into :ctOrderLines, :sumOrderLines
     from Sales
    where Order = :SearchOrder
      and Deleted <> 'Y';

このことから、SETとSELECT INTOの間に何か違いがあるのか疑問が生じました。7.4の資料を見てみたところ、わずかな違いがあるようでした(「あわせて読みたい記事」欄のSETおよびSELECT INTOのリンクを参照)。以下は、それらの引用です。

(SET)は、アプリケーション・プログラムに組み込むことができます。これは、設定されているすべての変数がグローバル変数の場合には、動的に準備可能な実行可能ステートメントです。REXX で指定してはなりません。 (SELECT INTO)は、アプリケーション・プログラムに組み込んで使用します。それ以外の使用法はありません。これは実行可能ステートメントですが、動的に準備することはできません。REXX で指定してはなりません。

それほど違いはないでしょうか。いくつか短いテスト プログラムを書いてみましたが、SETとSELECT INTOはどちらもまったく同じように動作しました。

Mikeさんの質問に対する答えは、断然、「 イエス 」です。これは、間違いなく、良いコーディング手法と言えます。

今でもプログラムでSQLを使用していないショップがたくさんあることが私には信じられないのですが、ここ何年かの間にも、私が1990年代に書いていたようなコードが、現在も多くのショップで書かれているのを何度も目にしました。現実を認めざるを得ないようです。そんな折に、Mikeさんのような方々からメールをもらうと励みになります。さらには、この堅牢なIBM i プラットフォームの未来は、業界紙で言われているほど暗いものではないのだと、希望が湧いてきます。

SQLで悪戦苦闘しているという方は、 私宛てにコードを送ってみてください 。お手伝いしたいと思います。SQLに関する問題でメールを送ってくれるほとんどの方は、問題解決のすぐ手前にいるように思います。背中をちょっと一押しすることは難しいことではありません。約束はしませんが、できることはお手伝いします。このようなお願いをするのは、全部が全部、他の人のためを思ってのこと、というわけではありません。他の人のコードを読むことで、私も多くのことを学べるからです。MikeさんがSELECT INTOではなくSETを使用していたことも、そうした学びの一例でした。

あわせて読みたい記事

PAGE TOP