WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

パラメータ化クエリと sp_executesql 変換

by WebSurfer 11. December 2019 16:05

SqlParameter クラスを利用してクエリをパラメータ化すると sp_executesql に変換されて SQL Server で実行されるのですが、変換によって予期せぬ問題が出ることがあるという話を書きます。

sp_executesql に変換

元の話は MSDN Forum のスレッド「パラメータを利用したLikeで欲しい結果が得られない 」です。

具体的にどういうことかは以下の通りです。

SQL Server のデータベースに以下の画像の通り tbl_Item テーブルがあり、Item_CD 列で LIKE 句による検索をかけるとします。型の nvarchar(6) を覚えておいてください。

tbl_Item テーブル

この tbl_Item テーブルをベースに、Visual Studio のデーターソース構成ウィザードで型付 DataSet + TableAdapter を作成します。Item_CD 列で検索できる TableAdapter のメソッドを追加します。その SELECT クエリは以下の画像の通りです。

TableAdapter のメソッド

上の画像の通り型付 DataSet + TableAdapter が作成できたら「データのプレビュー」画面を開きバラメータ ICD に %A00001% を代入して[プレビュー]ボタンをクリックします。3 レコードヒットすると期待しますが、結果は一件もヒットしません。

データのプレビュー

ICD に代入した %A00001% を %00001% に代えた場合は 3 件のレコードがヒットします。上に紹介した MSDN Forum のスレッドの最初の質問に書いてあった通りです。

データのプレビュー

何故 %A00001% では一件もヒットしないかですが、上に紹介した MSDN Forum のスレッドで質問者さんがアップしてくれたプロファイラの画像を見ると分かるので、借用して下に貼っておきます。

プロファイラの画像

理由は Item_CD 列の型 nvarchar(6) の 6 で制限を受けるからです。具体的には以下の 2 つです。

(1) SqlParameter の Size

上の画像の通り SELECT クエリをベースに Visual Studio のデータソース構成ウィザードで FillByICD, GetDataByICD メソッドを生成していますが、その際 @ICD には SqlParameter が以下のように初期化されパラメータとして設定されます。

this._commandCollection[1].Parameters.Add(
  new SqlParameter("@ICD", SqlDbType.NVarChar, 6,
  ParameterDirection.Input, 0, 0, "Item_CD", 
  DataRowVersion.Current, false, null, "", "", ""));

SqlParameter コンストラクタの第 3 引数がデータベースの Item_CD 列の型 nvarchar(6) の 6 となり、結果 SQL Server に送信するデータの最大量が 6 文字に制限されます。

上のプロファイラの画像で @CID=N'%A0000' と 6 文字になっているところに注目してください。この時は C# のコードでは "%A00001%" という文字列を代入したとのことですが、SQL Server では先頭から 6 文字だけしか受け取れていません。

(2) sp_executesql の N'@ICD nvarchar(6)'

SqlParameter を使った ADO.NET のパラメータ化クエリは、上のプロファイラ画像のとおり sp_executesql に変換されます。その中の N'@ICD nvarchar(6)' の 6 でも制限を受けます。

一番上の画像を見てください。SSMS で sp_executesql を実行したもので、そこでは @CID=N'%A00001%' としていますが、先頭から 6 文字しか SQL Server には渡せないようで、一件もヒットしていません。(@ICD には %A0000 が代入されると思われる)

ちなみに、N'@ICD nvarchar(6)', の 6 を 7 に変えると同じく @CID=N'%A00001%' で 3 件ヒットします。(@ICD には %A00001 が代入されると思われる)

何が N'@ICD nvarchar(6)', の 6 を決めているのかという疑問は残っていますが、この 6 が制限の一つであるのは結果から間違いなさそうです。

回避策は WHERE 句を WHERE Item_CD LIKE N'%' + @ICD + N'%' のように変更して、@ICD には A00001 と入力するのがよさそうです。

と言うか、普通は最初から WHERE Item_CD LIKE N'%' + @ICD + N'%' とすると思います。だから、今回の問題のような話は初耳だったのかもしれません。

LIKE 句を使った時の問題は、自分が知る限りですが先の記事「固定長パラメータの LIKE 比較」に書いた話がありました。

加えて、LIKE 句を使うと今回のような問題も起こり得るということを知ることができて、MSDN Forum のおかげで一つ利口になったような気がします。

Tags: , ,

ADO.NET

Linq to Entities / Objects

by WebSurfer 13. February 2019 15:17

Linq to Entities は、Linq to Objects とは違って、そのクエリ式が SQL Server などの DB で使われる SQL に変換できる必要があるという話を書きます。CodeZine の記事「LINQにも色々 ~SQLに変換されるモノと変換されないモノ」を見てください。図だけ借用して以下にも貼っておきます。

Linq to Objects / Enitities

その図を見れば一目瞭然だと思いますし、詳しいことは CodeZine の記事を読めば分かるのですが、それで終わってしまってはブログの記事としては面白くないので、どういう事例があったか(要するに失敗談)を書いておきます。(笑)

まず以下の例。これを実行すると ToList() のところで "System.NotSupportedException: LINQ to Entities does not recognize the method 'System.DateTime Parse(System.String)' method, and this method cannot be translated into a store expression." というエラーが出ます。

public class Filter
{
    public int Id { get; set; }
    public DateTime? Date { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // ADO.NET Entity Data Model
        TestDatabaseEntities ctx = new TestDatabaseEntities();
            
        var list = (from c in ctx.TestTable
                    select new Filter
                    {
                        Id = c.ID,
                        Date = DateTime.Parse(c.Date)
                    }).ToList();
    }
}

上の図の右側に「SQ に変換してデータベース上で実行」とありますが、DateTime.Parse メソッドが SQL に変換できないということでエラーになったということです。

上のコードで Filter クラスを初期化して Id, Date に代入するところからは C# に戻ってきて行うのかと期待してましたが、そうではなくて ToList の前まで全部 SQL に変換して SQL Server で実行しようとするようです。

ちなみに、Filter クラスの Date プロパティを string 型に変更して、DateTime.Parse なしで直接 c.Data を代入するようにすれば SQL に変換出来るようで、エラーなく期待した結果が得られます。

他の解決策としては、LINQ to Entities クエリで使用する SQL Server 関数を公開する SqlFunctions クラスのメソッドに該当するものがあれば、それの利用を検討するのがよさそうです。具体例は、Microsoft のドキュメント「方法: データベース関数を呼び出す」のサンプルコードを見てください。

上のコードの DateTime.Parse は該当する SqlFunctions クラスのメソッドは見つかりませんでしたが、「方法: カスタム データベース関数を呼び出す」にある方法も考えれば対応可能かもしれません。(未検証・未確認です)

なお、自分の環境 (Windows 10, VS2015, .NET 4.6.1, EF 6.2) では、上記の記事のリンク先の名前空間: System.Data.Objects.SqlClient の SqlFunctions クラスのメソッドでは NotSupportedException となり、名前空間: System.Data.Entity.SqlServer の SqlFunctions Class のメソッドを使う必要がありました。他の環境でどうかは分かりませんが注意してください。

もう一つは以下の例。これはコードのコメントにも書いてある通り delivery も test も Linq to Entities のクエリ式で、両方を合体して SQL に変換でき、foreach で DB に SQL を投げることができるので問題なく期待した結果が得られます。(Northwind の Order Details, Products テーブルから ADO.NET Entity Data Model を作成して使っています)

// これは Linq to Entities
var delivery = 
    from d in context.Order_Details
    group d by d.ProductID into g
    orderby g.Key
    select new
    {
       ItemCode = g.Key,
        Count = g.Sum(x => x.Quantity),
        SumAmount = g.Sum(x => x.UnitPrice * x.Quantity)
    };

// これも Linq to Entities
var test = from p in context.Products
           join d in delivery
           on p.ProductID equals d.ItemCode into dGroup
           from item in dGroup.DefaultIfEmpty()
           select new
           {
               ItemCode = p.ProductID,
               Name = p.ProductName,
               Count = item.Count,
               SumAmount = item.SumAmount
           };

// delivery を含めた test のコード全体を Linq to Entities と
// して SQL に変換することができ、foreach で DB に SQL を投げ
// ることができるので問題ない。
foreach (var x in test)
{
    Console.WriteLine(
      $"Name: {x.Name}, Count: {x.Count}, Sum: {x.SumAmount}");
}

上のような複雑なクエリ式が SQL に変換できるというのが驚きですが、それはちょっと置いといて、失敗事例はどういうことだったのかを書きます。

それは、DataTable から Linq to Objects のクエリ式を使って匿名型のオブジェクトのコレクションを取得し、それを Linq to Entities のクエリ式に組み合わせたことです。

具体的には、上のコードの delivery を以下のように DataTable(コードの table がそれ)から取得するようにしました。

// これは Linq to Object
var delivery = 
    from d in table.AsEnumerable()
    group d by d.Field<int>("ProductID") into g
    orderby g.Key
    select new
    {
        ItemCode = g.Key,
        Count = g.Sum(x => x.Field<Int16>("Quantity")),
        SumAmount = g.Sum(x => x.Field<decimal>("UnitPrice") * 
                               x.Field<Int16>("Quantity"))
    };

そうすると foreach のところで " System.NotSupportedException: Unable to create a constant value of type 'Anonymous type'. Only primitive types or enumeration types are supported in this context." というエラーになります。

エラーメッセージが前者の例とは違っていてため、最初、原因が分からなかったのですが、匿名型のオブジェクトのコレクションを Linq to Entities のクエリに組み込むと SQL に変換できないということが問題のようです。

なお、匿名型でなく、別にクラスとプロパティを定義し、それを初期化して各プロパティに代入するようにしても、上のエラーメッセージの 'Anonymous type' が定義したクラス名に変わるだけで同じエラーになります。

解決策は test のクエリ式も Linq to Objects にすることで、具体的には context.Products を context.Products.ToList() にすれば期待した結果が得られます。

Tags:

ADO.NET

VS2015 でユーザーインスタンス利用

by WebSurfer 24. September 2018 13:57

SQL Server Express のユーザーインスタンスを利用して、Visual Studio Community 2015 の TableAdapter 構成ウィザードで型付 DataSet + TableAdapter を自動生成する際の注意点を書いておきます。

Visual Studio 2010 Professional と同じ手順で作業すると、作業の過程で以下のように、"ファイル xxx.mdf の自動的に名前が付けられたデータベースをアタッチできませんでした。同じ名前のデータベースが既に存在するか、指定されたファイルを開けないか、UNC 共有に配置されています。" というエラーが出て先に進めません。

エラーメッセージ

原因はここまでの作業で生成された接続文字列に User Instance=True が付与されないからです。

接続文字列には AttachDbFilename でアタッチする .mdf ファイルは指定されるのですが、その場合 User Instance=True で明示的にユーザーインスタンスを使うようにする必要があるようです。

(想像ですが、User Instance=True が付与されていないので、既定の(または名前付き)インスタンスにアタッチしようとして、権限の問題で失敗していると思われます。先の記事「DB のアタッチ」参照)

TableAdapter 構成ウィザードで作業を進めていく際、「接続の追加」ダイアログで[詳細設定(V)...]ボタンをクリックすると「詳細プロパティ」ダイアログが表示されますので、そこでユーザーインスタンスを使用するように設定する必要があります。

詳細プロパティ

「詳細プロパティ」で[Data Source]を LocalDB から SQL Server Express に変更するところは気が付きましたが、もう一つ[User Instance]がデフォルトで False になっていて、これを True に変更する必要があることに気が付きませんでした。

ちなみに、Visual Studio 2010 Professional では「詳細プロパティ」の[User Instance]は True に設定されています。

「詳細プロパティ」の設定後、先に進んで TableAdapter 構成ウィザードに戻ったときに接続文字列を確認できます。

接続文字列

上の画像のように User Instance=True となっていれば OK です。後は従前の手順で進めていけば問題なく型付 DataSet + TableAdapter が完成するはずです。

ユーザーインスタンスはすでに非推奨になっていて、最近の開発には LocalDB を利用するので、このような問題に悩むケースはほとんどなさそうですが、忘れないよう備忘録として残しておくことにしました。(笑)

Tags: ,

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  April 2020  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar