プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2018/01/30

Entity Framework Core におけるデータの保存

update2020/09/16 event_note2018/01/30 0:35

Entity Framework Core におけるデータの基本的な保存方法と、エンティティ同士が関連している場合のデータの保存方法についてです。

公式サイトに詳しい解説があるので、ほとんど訳しただけになってしまいました・・・。

訳が微妙なので、原文を読まれたほうが良いかもしれません。

環境

  • Visual Studio 2017
  • .NET Core 2.0
  • Entity Framework Core 2.0

扱うモデル

公式サイトと同じ、以下のモデルを基本的な例とします。
(場合によっては別の例が挙げられていますが・・・)

// Blog is the principal entity
public class Blog
{
    // Blog.BlogId is the principal key
    // (in this case it is a primary key rather than an alternate key)
    public int BlogId { get; set; }
    public string Url { get; set; }

    // Blog.Posts is a collection navigation property
    public List<Post> Posts { get; set; }
}

// Post is the dependent entity
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
 
    // Post.BlogId is the foreign key
    public int BlogId { get; set; }
    // Post.Blog is a reference navigation property
    // Post.Blog is the inverse navigation property of Blog.Posts (and vice versa)
    public Blog Blog { get; set; }
}

基本操作

データの追加

DbSet.Add メソッドを使用して、エンティティクラスの新しいインスタンスを追加します。
SaveChanges を呼び出すと、データがデータベースに挿入されます。

using (var context = new BloggingContext())
{
    var blog = new Blog { Url = "http://sample.com" };
    context.Blogs.Add(blog);
    context.SaveChanges();

    Console.WriteLine(blog.BlogId + ": " +  blog.Url);
}

データの更新

EF は、コンテキストによって追跡される既存のエンティティに対する変更を自動的に検出します。
これには、データベースから取得または照会するエンティティ、および以前にデータベースに追加および保存されたエンティティが含まれます。

従って、プロパティに割り当てられた値を変更したら、SaveChanges を呼び出すだけでデータを更新できます。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.First();
    blog.Url = "http://sample.com/blog";
    context.SaveChanges();
}

ここで、以下のようにインスタンスを丸ごと差し替えてしまうとデータの更新は反映されないので、やってはいけません。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.First();
    // これだとデータは更新されない
    blog.Url = new Blog { Url = "http://sample.com" };
    context.SaveChanges();
}

必ずプロパティ毎にデータをセットする必要があります。

データの削除

エンティティクラスのインスタンスを削除するには、DbSet.Remove メソッドを使用します。

エンティティがデータベースに存在する場合、SaveChanges の実行中に削除されます。
エンティティがまだデータベースに保存されていない(追加されたとして追跡されている)場合、そのエンティティはコンテキストから削除され、SaveChanges が呼び出されると挿入されなくなります。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.First();
    context.Blogs.Remove(blog);
    context.SaveChanges();
}

1つの SaveChanges での複数の操作を行う

複数の Add Update Remove の操作を1つの SaveChanges に対して組み合わせることもできます。

尚、ほとんどのデータベースプロバイダーにおいて、SaveChanges はトランザクショナルです。
これは、すべての操作が成功または失敗のどちらかであり、操作が部分的に適用されることはないということを意味します。

using (var context = new BloggingContext())
{
    // seeding database
    context.Blogs.Add(new Blog { Url = "http://sample.com/blog" });
    context.Blogs.Add(new Blog { Url = "http://sample.com/another_blog" });
    context.SaveChanges();
}

using (var context = new BloggingContext())
{
    // add
    context.Blogs.Add(new Blog { Url = "http://sample.com/blog_one" });
    context.Blogs.Add(new Blog { Url = "http://sample.com/blog_two" });

    // update
    var firstBlog = context.Blogs.First();
    firstBlog.Url = "";

    // remove
    var lastBlog = context.Blogs.Last();
    context.Blogs.Remove(lastBlog);

    context.SaveChanges();
}

関連データの保存

リレーションシップについては以下の記事に書いているので、そちらを参照してください。

新規作成

英語では Adding a graph of new entities と書かれていました。

親のテーブルに新規にレコードを作成し、その子となるテーブルにもレコードを作成することを、エンティティのグラフを追加すると表現しているようですね。

以下がコードの例です。

using (var context = new BloggingContext())
{
    var blog = new Blog
    {
        Url = "http://blogs.msdn.com/dotnet",
        Posts = new List<Post>
        {
            new Post { Title = "Intro to C#" },
            new Post { Title = "Intro to VB.NET" },
            new Post { Title = "Intro to F#" }
        }
    };

    context.Blogs.Add(blog);
    context.SaveChanges();
}

この場合、Blogs のテーブルと Posts のテーブルにそれぞれデータが挿入されます。
Blog クラスには Posts という Navigation property があるため、Blogs テーブルと Posts テーブルのリレーションシップは EF Core によって自動的に解決されます。

関連するエンティティの追加

Navigation property に新規にエンティティを追加すると、関連するテーブルに自動的にレコードが追加されます。

以下がコード例です。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Include(b => b.Posts).First();
    var post = new Post { Title = "Intro to EF Core" };

    blog.Posts.Add(post);
    context.SaveChanges();
}

データベースからフェッチされた Blog エンティティの Posts プロパティに新しくデータを追加しています。 この結果、データベースの Posts テーブルにもレコードが追加されます。

リレーションシップの変更

エンティティの Navigation property を変更すると、対応する変更がデータベースの外部キー列に対して行われます。

次の例では、post エンティティは新しい blog エンティティに属するように更新されます。
Navigation property である post.Blog に新しい blog エンティティをセットしているからです。

また、blog はデータベースにも挿入されることに注意してください。
blog はすでにコンテキスト(post)によって追跡されているエンティティの Navigation property から参照されている新しいエンティティであるためです。

using (var context = new BloggingContext())
{
    var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };
    var post = context.Posts.First();

    post.Blog = blog;
    context.SaveChanges();
}

リレーションシップの削除

リレーションシップを削除するには、Navigation propery を null に設定するか、関連するエンティティを Navigation propery のコレクションからから削除します。

リレーションシップを削除すると、そのリレーションシップで設定されたカスケード削除動作に従って、エンティティの依存関係に副作用が生じる可能性があります。

デフォルトでは、必要な関係に従ってカスケード削除動作が設定され、子エンティティと依存エンティティがデータベースから削除されます。
そのリレーションシップがオプションの関係の場合、カスケード削除はデフォルトでは設定されていませんが、外部キーのプロパティは null に設定されます。

リレーションシップの必要性の設定方法については、Required and Optional Relationships を参照してください。

カスケード削除動作の仕組み、それを明示的に設定する方法、EF の規則によってどのように選択されるかの詳細については、Cascade Delete を参照してください。

次の例では、カスケード削除が BlogPost の関係に対して設定されているので、post エンティティはデータベースから削除されます。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Include(b => b.Posts).First();
    var post = blog.Posts.First();

    blog.Posts.Remove(post);
    context.SaveChanges();
}