< 前へ | 次へ >

演習 4: prepare を使用したクエリーの動的構成

EGL prepare ステートメントを使用すると、SQL ステートメントが動的に構成されます。

前回の演習で、パラメーターより大きいか等しい価格を持つ品目をデータベースから選択する関数を作成しました。
function printExpensiveItems(maxPrice Price in)
  
tempItem Item;
open expensiveItems for tempItem with
  #sql{
    select
      EGL.ITEM.ITEM_ID, EGL.ITEM.NAME, EGL.ITEM.IMAGE,
      EGL.ITEM.PRICE, EGL.ITEM.DESCRIPTION
    from EGL.ITEM
    where
      EGL.ITEM.PRICE >= :maxPrice
    order by
      EGL.ITEM.ITEM_ID asc
  };
  
forEach (tempItem)
  SysLib.writeStdout(tempItem.Name :: " " ::
    tempItem.Description :: " $" :: tempItem.Price);
end
パラメーターより小さいか等しい価格の品目を返す場合は、どのような関数を使用するのでしょうか。あるいは、最大価格と最小価格との間の品目を返す場合は、どのような関数を使用するのでしょうか。open ステートメントで明示的な SQL を使用する場合は、SQL ステートメントの Where 文節にある比較がハードコーディングされるため、それぞれの可能性ごとに 1 つ、計 3 つの関数をコーディングしなければなりません。
SELECT ステートメントをより柔軟に作成する方法は、EGL prepare ステートメントを使用して、実行時にクエリーをアセンブルすることです。prepare を使用するには、まず SQL コードのテキストを含む STRING 変数を作成します。
selectStatement STRING = 
  "select * from EGL.ITEM where EGL.ITEM.PRICE >= 500";
次に、prepare ステートメントを使用して、このストリングを SQL ステートメントに変換します。ステートメントの名前を指定し、オプションで、ステートメントが影響するのがどの SQLRecord パーツであるかを示すレコード変数も指定します。
tempItem Item;
prepare preparedStatement from selectStatement for tempItem;
これにより、getopen、または execute ステートメント内の明示的な SQL の代わりに、準備済みステートメントを使用できるようになります (execute については、後の演習でより詳細に学習します)。
open expensiveItems for tempItem with preparedStatement;

準備済みステートメントでよく発生するエラー

この方法でストリングから SQL ステートメントを作成するのは、SQL に精通したプログラマーの上級テクニックです。EGL は設計時に SQL ステートメントが正確であるかどうか検査しないため、実行時にストリングが正しい SQL ステートメントに解決することを確認する必要があります。以下は、準備済みステートメントの処理でよく生じるエラーのリストです。

ホスト変数
準備済みステートメントでは、明示的な SQL で使用するのと同じ方法でホスト変数を使用できません。次の例を見てください。
tempItem Item;
maxPrice Price = 500;
selectStatement STRING =
  "select * from EGL.ITEM where EGL.ITEM.PRICE >= :maxPrice";
prepare preparedStatement from selectStatement for tempItem;
この場合、実際の SQL ステートメントには、全くそのままのストリング :maxPrice が含められます。そのストリングがホスト変数として使用されて maxPrice 変数の値で置換されることはありません。結果は、誤った SQL ステートメントとなります。
代わりに、変数の値をストリングに挿入する必要があります。
tempItem Item;
maxPrice Price = 500;
selectStatement STRING =
  "select * from EGL.ITEM where EGL.ITEM.PRICE >= " :: maxPrice;
prepare preparedStatement from selectStatement for tempItem;
この場合、maxPrice 変数の値は EGL がストリングを作成するときに解決され、次の SQL ステートメントを作成します。
select * from EGL.ITEM where EGL.ITEM.PRICE >= 500
後でこの演習で、準備済みステートメントで変数を使用する別の方法を学習します。
スペーシング
SQL ステートメント内のキーワードとキーワードは、スペースで分離してください。例として、複数のストリング値を連結記号 (::=) で連結することにより準備された SQL ステートメントを示します。
incorrectSQL STRING;
incorrectSQL = "select * from EGL.ITEM where";
incorrectSQL ::= "EGL.ITEM.PRICE >= :maxPrice";
incorrectSQL ::= "order by EGL.ITEM.PRICE";
この例からは、次の誤った SQL ステートメントが作成されます。
select * from EGL.ITEM whereEGL.ITEM.PRICE >= :maxPriceorder by EGL.ITEM.PRICE
このステートメントには、WHERE の後と ORDER BY の前にそれぞれスペースが必要です。
適切なステートメント
EGL は準備済みステートメントに対して妥当性検査を行いません。ステートメントが正しい SQL であることを確認することは、お客様に任されています。

また、準備済み SQL ステートメントは、明示的な SQL と同様に、一緒に使用する EGL ステートメントに適合している必要があります。例えば、EGL get および open ステートメントには結果セットが必要です。これらのステートメントのいずれかと一緒に準備済みステートメントを使用するには、SQL コードが結果セットを返す必要があります。このため、SQL INSERT または DELETE ステートメントを準備し、そのステートメントを get または open と一緒に使用することはできません。

演習 4A: 準備済みステートメントの使用

この演習では、前回の演習で作成したプログラムを、2 つのパラメーター (そのうちの 1 つまたは両方を NULL にすることが可能) を受け入れるように変更します。この関数により、非 NULL のパラメーターに基づいてクエリーが作成され、ある一定の範囲の価格の品目が戻ります。

  1. selectItems.egl ファイルを開きます。
  2. NULL 可能な 2 つの Price パラメーターを受け取る printItemRange という名前の関数を追加します。
    function printItemRange(minPrice Price? in, maxPrice Price? in)
    
    end

    パラメーター・タイプの後の疑問符 (?) は、変数が NULL 値を受け取ることができることを示しています。このようにして、関数は結果の範囲について最小と最大のいずれかまたは両方を受け取ることができます。

  3. SQL コードを保持して SELECT ステートメントの開始を SQL コードに挿入する、STRING 変数を作成します。
    selectStatement STRING = "select * from EGL.ITEM where ";
    次のステップは、適切な Where 文節がどのような分節であるかを判別することです。次の 4 つの状況が考えられます。
    • 両方のパラメーターが NULL である。この場合、関数はゼロより大きいか等しい価格の品目をすべて出力するため、Where 文節は where EGL.ITEM.PRICE > 0 になります。
    • 両方のパラメーターが指定されている。この場合、関数は SQL BETWEEN キーワードを使用して、パラメーターとパラメーターの間、またはパラメーターと等しい値を出力します。Where 文節は、where EGL.ITEM.PRICE between :minPrice and :maxPrice になります。
    • 最大パラメーターのみが指定されている。この場合、関数は最大より小さいか等しい価格の品目をすべて出力し、where EGL.ITEM.PRICE <= :maxPrice になります。
    • 最小パラメーターのみが指定されている。この場合、関数は最小より大きいか等しい価格の品目をすべて出力し、where EGL.ITEM.PRICE >= :minPrice になります。
    これを作成する方法はいくつかありますが、単純な方法として、3 つのネストされた if ステートメントを使用する方法を示します。
  4. 次のコードを関数に追加して、Where 文節を完成させます。
    if (minPrice == NULL && maxPrice == NULL)
      // Two empty parameters;
      // return all rows with PRICE greater than zero.
      selectStatement ::= "EGL.ITEM.PRICE > 0 ";
    else
    
      if (minPrice != NULL && maxPrice != NULL)
        // Both parameters were specified.
        selectStatement ::= "EGL.ITEM.PRICE between " :: minPrice :: " and " :: maxPrice :: " ";
      else
        // Only one parameter was specified.
        if (minPrice == NULL)
          // Maximum was specified.
          selectStatement ::= "EGL.ITEM.PRICE <= " :: maxPrice :: " ";
        else
          // Minimum was specified.
          selectStatement ::= "EGL.ITEM.PRICE >= ":: minPrice :: " ";
        end
      end
      
    end
  5. ORDER BY 節を使用してステートメントを完成させます。
    selectStatement ::= "order by EGL.ITEM.PRICE";
  6. SQL ステートメントが正しいことを確認するために、コンソールに出力します。
    SysLib.writeStdout("Completed query: "::selectStatement);
  7. 使用するステートメントを準備します。これは、準備済みステートメントがアクセスするテーブルから作成されるレコードを参照して行います。
    tempItem Item;
    prepare preparedStatement
      from selectStatement
      for tempItem;
  8. open ステートメントで、明示的な SQL の代わりに準備済みステートメントを使用します。
    open expensiveItems for tempItem with preparedStatement;
  9. クエリーの結果を出力します。
    forEach (tempItem)
      SysLib.writeStdout(tempItem.Name :: " " ::
        tempItem.Description :: " $" :: tempItem.Price);
    end
    完成した関数は次のようになります。
      function printItemRange(minPrice Price? in, maxPrice Price? in)
        
        selectStatement STRING = "select * from EGL.ITEM where ";
        
        if (minPrice == NULL && maxPrice == NULL)
          // Two empty parameters;
          // return all rows with PRICE greater than zero.
          selectStatement ::= "EGL.ITEM.PRICE > 0 ";
        else
        
          if (minPrice != NULL && maxPrice != NULL)
            // Both parameters were specified.
            selectStatement ::= "EGL.ITEM.PRICE between " :: minPrice :: " and " :: maxPrice :: " ";
          else
            // Only one parameter was specified.
            if (minPrice == NULL)
              // Maximum was specified.
              selectStatement ::= "EGL.ITEM.PRICE <= " :: maxPrice :: " ";
            else
              // Minimum was specified.
              selectStatement ::= "EGL.ITEM.PRICE >= ":: minPrice :: " ";
            end
          end
          
        end
        
        // Regardless of parameters, sort by price.
        selectStatement ::= "order by EGL.ITEM.PRICE";
        
        // Print completed SQL code.
        SysLib.writeStdout("Completed query: "::selectStatement);
        
        // Prepare the statement to be used.
        tempItem Item;
        prepare preparedStatement
          from selectStatement
          for tempItem;
        
        // Use the prepared statement in place of explicit SQL.
        open expensiveItems for tempItem with preparedStatement;
        
        // Print the results to the console.
        forEach (tempItem)
          SysLib.writeStdout(tempItem.Name :: " " ::
            tempItem.Description :: " $" :: tempItem.Price);
        end
        
      end
  10. プログラムの main 関数から関数を呼び出します。
      function main()
        minPrice, maxPrice Price?;
        minPrice = 50;
        maxPrice = 500;
        try
          printItemRange(minPrice, maxPrice);
        onException(exception SQLException)
          handleDBException(exception);
        end
      end
  11. プログラムを生成し、実行します。
上記の例のパラメーター値を使用して、関数は価格が 50 から 500 の間の品目を出力します。
Completed query: select * from EGL.ITEM where EGL.ITEM.PRICE 
  between 50.00 and 500.00 order by EGL.ITEM.PRICE
Serial Mouse Great little mouse.  All kinds'a applications. $65.22
Keyboard Got QWERTY?  If not, go get this awesome flat keyboard. $78.99
Watch Keep time - and look rakish! $99.01
Shredder Don't let that politically-sensitive document fall into the wrong hands! $198.99
Combination Safe Keep your valuables safe and secure.  Put 'em in here. $222.22
次の例に示すように、パラメーター値を編集して、NULL 値を含むさまざまな範囲を試行できます。
function main()
  minPrice, maxPrice Price?;
  minPrice = 500;
  maxPrice = null;
  try
    printItemRange(minPrice, maxPrice);
  onException(exception SQLException)
    handleDBException(exception);
  end
end
値として 500 と NULL を渡すと、次のような結果が生じます。
Completed query: select * from EGL.ITEM where 
EGL.ITEM.PRICE >= 500.00 order by EGL.ITEM.PRICE
Desktop PC Lots'a power, at the right price.  Includes monitor. $888.55
Laptop PC Great little machine.  Take it anywhere with you. $1111.11

これで、selectItems.egl ファイルのコードが完成しました。 このファイル内にエラーがある場合 (赤の X 記号でマークされている場合) は、演習 4A で完成した selectItems.egl ファイルのファイルに記載されているコードと、作成したコードが一致していることを確認してください。

演習 4B: prepare ステートメント内のホスト変数

前の例では SQL ステートメントは、Where 文節内の変数が固定されていたという点で制限がありました。どの変数が使用されるかわかる前であっても、準備済みステートメントを作成することができます。このタイプのステートメントはより複雑ですが、ステートメントを準備しておいて後で詳細を決めたい場合には柔軟性があります。

前の例では、変数をクエリー・ストリングの残りの部分と連結するために、変数の値を解決しなければなりませんでした。
selectStatement STRING = "select * from EGL.ITEM where ";
...
selectStatement ::= "EGL.ITEM.PRICE between " :: minPrice :: " and " :: maxPrice :: " ";
準備済みステートメントを作成する際に、疑問符 (?) をステートメントに置いて、その値が後で埋められることを示すこともできます。
selectStatement2 STRING = "select * from EGL.ITEM where " ::
  "EGL.ITEM.PRICE between ? and ? " ::
  "order by EGL.ITEM.PRICE";

tempItem Item;
prepare preparedStatement2 
  from selectStatement2
  for tempItem;
これで、準備済みステートメントに 2 つのホスト変数を置く場所ができました。これらの場所は、BETWEEN 分節内の疑問符 (?) によって示されています。準備済みステートメントを使用する準備ができたら、using ステートメントを使用して値を挿入します。 例えば、価格が 5 から 500 の間の行を検索するには、リテラルの 5 と 500 をステートメントに渡します。
open expensiveItems2 for tempItem with preparedStatement2
  using 5, 500;
もちろん、using キーワードと一緒に変数を使用することもできます。
open expensiveItems2 for tempItem with preparedStatement2
  using minPrice, maxPrice;
このような方法で準備済みステートメントでホスト変数を使用すると、ステートメントを 1 度アセンブルしておいて、異なる値で何度も呼びだす際に便利です。例えば、次の関数は 3 つの異なる範囲内にある値を出力するために、3 回同じ準備済みステートメントを使用します。
function printRanges()
  
  tempItem Item;
  selectStatement string = "select * from EGL.ITEM where " ::
    "EGL.ITEM.PRICE between ? and ? " ::
    "order by EGL.ITEM.PRICE";

  prepare preparedStatement from selectStatement for tempItem;

  SysLib.writeStdout("¥nPrinting values between 0 and 200");
  open expensiveItems for tempItem with preparedStatement using 0, 200;
  forEach(tempItem)
    SysLib.writeStdout(tempItem.Name :: " " :: tempItem.Description ::
      " $" :: tempItem.Price);
  end

  SysLib.writeStdout("¥nPrinting values between 200 and 500");
  open expensiveItems for tempItem with preparedStatement using 200, 500;
  forEach(tempItem)
    SysLib.writeStdout(tempItem.Name :: " " :: tempItem.Description ::
      " $" :: tempItem.Price);
  end

  SysLib.writeStdout("¥nPrinting values between 500 and 1,000");
  open expensiveItems for tempItem with preparedStatement
    using 500, 1000;
  forEach(tempItem)
    SysLib.writeStdout(tempItem.Name :: " " :: tempItem.Description ::
      " $" :: tempItem.Price);
  end

end

ホスト変数をこのように使用すると、前のセクションのプログラムを再作成でき、起こり得るケースごとにステートメントを変更することなく 1 つの準備済みステートメントを使用できます。

  1. 前のセクションと同様に、selectItems.egl ファイルで、最大および最小パラメーターを受け入れる新規関数を追加します。
    function printItemRange2(minPrice Price? in, maxPrice Price? in)
    
    end
    この関数でステートメントを準備してから、後で変数を挿入します。
  2. 準備済みステートメントを保持するストリング変数を作成し、prepare ステートメントを使用して準備済みステートメントを準備します。
    selectStatement STRING = "select * from EGL.ITEM where " ::
      "EGL.ITEM.PRICE between ? and ? " ::
      "order by EGL.ITEM.PRICE";
    
    tempItem Item;
    prepare preparedStatement 
      from selectStatement
      for tempItem;
    この準備済みステートメントに 2 つのパラメーターを渡して、検索の境界を示します。最大価格が指定されていない場合は、検索の上限を知っておく必要があります。なぜなら、ステートメントを準備した後で EGL.ITEM.PRICE between ? and ? 分節を EGL.ITEM.PRICE >= ? に変換することはできないからです。したがって、上限として使用するテーブル内の最高価格を調べておく必要があります。この上限は、SQL MAX() 関数で簡単に調べることができます。
  3. MAX() 関数を使用してそれをレコード変数の Price フィールドに入れて Item 変数を作成し、データベース内の最も高額の品目の価格を取得します。
    get mostExpensiveItem 
      into mostExpensiveItem.Price
      with
        #sql{
          select
            max(EGL.ITEM.PRICE)
          from EGL.ITEM
        };
    これで、mostExpensiveItem レコードに、Items テーブル内の最高価格が含まれます。
  4. どのパラメーターが指定されたかを判別し、それに従って準備済みステートメントを使用します。
    if (minPrice == NULL && maxPrice == NULL)
      // Two empty parameters;
      open expensiveItems for tempItem with preparedStatement 
        using 0, mostExpensiveItem.Price;
    else
    
      if (minPrice != NULL && maxPrice != NULL)
        // Both parameters were specified.
        open expensiveItems for tempItem with preparedStatement 
          using minPrice, maxPrice;
      else
        // Only one parameter was specified.
        if (minPrice == NULL)
          // Maximum was specified.
          open expensiveItems for tempItem with preparedStatement 
            using 0, maxPrice;
        else
          // Minimum was specified.
          open expensiveItems for tempItem with preparedStatement 
            using minPrice, mostExpensiveItem.Price;
        end
      end
      
    end
  5. forEach を使用して、結果を出力します。
    forEach (tempItem)
      SysLib.writeStdout(tempItem.Name :: " " ::
        tempItem.Description :: " $" :: tempItem.Price);
    end
    完成した関数は次のようになります。
    function printItemRange2(minPrice Price? in, maxPrice Price? in)
      
      selectStatement STRING = "select * from EGL.ITEM where " ::
        "EGL.ITEM.PRICE between ? and ? " ::
        "order by EGL.ITEM.PRICE";
      
      // Prepare the statement to be used.
      tempItem Item;
      prepare preparedStatement 
        from selectStatement
        for tempItem;
      
      mostExpensiveItem Item;
      get mostExpensiveItem 
        into mostExpensiveItem.Price
        with
        #sql{
          select
            max(EGL.ITEM.PRICE)
          from EGL.ITEM
        };
        
      if (minPrice == NULL && maxPrice == NULL)
        // Two empty parameters;
        open expensiveItems for tempItem with preparedStatement 
          using 0, mostExpensiveItem.Price;
      else
      
        if (minPrice != NULL && maxPrice != NULL)
          // Both parameters were specified.
          open expensiveItems for tempItem with preparedStatement 
            using minPrice, maxPrice;
        else
          // Only one parameter was specified.
          if (minPrice == NULL)
            // Maximum was specified.
            open expensiveItems for tempItem with preparedStatement 
              using 0, maxPrice;
          else
            // Minimum was specified.
            open expensiveItems for tempItem with preparedStatement 
              using minPrice, mostExpensiveItem.Price;
          end
        end
        
      end
      
      // Print the results to the console.
      forEach (tempItem)
        SysLib.writeStdout(tempItem.Name :: " " ::
          tempItem.Description :: " $" :: tempItem.Price);
      end
      
    end
  6. main 関数を変更して、古い関数の代わりに新しい関数を呼び出します。
    function main()
      minPrice, maxPrice Price?;
      minPrice = 50;
      maxPrice = 500;
      try
        printItemRange2(minPrice, maxPrice);
      onException(exception SQLException)
        handleDBException(exception);
      end
    end
  7. NULL 値を含むさまざまな値の minPrice および maxPrice を使用する新規プログラムを保存し、生成して実行します。 結果は前の関数と同じですが、関数は異なる方法で結果に達しました。
これで、selectItems.egl ファイルのコードが完成しました。 このファイル内にエラーがある場合 (赤の X 記号でマークされている場合) は、演習 4B で完成した selectItems.egl ファイルに記載されているファイルのコードと、作成したコードが一致していることを確認してください。

演習のチェックポイント

有効な準備済みステートメントの作成は複雑ではありますが、作成されたステートメントは明示的な SQL でハードコーディングされたステートメントよりも柔軟性があります。後の演習で、SQL ステートメントをカスタマイズするもう 1 つの方法である、SQLRecord パーツにデフォルトの SQL コードを設定する方法を学習します。

準備済みステートメントについて詳しくは、「prepare」を参照してください。
< 前へ | 次へ >