け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト
ADO.net Entitiy Framework (以下、EF) と SQL Server Compact Edition (以下、SqlCe) は共に、Visual Studio 2008 SP1 からの強力な切り札なのですが、この二つの組み合わせには少々困った制限仕様があります。
つまり、「SqlCe の AutoIncrement が true になった列を持つテーブルは、EF において追加処理が出来ない」というものです。
この制限がある故に SqlCe は EF では使用しにくく、以前の仕組みである Linq for SQL ではそもそもサポートしておらず、旧来の Dataset を使った仕組みに終始してしまう形になっています。
が、実際にはあくまでも「追加処理中に非対応エラーが出る」だけなので、このあたりを自前の処理に差し替えてさえおけば、ほぼ他のデータベースと同様に使用することが出来ます。
(10/17追記) リレーションシップを考慮した修正コードを掲載したエントリを追加しました
続・EF と SqlCe で仕様上できない追加処理を差し替える
(以下、各種コード)
まずコードの前提として ObjectContext クラスから派生したクラス FooEntities と、EntityObject クラスから派生した Hoge 、Fuga があるとします。
ObjectContext が使用する Entity SQL ではサポートがされていない以上、SqlCe に生の SQL を直接送信する必要がありますので、まずはその処理を行うためのヘルパーメソッドを FooEntities に実装します。
public static R GetKey<T, R>(SqlCeConnection connect, T entity, string tableName, string keyName)
where T : EntityObject
{
SqlCeCommand cmd = connect.CreateCommand();
cmd.CommandText = String.Format(@"SELECT {1} FROM {0} ORDER BY {1} DESC", tableName, keyName);
return (R)cmd.ExecuteScalar();
}
public static R GetKey<T, R>(SqlCeConnection connect, T entity, string tableName)
where T : EntityObject
{ return GetKey<T, R>(connect, entity, tableName, "Id"); }
public static R GetKey<T, R>(SqlCeConnection connect, T entity)
where T : EntityObject
{ return GetKey<T, R>(connect, entity, typeof(T).Name); }
public static int GetKey<T>(SqlCeConnection connect, T entity)
where T : EntityObject
{ return GetKey<T, int>(connect, entity, typeof(T).Name); }
public static SqlCeParameter CreateNullOrValue<T, R>(string name, T param, Func<T, R> getter)
where T : class
{
return (param == null)
? new SqlCeParameter(name, DBNull.Value)
: new SqlCeParameter(name, getter(param));
}
public static SqlCeParameter CreateNullOrValue<T>(string name, T param)
where T : class
{ return CreateNullOrValue(name, param, x => x); }
public void SqlTransaction(Action<SqlCeConnection, SqlCeTransaction> func)
{
using (SqlCeConnection context = new SqlCeConnection(@"Data Source=" + this.Connection.DataSource))
{
context.Open();
using (SqlCeTransaction scope = context.BeginTransaction())
{
try { func(context, scope); }
catch (SqlCeException err)
{
MessageBox.Show(
String.Format("更新処理中に例外が発生しました。\n{0}\n更新処理はロールバックされます。", err.Message),
"更新エラー",
MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
scope.Rollback();
throw;
}
}
context.Close();
}
}
クラスメソッド2つ、インスタンスメソッド一つ。
軽く説明しますと、クラスメソッドの方は SqlCeCommand を実行する際に使用するヘルパーコードです。GetId は「最後に追加したレコードのキーを取得する」メソッドで、CreateNullOrValue は「null を許容するテーブル列に対し、DBNull.Value か有効な値かを選択してパラメータとして設定する」メソッドです。
インスタンスメソッドは、SqlCeConnection を生成してトランザクション処理を行うするためのメソッドです。ObjectContext が持つコネクションは EntityConnection 型で、これは SqlCeConnection と互換性がないため、データソース名だけもらって別に生成しています。
# ちなみに、EntityConnection#BeginTransaction を発行している(トランザクション処理中)状態でも、このコードが動いていることは確認しました。
# おそらくは EntityConection のトランザクション処理側で更新機能を使う前なら大丈夫だと思います。
次に、対応するテーブルに AutoIncrement = true の列があるエンティティ型に追加する処理です。
まず、処理がわかりやすいように この処理をインターフェイスとして定義します。
interface IEntityInsertHelper
{
void InsertItem(SqlCeConnection connection);
}
次に、このインターフェイスを追加処理の差し替えが必要な各エンティティ型に明示的に実装していきます。
普通に実装しても良いのですが、通常は必要としない処理なので出来るだけ見えないように実装する方が良いかと。
#region IEntityInsertHelper メンバ
void IEntityInsertHelper.InsertItem(SqlCeConnection connection)
{
var command = connection.CreateCommand();
command.CommandText = "INSERT INTO AddressGroup(AreaId, Name, Deleted, Apartments, Format, InputFormat, OldId, Created)" +
" VALUES (@area, @name, @deleted, @ap, @format, @input, @old, @created)";command.Parameters.Add(new SqlCeParameter("area", this.Area.Id));
command.Parameters.Add(new SqlCeParameter("name", this.Name));
command.Parameters.Add(new SqlCeParameter("deleted", this.Deleted));
command.Parameters.Add(new SqlCeParameter("ap", this.Apartments));
command.Parameters.Add(FooEntities.CreateNullOrValue("format", this.Format));
command.Parameters.Add(FooEntities.CreateNullOrValue("input", this.InputFormat));
command.Parameters.Add(FooEntities.CreateNullOrValue("old", this.NewestInfo, x => x.Id));
command.Parameters.Add(new SqlCeParameter("created", this.Created.Date));command.ExecuteNonQuery();
this.Id = FooEntities.GetKey(connection, this);
}#endregion
で、これが実際の追加処理になります。テーブルの列の内容に合わせて適時修正して下さい。
最初の4つと最後のパラメータが NULL 値を許容しない列で、AreaID はエンティティのリレーションシップを定義しています。
残りの3つのパラメータが NULL 値を許容する列で、最初に作ったヘルパーメソッドでパラメータに設定する値を選択しています。とくに NULL を許容するリレーションシップは、NULL 値かどうかを判断する要素と実際に使用する値が違うのでこういう表記になっています。
大まかに説明すると、INSERT SQL文を生成、パラメータを設定、実行。その後、AutoIncrement によって生成された Id 値を取得して設定しています。要は「最後に追加した行が自分だから、その Id をくれ」ということです。
そして肝心の差し替え処理です。
これは ObjectContext クラスの SavingChanged イベントハンドラを使って行います。
このイベントは、コンテキスト内の追加・変更・削除されたデータをデータベースに反映する直前に呼び出されるイベントで、ここで通常はデータの検証処理などを行うわけですが、今回はここでさらに一部の追加処理だけを自前でやってしまおうと言うことになります。
FooEntities() {
this.SavingChanges += ContextSavingChanges;
}private void ContextSavingChanges(object sender, EventArgs e)
{
SqlTransaction((c, t) =>
{
foreach (var entry in this.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
{
IEntityInsertHelper obj = (entry.Entity as IEntityInsertHelper);
if (obj != null)
{
obj.InsertItem(c);
this.Detach(obj);
(obj as EntityObject).EntityKey = this.CreateEntityKey(typeof(NewsPaperEntities).Name + "." + obj.GetType().Name, obj);
this.Attach(obj as IEntityWithKey);
}
}
});
}
まずはイベントハンドラをイベントに紐づけます。通常はコンストラクタにコードを追加したらいいと思います。
次にイベントハンドラですが、簡単に説明すると……
となります。独自に追加した後、対応するキーを再設定する訳です。EntityKeyプロパティは1度設定すると変更できないので、一端コンテキストから外すことでそれに対応しています。アタッチした状態では未変更状態になり、このイベントの後に続く本来の更新作業では無視されることになります。
実際にはこの追加処理の前にインスタンスに対する検証処理を行う必要がありますが、今回は省略しています。
これで、あとは必要な各エンティティ型に対して IEntityInsertHelper インターフェイス を実装するだけでうまく動作できると思います。
OVA『バカとテストと召喚獣 〜祭〜』上巻 [Blu-ray]
(メディアファクトリー)
新品 ¥ 4,409
中古 ¥ 3,680
OVA『バカとテストと召喚獣 〜祭〜』下巻 [Blu-ray]
(メディアファクトリー)
新品 ¥ 4,438
中古 ¥ 2,980
生徒会役員共DJCD「アニメ生徒会役員共が全部わかるラジオ、略して全ラ!...
(キングレコード)
新品 ¥ 2,253
中古 ¥ 1,850
WORKING!! 9 初回限定特装版 (SEコミックスプレミアム)
(高津 カリノ)
新品 ¥ 2,896
中古 ¥ 2,835
「声優グランプリ」公認!声優界<雀王>決定戦! <J-1グランプリ> V...
(オムニバス)
新品 ¥ 3,159
中古 ¥ 3,099
デッドライジング 2【CEROレーティング「Z」】
(カプコン)
新品 ¥ 2,740
中古 ¥ 1,019
スプリンターセル コンヴィクション
(ユービーアイ ソフト)
新品 ¥ 2,979
中古 ¥ 1,400
Xbox 360 250GB
(マイクロソフト)
中古 ¥ 25,600
学園黙示録 HIGHSCHOOL OF THE DEAD 7 オリジナル...
(佐藤 大輔)
新品 ¥ 9,800
中古 ¥ 4,879
学園黙示録 HIGHSCHOOL OF THE DEAD SECRET ...
(佐藤 大輔)
中古 ¥ 2,500
ソードアート・オンライン〈5〉ファントム・バレット (電撃文庫)
(川原 礫)
新品 ¥ 578
中古 ¥ 450
RPG W(・∀・)RLD6 ―ろーぷれ・わーるど― (富士見ファンタ...
(吉村 夜)
新品 ¥ 651
中古 ¥ 1
魔法少女リリカルなのはViVid (3)限定版 (角川コミックス・エース...
(藤真 拓哉)
新品 ¥ 1,680
中古 ¥ 1,280
魔法戦記リリカルなのはForce (3)限定版 (角川コミックス・エース...
(緋賀 ゆかり)
新品 ¥ 1,680
中古 ¥ 420
ダンジョンズ&ドラゴンズ第4版スターター・セット
(ジェームズ ワイアット)
新品 ¥ 4,980
中古 ¥ 4,311
アリアンロッド・サガ・リプレイ(5) 激闘のピースメイカー (富士見ド...
(菊池 たけし/F.E.A.R.)
新品 ¥ 693
中古 ¥ 190
アリアンロッド・サガ・コンチェルト 1 (電撃コミックス)
(佐々木 あかね)
新品 ¥ 599
中古 ¥ 90
堀さんと宮村くん(7)(ガンガンコミックスONLINE)
(HERO)
新品 ¥ 1,000
中古 ¥ 747
浅尾さんと倉田くん(3)(ガンガンコミックスONLINE)
(HERO)
新品 ¥ 690
中古 ¥ 376
7と嘘吐きオンライン―HERO個人作品集―(ガンガンコミックスONLIN...
(HERO)
新品 ¥ 710
中古 ¥ 169
ONE PIECE 65 (ジャンプコミックス)
(尾田 栄一郎)
NARUTO―ナルト― 59 (ジャンプコミックス)
(岸本 斉史)
ワンピース 海賊無双(通常版)(初回特典:オリジナルカスタムテーマ9種DLコード、ソーシャルゲーム専用レアフィギュア用コード同梱)
(バンダイナムコゲームス)
ドラゴンズドグマ(数量限定特典『バイオハザード6』体験版用DLコード同梱)
(カプコン)