JDBC でプリペアードステートメント を使ったクエリでハマった事
Microsoft 的には、「パラメーター化されたクエリー」って言うのかな。
んでハマったポイントは、GROUP BY にパラメータの値を渡しているとエラーになる場合がある事。しかも実際の値で動かすと正常に動作するから性質が悪い。
具体的な例を書くと…
CREATE TABLE [Table1] ( [Id] INT IDENTITY(1, 1) PRIMARY KEY, [登録日] DATE, [金額] NUMERIC(12, 0) ) INSERT INTO [Table1] VALUES ('2000/01/01', 100) INSERT INTO [Table1] VALUES ('2001/01/01', 100) INSERT INTO [Table1] VALUES ('2002/01/01', 100) INSERT INTO [Table1] VALUES ('2003/01/01', 100) INSERT INTO [Table1] VALUES ('2004/01/01', 100) INSERT INTO [Table1] VALUES ('2005/01/01', 100) INSERT INTO [Table1] VALUES ('2006/01/01', 100) INSERT INTO [Table1] VALUES ('2007/01/01', 100) INSERT INTO [Table1] VALUES ('2008/01/01', 100) INSERT INTO [Table1] VALUES ('2009/01/01', 100) INSERT INTO [Table1] VALUES ('2010/01/01', 100) DECLARE @小学校卒業年 INT DECLARE @中学校卒業年 INT SET @小学校卒業年 = 2003 SET @中学校卒業年 = 2006 SELECT CASE WHEN DATEPART(YYYY, [登録日]) <= @小学校卒業年 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @中学校卒業年 THEN '2.中学生時代' ELSE '3.大人' END AS [年代], SUM([金額]) AS [金額] FROM [Table1] GROUP BY CASE WHEN DATEPART(YYYY, [登録日]) <= @小学校卒業年 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @中学校卒業年 THEN '2.中学生時代' ELSE '3.大人' END DROP TABLE [Table1]
こんな感じの事を実現しようと Java 側で実装するクエリは、
※テーブルやデータは既に存在する前提
SELECT CASE WHEN DATEPART(YYYY, [登録日]) <= ? THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= ? THEN '2.中学生時代' ELSE '3.大人' END AS [年代], SUM([金額]) AS [金額] FROM [Table1] GROUP BY CASE WHEN DATEPART(YYYY, [登録日]) <= ? THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= ? THEN '2.中学生時代' ELSE '3.大人' END
こうなり、これを JDBC のプリペアードステートメント を使って実行すると、SQL SERVER 側では次の様なクエリが実行されます。
※大分端折ってますが、SQL Server Profiler で実行されたクエリを見るとこんな感じでした。*1
DECLARE @p0 INT DECLARE @p1 INT DECLARE @p2 INT DECLARE @p3 INT SET @p0 = 2003 SET @p1 = 2006 SET @p2 = 2003 SET @p3 = 2006 SELECT CASE WHEN DATEPART(YYYY, [登録日]) <= @p0 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @p1 THEN '2.中学生時代' ELSE '3.大人' END AS [年代], SUM([金額]) AS [金額] FROM [Table1] GROUP BY CASE WHEN DATEPART(YYYY, [登録日]) <= @p2 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @p3 THEN '2.中学生時代' ELSE '3.大人' END
これを実行すると、以下の様なエラーになります。
メッセージ 8120、レベル 16、状態 1、行 51
列 'Table1.登録日' は選択リスト内では無効です。この列は集計関数または GROUP BY 句に含まれていません。
SELECT と GROUP BY で使っている変数名が異なっています、そのためエラーが発生しています。
でも、実際の値を使って実行すると次の様なクエリになりエラーになりません。このせいで何が原因か暫くわからなかった!
SELECT CASE WHEN DATEPART(YYYY, [登録日]) <= 2003 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= 2006 THEN '2.中学生時代' ELSE '3.大人' END AS [年代], SUM([金額]) AS [金額] FROM [Table1] GROUP BY CASE WHEN DATEPART(YYYY, [登録日]) <= 2003 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= 2006 THEN '2.中学生時代' ELSE '3.大人' END
ちなみに、.NET の SqlCommand クラス (System.Data.SqlClient) では、名前付きパラメータを使えるので、上記の様な事は起きません。
.NET 側で実装するクエリは、以下のような形になり SQL SERVER 側でも同じクエリで実行されました。
※これも SQL Server Profiler で確認*2
SELECT CASE WHEN DATEPART(YYYY, [登録日]) <= @小学校卒業年 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @中学校卒業年 THEN '2.中学生時代' ELSE '3.大人' END AS [年代], SUM([金額]) AS [金額] FROM [Table1] GROUP BY CASE WHEN DATEPART(YYYY, [登録日]) <= @小学校卒業年 THEN '1.小学生時代' WHEN DATEPART(YYYY, [登録日]) <= @中学校卒業年 THEN '2.中学生時代' ELSE '3.大人' END
※.NET でも、名前付きパラメータを使えないケース(OdbcCommand クラス (System.Data.Odbc) 等)では、Java と同様のエラーが起きます。
*1:SQL SERVER 2005 では、[http://msdn.microsoft.com/ja-jp/library/ff487303.aspx:title] を使って実行されてました。
*2:[http://msdn.microsoft.com/ja-jp/library/ms188001.aspx:title] を使って実行されていました。