Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



[2009年09月29日]

EF と SqlCe で仕様上できない追加処理を差し替える

2009年10月17日 22:36更新 筆者: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 で仕様上できない追加処理を差し替える

(以下、各種コード)

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);
      }
    }
  });
}

まずはイベントハンドラをイベントに紐づけます。通常はコンストラクタにコードを追加したらいいと思います。
次にイベントハンドラですが、簡単に説明すると……

  1. トランザクション処理開始
  2. ObjectStateManager から追加処理を要するエンティティを取得、列挙
  3. エンティティのうち、 IEntityInsertHelper インターフェイスを持つものだけに処理を続行
  4. 独自の追加処理を実行( Id などの一意的なキーを内部で設定)
  5. そのインスタンスをコンテキストから一時削除する(インスタンスの EntityKey が null になる)
  6. インスタンスに、改めて EntityKey を生成、設定する
  7. 再びコンテキストに登録(リレーションシップは自動的に接続される、EntityStateは未変更状態になる)

となります。独自に追加した後、対応するキーを再設定する訳です。EntityKeyプロパティは1度設定すると変更できないので、一端コンテキストから外すことでそれに対応しています。アタッチした状態では未変更状態になり、このイベントの後に続く本来の更新作業では無視されることになります。

実際にはこの追加処理の前にインスタンスに対する検証処理を行う必要がありますが、今回は省略しています。

これで、あとは必要な各エンティティ型に対して IEntityInsertHelper インターフェイス を実装するだけでうまく動作できると思います。

本日のリンク元
その他のリンク元
検索