【C#】Entity Framework Coreでテーブルのデータを更新する

目次

概要

Entity Framework Core (EF Core) を使用して、データベース内の既存レコードを変更し、確定(コミット)する基本的な実装パターンです。 EF Core の「変更追跡(Change Tracking)」機能を利用し、オブジェクトのプロパティを書き換えて SaveChangesAsync を呼ぶだけで、自動的に適切な UPDATE SQLが発行されます。

仕様(入出力)

  • 入力: 更新対象の特定条件(例: 商品名)、変更後の新しい値。
  • 出力: 更新処理の結果(更新前後の値をコンソール表示)。
  • 前提: .NET 6.0以上、Microsoft.EntityFrameworkCore.InMemory(動作確認用)。

基本の使い方

  1. データを取得する(追跡状態にする)。
  2. プロパティを変更する。
  3. SaveChangesAsync で変更をデータベースに反映する。
// 1. 取得(Firstなどを使う)
var product = await context.Products
    .FirstAsync(p => p.Name == "Old Product");

// 2. 変更(値を変えるだけ)
product.Price = 1200;
product.LastUpdated = DateTime.Now;

// 3. 保存(変更があった箇所のみUPDATE文が発行される)
await context.SaveChangesAsync();

コード全文

ここでは「商品データの価格改定」を行うシナリオです。 動作確認のためインメモリデータベースを使用しています。

外部ライブラリとして Microsoft.EntityFrameworkCore.InMemory が必要です。

dotnet add package Microsoft.EntityFrameworkCore.InMemory
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

public class Program
{
    public static async Task Main()
    {
        var options = new DbContextOptionsBuilder<ShopDbContext>()
            .UseInMemoryDatabase("ShopDb")
            .Options;

        // --- 1. 初期データの準備 ---
        using (var context = new ShopDbContext(options))
        {
            if (!await context.Products.AnyAsync())
            {
                context.Products.Add(new Product 
                { 
                    Name = "Standard Plan", 
                    Price = 1000, 
                    LastUpdated = DateTime.Now.AddDays(-10) 
                });
                await context.SaveChangesAsync();
            }
        }

        // --- 2. データの更新処理 ---
        using (var context = new ShopDbContext(options))
        {
            var worker = new ProductManager(context);
            // "Standard Plan" の価格を 1500 に変更
            await worker.UpdateProductPriceAsync("Standard Plan", 1500);
        }

        // --- 3. 結果の確認 ---
        using (var context = new ShopDbContext(options))
        {
            var product = await context.Products.FirstAsync(p => p.Name == "Standard Plan");
            Console.WriteLine($"[確認] 商品名: {product.Name}");
            Console.WriteLine($"       価格: {product.Price}");
            Console.WriteLine($"       更新日時: {product.LastUpdated}");
        }
    }
}

// 業務ロジッククラス
public class ProductManager
{
    private readonly ShopDbContext _context;

    public ProductManager(ShopDbContext context)
    {
        _context = context;
    }

    public async Task UpdateProductPriceAsync(string productName, decimal newPrice)
    {
        // データを取得(デフォルトで変更追跡が有効)
        var product = await _context.Products
            .FirstOrDefaultAsync(p => p.Name == productName);

        if (product == null)
        {
            Console.WriteLine($"エラー: 商品 '{productName}' は見つかりませんでした。");
            return;
        }

        Console.WriteLine($"[更新前] {product.Name}: {product.Price}円 ({product.LastUpdated})");

        // プロパティを変更
        product.Price = newPrice;
        product.LastUpdated = DateTime.Now;

        // データベースへ反映
        // 変更されたプロパティのみがUPDATE文に含まれます
        int affectedRows = await _context.SaveChangesAsync();

        Console.WriteLine($"[更新完了] {affectedRows}件のデータを保存しました。");
    }
}

// エンティティ定義
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public DateTime LastUpdated { get; set; }
}

// DbContext定義
public class ShopDbContext : DbContext
{
    public ShopDbContext(DbContextOptions<ShopDbContext> options)
        : base(options) { }

    public DbSet<Product> Products => Set<Product>();
}

実行結果例

[更新前] Standard Plan: 1000円 (2025/01/01 10:00:00)
[更新完了] 1件のデータを保存しました。
[確認] 商品名: Standard Plan
       価格: 1500
       更新日時: 2025/01/11 10:00:00

カスタムポイント

  • 更新日時の自動化: エンティティのプロパティ変更で手動セットする代わりに、DbContextSaveChanges をオーバーライドして、変更されたエンティティの LastUpdated を一律で現在時刻にする手法も一般的です。
  • 部分更新: EF Core は変更された列だけを UPDATE 文に含めます。全ての列を更新したいわけではない場合も、このコードのままで最適化されています。
  • Attachの使用: Web APIなどで、画面から受け取ったオブジェクト(DBから読み込んでいない状態)を更新したい場合は、context.Products.Attach(product) を使い、EntityState.Modified を設定するパターンを使用します。

注意点

  1. 取得漏れ: FirstOrDefaultAsync は見つからない場合に null を返します。そのままプロパティにアクセスすると NullReferenceException になるため、必ずnullチェックを行ってください。
  2. SaveChangesAsyncの呼び忘れ: プロパティを変えただけではデータベースには反映されません。必ず SaveChangesAsync を呼び出してください。
  3. 同時実行制御(Concurrency): 取得してから保存するまでの間に、別のユーザーが同じデータを書き換えている可能性があります。厳密な制御が必要な場合は、[Timestamp] 属性(行バージョン)を使用し、DbUpdateConcurrencyException をハンドリングする必要があります。

応用

EF Core 7.0以降では、データをメモリに読み込まずにデータベース上で直接一括更新する ExecuteUpdateAsync が使用可能です。大量データの更新で非常に高速です。

// 取得処理(SELECT)を行わず、条件に一致する全行を直接UPDATEする
await context.Products
    .Where(p => p.Price < 1000)
    .ExecuteUpdateAsync(s => s
        .SetProperty(p => p.Price, p => p.Price * 1.1m) // 10%値上げ
        .SetProperty(p => p.LastUpdated, DateTime.Now));

まとめ

パフォーマンスを最優先する一括更新では、EF Core 7以降の ExecuteUpdateAsync の利用を検討してください。

通常の実装では「取得」→「書き換え」→「SaveChangesAsync」の3ステップが基本です。

EF Coreの変更追跡機能により、差分のみが効率的にSQL化されます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次