by WebSurfer
2018年4月30日 16:31
以下のコードを実行すると foreach (var item in list2) のところで System.NotSupportedException がスローされ、
1 つの LINQ to Entities クエリに含まれる構造的に互換性のない 2 つの初期化に、型 'ConsoleAppJoinByLinq2.JoinedList' が指定されています。1 つの型を同じクエリ内の 2 つの場所で初期化することはできますが、両方の場所で同じプロパティが同じ順序で設定されている必要があります。
・・・というエラーメッセージが表示されます。その理由と解決策を書きます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleAppJoinByLinq2
{
public class JoinedList
{
public int ProductID { set; get; }
public string ProductName { get; set; }
public int CategoryID { get; set; }
public string CategoryName { set; get; }
}
class Program
{
static void Main(string[] args)
{
NORTHWINDEntities db = new NORTHWINDEntities();
var list1 = from c in db.Categories
where c.CategoryID == 1
select new JoinedList
{
CategoryID = c.CategoryID,
CategoryName = c.CategoryName
};
var list2 = from c in list1
join p in db.Products
on c.CategoryID equals p.CategoryID
select new JoinedList
{
ProductID = p.ProductID,
ProductName = p.ProductName,
CategoryName = c.CategoryName
};
foreach (var item in list2)
{
Console.WriteLine("{0}, {1}, {2}",
item.ProductID, item.ProductName, item.CategoryName);
}
}
}
}
上のコードでは、SQL Server のサンプルデータベース Northwind をベースに Visual Studio Communirt 2015 のウィザードを使って作った Entity Data Model (EDM) を使っています。以下の画像を見てください。
NORTHWINDEntities は EDM を作ると一緒に自動生成される DbContext クラスを継承したコンテキストクラスです。
Categories と Products は NORTHWINDEntities コンテキストクラスのプロパティで、データベースの当該テーブルを表すエンティティのコレクションを取得・設定するものです。
そして、肝心の話の何故エラーになるかの理由ですが、エラーメッセージの「両方の場所で同じプロパティが同じ順序で設定」という条件が満たされてない、即ち、list1 と list2 のクエリで JoinedList を初期化する際のプロパティの設定が異なるからです。
その前のエラーメッセージの条件「1 つの型を同じクエリ内の 2 つの場所で初期化」には該当しないように見えますが、list1 と list2 のクエリは両方 foreach のところで遅延評価されて、結局「同じクエリ内」ということになるようです。
解決策は、
-
list1 のクエリに ToList() を適用する(遅延評価されないように)、または、
-
JoinedList を初期化する際のプロパティの設定を、並び順序を含めて list1 / list2 のクエリで同じになるようにする
・・・です。そうすれば、以下の通り期待した結果が得られます。
9bdbf566-3a37-4e58-a35a-7d38e4f25d07|1|1.0
Tags: Linq
ADO.NET
by WebSurfer
2017年6月18日 18:02
Visual Studio のデータソース構成ウィザードを使って、@IT の記事「Microsoft Visual Studio 2005 による Web アプリケーション構築技法」の「D. テーブルアダプタへのクエリ追加」のセクションに書いてあるように、特定の条件で DataSet / DataTable を生成するメソッドを作ることができます。
以下の画像はサンプルデータベース Northwind の Orders テーブルから ShippedDate が NULL のレコードを抽出して DataGridView に表示したものですが、このようなことができるメソッドを作るにはどうすればよいかということを書きます。
SELECT クエリで WHERE ShippedDate = @ShippedDate という条件で TableAdapter にメソッドを追加すると、そのメソッドの引数に null が渡された場合は、メソッド内部でパラメータ @ShippedDate に DBNull.Value が代入されるコードが生成されます。
しかしながら、それでは ShippedDate が NULL のレコードを抽出することはできません。ShippedDate が NULL のレコードを抽出するためには WHERE ShippedDate IS NULL とする必要があります。
ではどうすればいいかですが、クエリビルダで元となる SELECT クエリを作成する際、@IsNull_ShippedDate(名前は任意)というような引数に null が渡されたか否かを判定するパラメータを追加し、以下の画像のように WHERE 句を組み立ててそれをベースにメソッドを生成します。
これにより、DataTable を返す GetDataByNullableShippedDate メソッド(名前は任意)の例ですが、以下のようなコードが生成されます。(注:自動生成されたコードそのものではなく、読みやすくするため改行等を行っています)
public virtual OrdersDataTable GetDataByNullableShippedDate(
int? IsNull_ShippedDate, DateTime? ShippedDate)
{
this.Adapter.SelectCommand = this.CommandCollection[1];
if (IsNull_ShippedDate.HasValue == true)
{
this.Adapter.SelectCommand.Parameters[0].Value =
(int)IsNull_ShippedDate.Value;
}
else
{
this.Adapter.SelectCommand.Parameters[0].Value =
DBNull.Value;
}
if (ShippedDate.HasValue == true)
{
this.Adapter.SelectCommand.Parameters[1].Value =
(DateTime)ShippedDate.Value;
}
else
{
this.Adapter.SelectCommand.Parameters[1].Value =
DBNull.Value;
}
OrdersDataTable dataTable = new OrdersDataTable();
this.Adapter.Fill(dataTable);
return dataTable;
}
この GetDataByNullableShippedDate メソッドを以下のように呼び出して DataTable を取得し DataGridView に表示したのが一番上の画像です。
public partial class Form14 : Form
{
private OrdersTableAdapter adapter = new OrdersTableAdapter();
private BindingSource bindingSource1 = new BindingSource();
private NorthwindDataSet.OrdersDataTable table;
public Form14()
{
InitializeComponent();
DateTime? shippedDate = null;
table = adapter.GetDataByNullableShippedDate(
shippedDate.HasValue ? 0 : 1, shippedDate);
bindingSource1.DataSource = table;
this.dataGridView1.DataSource = bindingSource1;
}
}
もちろん、NULL でない特定の日付の ShippedDate のレコードも上のコードで抽出できます。
by WebSurfer
2016年6月4日 15:12
パラメータ化クエリを使用するというセキュリティ対策として普通にやるべきことをやっていれば、照合順序の違いによる文字化けに悩むことはなさそうという話を書きます。
元は MSDN Forum の「nvarcharに日本語を入力すると?????????になる」という表題のスレッドでの話です。
上の MSDN Forum のスレッドの問題は、簡単に書くと、SQL Azure の照合順序のデフォルトは SQL_Latin1_General_CP1_CI_AS となっていて、それに例えば以下のように 'あいうえお' というように N プレフィックスをつけないリテラルを INSERT すると文字化けするという話です。
INSERT INTO [Table] ([Name]) VALUES ('あいうえお')
(何故文字化けするかは MSDN Blog の記事「Unicode型列(NCHAR/NVARCHAR) に格納されるデータが “?” になる」に説明されていますので、そちらを見てください。手抜きでスミマセン)
クエリをパラメータ化して ADO.NET + SqlClient 経由で INSERT, UPDATE をかければ、照合順序が Japanese_CI_AS(デフォルト)でも SQL_Latin1_General_CP1_CI_AS(SQL Azure のデフォルトらしい)でも文字化けはしません。
パラメータ化あり / なしでどう違うかの例を以下に書きます。
まず、サンプルとして SQL Server 2008 Express に照合順序が SQL_Latin1_General_CP1_CI_AS のデータベースを作りました。以下の画像の通りです。
それに以下のコードで INSERT してみます。上のクエリがパラメータ化なし、下のクエリがパラメータ化ありです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
string connString = @"接続文字列";
using(SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
string query =
"INSERT INTO [Table] ([Name]) VALUES ('あいうえお')";
using(SqlCommand cmd = new SqlCommand(query, conn))
{
cmd.ExecuteNonQuery();
}
query ="INSERT INTO [Table] ([Name]) VALUES (@Name)";
using(SqlCommand cmd = new SqlCommand(query, conn))
{
cmd.Parameters.Add(
new SqlParameter("@Name", SqlDbType.NVarChar, 50));
cmd.Parameters["@Name"].Value = "かきくけこ";
cmd.ExecuteNonQuery();
}
}
}
}
}
結果は以下の通りです。赤枠で囲ったものがパラメータ化なし、青枠で囲ったものがパラメータ化ありです。
何故パラメータ化クエリを使うと文字化けの問題がなくなるのかと言うと、TechNet の記事「第 4 回 アドホック クエリのパラメータ化」の下の方に書いてあるように "SqlParameter クラスを利用すると、内部的には sp_executesql に変換されて実行されるようになり"、 その際以下のように N プレフィックスが付与されるからです。
exec sp_executesql
N'INSERT INTO [Table] ([Name]) VALUES (@Name)',
N'@Name nvarchar(5)',
@Name=N'かきくけこ'
というわけで、クエリをパラメータ化して ADO.NET + SqlClient を使えば(普通のやり方をしていれば)、照合順序の違いによって文字化けに悩むことはなさそうです。