【C#】EF Coreでデータを読み込まずに更新する方法(Attach / Entry)

目次

概要

Entity Framework Core (EF Core) で、データベースからレコードを取得(SELECT)せずに、更新(UPDATE)だけを実行するパフォーマンス重視の実装パターンです。 主キー(ID)が既知の場合、Attach メソッドを使用してエンティティを追跡状態にし、変更したいプロパティのみを更新することで、データベースへの往復回数を減らします。

仕様(入出力)

  • 入力: 更新対象のID、新しい値(例:メールアドレス)。
  • 出力: 更新処理の完了。
  • 前提: .NET 6.0以上、Microsoft.EntityFrameworkCore.InMemory(動作確認用)。
  • 動作: 該当IDのレコードが存在すれば更新し、存在しない場合は例外(DbUpdateConcurrencyException)が発生する可能性があります。

基本の使い方

オブジェクトを new してIDを設定し、Attach でコンテキストに接続してから値を変更します。

// 1. 空のオブジェクトを作成し、IDだけセットする(スタブ)
var stub = new Member { Id = targetId };

// 2. コンテキストにアタッチ(追跡開始)
// この時点では Unchanged 状態
context.Members.Attach(stub);

// 3. プロパティを変更
// 変更されたプロパティのみが Modified としてマークされる
stub.Email = "new_address@example.com";

// 4. 保存(変更された列のみ UPDATE SQL が発行される)
await context.SaveChangesAsync();

コード全文

ここでは「会員(Member)」のメールアドレスを、データベースから読み込まずにID指定で更新するコンソールアプリケーションを提示します。

外部ライブラリとして 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<AppDbContext>()
            .UseInMemoryDatabase("MemberDb")
            .Options;

        // --- 1. 初期データの準備 ---
        using (var context = new AppDbContext(options))
        {
            if (!await context.Members.AnyAsync())
            {
                context.Members.Add(new Member 
                { 
                    Id = 101, 
                    Name = "Alice", 
                    Email = "alice@old.com", 
                    Points = 500 
                });
                await context.SaveChangesAsync();
            }
        }

        // --- 2. 読み込まずに更新を実行 ---
        using (var context = new AppDbContext(options))
        {
            var worker = new DisconnectedUpdater(context);
            
            // ID:101 のメールアドレスのみを変更
            // 名前やポイントは取得しないため上書きされず維持される
            await worker.UpdateEmailWithoutLoadingAsync(101, "alice@new-domain.com");
        }

        // --- 3. 結果の確認 ---
        using (var context = new AppDbContext(options))
        {
            var member = await context.Members.FindAsync(101);
            Console.WriteLine($"[確認] ID: {member.Id}");
            Console.WriteLine($"       Name: {member.Name}");
            Console.WriteLine($"       Email: {member.Email}"); // ここが変わっているはず
            Console.WriteLine($"       Points: {member.Points}");
        }
    }
}

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

    public DisconnectedUpdater(AppDbContext context)
    {
        _context = context;
    }

    public async Task UpdateEmailWithoutLoadingAsync(int memberId, string newEmail)
    {
        // データベースへの問い合わせ(SELECT)は行わない
        // 更新対象のIDを持つ「スタブ(身代わり)」オブジェクトを作成
        var stub = new Member 
        { 
            Id = memberId 
        };

        try
        {
            // Attachメソッドで追跡を開始
            // EF CoreはこのオブジェクトがDBに存在するとみなして監視する
            _context.Members.Attach(stub);

            // プロパティを変更
            // ここで変更した箇所だけが UPDATE 文の SET 句に含まれる
            stub.Email = newEmail;

            // 保存
            await _context.SaveChangesAsync();
            
            Console.WriteLine($"ID {memberId} の更新に成功しました。");
        }
        catch (DbUpdateConcurrencyException)
        {
            // 指定したIDがDBに存在しない場合に発生
            Console.WriteLine($"エラー: ID {memberId} は存在しないか、削除されています。");
        }
    }
}

// エンティティ定義
public class Member
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public int Points { get; set; }
}

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

    public DbSet<Member> Members => Set<Member>();
}

実行結果例

ID 101 の更新に成功しました。
[確認] ID: 101
       Name: Alice
       Email: alice@new-domain.com
       Points: 500

カスタムポイント

  • 全列更新を行いたい場合: 特定のプロパティだけでなく、オブジェクト全体を強制的に「変更あり」としてマークし、全列をUPDATEしたい場合は Entry を使用します。C#var entity = new Member { Id = 1, Name = "New Name", Email = "New Email" ... }; // 全プロパティを更新対象にする(未設定のプロパティはnullや初期値で上書きされるので注意) context.Entry(entity).State = EntityState.Modified;
  • オプティミスティック同時実行制御: 行バージョン(Timestamp)列がある場合、スタブオブジェクトにもそのバージョンを設定することで、競合検知を行うことができます。

注意点

  1. データの存在: Attach はデータベースにそのIDが存在することを前提とします。もしIDが存在しない場合、SaveChangesAsync の実行時に DbUpdateConcurrencyException が発生します。
  2. 未初期化プロパティのリスク: Attach パターン(部分更新)ではなく State = EntityState.Modified(全更新)を使う場合、オブジェクトにセットしていないプロパティ(nullや0)もそのままDBに保存されてしまいます。意図しないデータ消失に注意してください。
  3. コンテキスト内の重複: 同じ DbContext インスタンス内で、既にそのIDのデータをロード済みの場合、Attach しようとすると「同じキーを持つインスタンスが既に追跡されている」という例外が発生します。短命なコンテキスト(Unit of Work)を使用するか、ChangeTracker.Clear() で追跡をリセットする必要があります。

応用

EF Core 7.0以降を使用している場合、ExecuteUpdateAsync メソッドを使用するのが最も現代的で高速です。これはオブジェクトの追跡すら行わず、直接SQLを発行します。

// EF Core 7.0+ の場合(最速)
await context.Members
    .Where(m => m.Id == 101)
    .ExecuteUpdateAsync(s => s
        .SetProperty(m => m.Email, "alice@new-domain.com"));

まとめ

  • Attach を使用することで、SELECTクエリを省略し、UPDATEのみを効率的に発行できます。
  • IDと変更したい値だけが手元にある「Web APIの更新処理」などで非常に役立ちます。
  • 全列更新(State = Modified)と部分更新(Attach + プロパティ変更)を使い分けて、意図しないデータの上書きを防いでください。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

目次