WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

EF でレコードの削除

by WebSurfer 2015年12月21日 16:34
2016/9/13 書換
間違いや新事実を見つけて部分的な書き換えや追記を行っているうちに意味がよく分からない記事になってしまったので、内容を整理して全面的に書き換えました。

Entity Framework Code First の機能を利用して SQL Server データーベースに作った親子関係のあるテーブルで、レコードの削除を行った際にハマって悩んだ話を書きます。

以下のコードは、Microsoft のチュートリアル「新しいデータベースの Code First」に記載されていたクラス定義ですが、これをそのままサンプルとして使用して説明します。

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
} 

上記のコードをベースに、チュートリアルの手順通りに SQL Server 2008 Express にデータベースを作ると以下の画像のテーブルとフィールドが生成されます。上に親子関係のあるテーブルと書きましたが、Blogs が親、Posts が子です。「Code First の規約 (Code First Conventions)」に従い、Posts テーブルに BlogId という名前の外部キーフィールドが生成され、NULL 不可になっているところに注目してください。

生成された DB

これに上の画像に示すデータを追加した後、例えば以下のようなコードで、コンテキストから一つの親(Blog オブジェクト)を取得し、その中の子のコレクション(List<Post>)から最初の要素を削除した後、SaveChanges メソッドでデータベースに結果を反映しようとしたとします。(Posts テーブルの中の PostId が 4 のレコードを削除しようと試みたということです)

class Program
{
  static void Main(string[] args)
  {
    using (var db = new BloggingContext())
    {
      var b = db.Blogs.Single(i => i.BlogId == 3);
      b.Posts.Remove(b.Posts[0]);
      db.SaveChanges();
    }
  }
}

そうすると、db.SaveChanges(); で InvalidOperationException がスローされます。エラーメッセージは以下のようになります。

"The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted."

上のエラーメッセージが何を言っているかを簡単に言うと「Posts テーブルの BlogId フィールドは NULL 不可だが、それを NULL にしようとして失敗した」ということです。

つまり、上記のコードは Posts テーブルの当該レコードを DELETE するのではなく、当該レコードの外部キーフィールド BlogId を NULL にしようとします。結果、BlogId フィールドは NULL 不可なので失敗します。

コードを少し変更(b.Posts.Remove... の b を db 変更)して以下のようにすると削除に成功します。上の画像の Posts テーブルのレコード一覧で赤枠で囲った部分が下のコードの実行結果ですが、元あった PostId が 7 のレコードが削除されています。

class Program
{
  static void Main(string[] args)
  {
    using (var db = new BloggingContext())
    {
      var b = db.Blogs.Single(i => i.BlogId == 4);
      db.Posts.Remove(b.Posts[0]);
      db.SaveChanges();
    }
  }
}

ハマったのはこの違いです。b と db で何が違うのでしょうか? 以下にその説明を書きます。

前者のコード b.Posts.Remove(b.Posts[0]);

b は Blog クラスのオブジェクトです。詳しく言うと、BloggingContext オブジェクトから Blogs プロパティを使って DbSet<Blog> オブジェクト(親のコレクション)を取得し、その中から BlogId == 3 の条件で取得した Blog オブジェクトです。

なので、前者のコードの意味は、Blog オブジェクトから Posts プロパティを使って List<Post>(外部キーで関連付けられた Post オブジェクトのコレクション)を取得し、それから b.Posts[0] に該当する子を削除するということになります。

つまり、親子の関係を絶つという指示を出しただけで、データベースの Posts テーブルから該当するレコードを削除していいとは誰も言ってないです。

親子の関係を絶つというのは、データベース上では Posts テーブルの当該レコードの外部キーフィールド BlodId を NULL に設定することに相当し、db.SaveChanges() でその操作が行われたということのようです。

なお、外部キーフィールド BlodId を NULL に設定する動きになると言っても、b.Posts[0] の BlogId プロパティが null に書き換えられるわけではないので注意してください(元の値のまま変わりません)。

コード上では、(1) 親が保持する子のコレクション List<Post> から b.Posts[0] に該当する子が外され、(2) b.Posts[0] に該当するエンティティの EntityState が Unchanged から Modified に変わるのみです。

結果からの想像ですが、フレームワークは上の (1), (2) を見て、db.SaveChanges() でデータベースの Posts テーブルの当該レコードの外部キーフィールド BlodId を NULL に設定しようとするようです。

後者のコード db.Posts.Remove(b.Posts[0]);

前者のコードからは、b(Blog オブジェクト)が db(BloggingContext オブジェクト)に変わっている点に注意してください。

なので、後者のコードの意味は、BloggingContext オブジェクト の Posts プロパティを使って DbSet<Post> オブジェクト(子エンティティのコレクション)を取得し、それから b.Posts[0] に該当する子エンティティを削除するということになります。

データベース上では Posts テーブルの当該レコードを削除することに相当するので、当該子エンティティの EntityState は Deleted に設定され、db.SaveChanges() で当該レコードは削除されます。


Posts テーブルの BlogId フィールドが NULL 不可になるのは Post クラスの外部キープロパティ BlogId を int 型にしたからです(クラスに定義されるプロパティが null にできない型の場合��、Code First の規約に従って、データベースのフィールドも NULL 不可になります)。

Microsoft の文書「Code First の規約」によると、外部キーフィールドの NULL 可 / 不可によって DELETE 操作の結果に以下の違いがあるそうです。

"依存エンティティの外部キーで null 値が許容されない場合、Code First はリレーションシップに連鎖削除を設定します。依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます"

上の例では Posts テーブルの BlogId フィールドは NULL 不可なので cascade delete が設定され、例えば以下のようなコードで親を DELETE すると関連する子も DELETE されます。

Blog b = db.Blogs.Single(i => i.BlogId == 6);
db.Blogs.Remove(b);
db.SaveChanges();

Post クラスの外部キープロパティ BlogId を int? 型(null 可)に変更して、Code First の機能を使って Posts テーブルを作ると、Code First の規約に従って、外部キーフィールド BlogId は NULL 可になります。

そのようにして作成したデータベースに対しては、外部キーフィールドBlogId は NULL 可なので当然ですが、上の「前者のコード」でエラーにならず、Posts テーブルの当該レコードの BlogId は NULL に設定されます。

「後者のコード」では Posts テーブルの当該レコードは削除されます。

一つ分からないのが、Microsoft の文書に "依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます" と書いてあったのに、実際に試したらそうなならなかった点です。

子を持つ親を削除しようとしたら FK 制約に引っかかって SqlException がスローされました。子があるのに先に親を削除しようとしたようです。なぜ Microsoft の文書と違うのか理由は不明です。

Tags:

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar