前回は、関連データの読み込み方法を見ました。
今回は、EF Core でデータを更新・削除する方法を整理します。
更新や削除では、エンティティがコンテキストに追跡されているかどうかが重要です。
追跡中エンティティを更新する
もっとも自然な更新方法は、データを取得し、プロパティを変更し、保存する流れです。
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
if (car == null)
{
return;
}
car.PetName = "New Vega";
await context.SaveChangesAsync();
この場合、car はコンテキストに追跡されています。
EF Core は変更を検知し、SaveChangesAsync() 時に UPDATE を実行します。
エンティティ状態を確認する
Car? car = await context.Cars.FirstOrDefaultAsync(car => car.Id == 1);
if (car != null)
{
Console.WriteLine(context.Entry(car).State);
car.PetName = "Updated";
Console.WriteLine(context.Entry(car).State);
}
取得直後は Unchanged、変更後は Modified になります。
ただし、変更検知のタイミングは設定や呼び出しによって変わることがあります。
状態を確実に反映したい場合は、DetectChanges() が関係します。
context.ChangeTracker.DetectChanges();
通常のアプリケーションコードでは、明示的に呼ばなくてもよい場面が多いです。
非追跡エンティティを更新する
Web API などでは、画面やリクエストから受け取ったオブジェクトを更新したいことがあります。
この場合、そのオブジェクトは現在のコンテキストに追跡されていません。
public sealed class CarUpdateRequest
{
public int Id { get; set; }
public string Make { get; set; } = "";
public string Color { get; set; } = "";
public string PetName { get; set; } = "";
}
単純に全項目を更新するなら、Update() を使えます。
static async Task UpdateCarAsync(
AutoLotContext context,
CarUpdateRequest request)
{
Car car = new Car
{
Id = request.Id,
Make = request.Make,
Color = request.Color,
PetName = request.PetName
};
context.Cars.Update(car);
await context.SaveChangesAsync();
}
Update() は、エンティティ全体を更新対象として扱います。
部分更新には注意が必要です。
安全に部分更新する
既存データを取得してから、変更してよいプロパティだけ更新する方法です。
static async Task RenameCarAsync(
AutoLotContext context,
int id,
string petName)
{
Car? car = await context.Cars.FindAsync(id);
if (car == null)
{
return;
}
car.PetName = petName;
await context.SaveChangesAsync();
}
この形は SQL が 1 回増える可能性がありますが、意図しない列の上書きを避けやすくなります。
API の更新処理では、「クライアントから来た値を全部信用して全列更新する」より、安全な場合が多いです。
プロパティ単位で更新対象にする
取得せずに特定プロパティだけ更新対象にする方法もあります。
Car car = new Car
{
Id = 1,
PetName = "Only Name Updated"
};
context.Cars.Attach(car);
context.Entry(car)
.Property(car => car.PetName)
.IsModified = true;
await context.SaveChangesAsync();
この方法では、PetName だけが更新対象になります。
ただし、コードは少し低レベルになるため、扱いには注意が必要です。
追跡中エンティティを削除する
削除の基本は、取得して Remove() することです。
Car? car = await context.Cars.FindAsync(1);
if (car == null)
{
return;
}
context.Cars.Remove(car);
await context.SaveChangesAsync();
Remove() すると状態は Deleted になります。
SaveChangesAsync() 時に DELETE が実行されます。
非追跡エンティティを削除する
ID だけ分かっている場合、スタブエンティティを作って削除できます。
Car car = new Car
{
Id = 1
};
context.Cars.Attach(car);
context.Cars.Remove(car);
await context.SaveChangesAsync();
この方法は、削除前にデータを取得しないため効率的です。
ただし、対象が存在しない場合や外部キー制約に違反する場合は、保存時に問題が起きます。
複数削除
検索してからまとめて削除する例です。
List<Car> oldCars = await context.Cars
.Where(car => car.Make == "OldMaker")
.ToListAsync();
context.Cars.RemoveRange(oldCars);
await context.SaveChangesAsync();
この方法は、対象を一度読み込みます。
大量削除では、EF Core の一括更新/削除機能や生 SQL も検討します。
EF Core 7 以降では、条件に一致する行を直接削除できます。
await context.Cars
.Where(car => car.Make == "OldMaker")
.ExecuteDeleteAsync();
これはエンティティを読み込まずに DELETE を実行します。
変更追跡を通らないため、用途を理解して使いましょう。
カスケード削除の失敗に注意する
関連データがある親を削除するとき、外部キー制約で失敗することがあります。
Car? car = await context.Cars.FindAsync(1);
if (car != null)
{
context.Cars.Remove(car);
await context.SaveChangesAsync();
}
Orders が残っていて、削除ルールが制限になっている場合、保存時に例外が発生します。
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
Console.WriteLine("削除に失敗しました。関連データが残っている可能性があります。");
Console.WriteLine(ex.Message);
}
関連データをどう扱うかは設計で決める必要があります。
- 子データも一緒に削除する
- 子データがある場合は親を削除できない
- 外部キーを null にする
- 論理削除にする
業務データでは、物理削除より論理削除が向いている場合も多いです。
論理削除という選択肢
削除フラグを持たせる設計です。
public sealed class Car
{
public int Id { get; set; }
public string Make { get; set; } = "";
public string Color { get; set; } = "";
public string PetName { get; set; } = "";
public bool IsDeleted { get; set; }
}
削除処理では、実際にはフラグを立てます。
Car? car = await context.Cars.FindAsync(id);
if (car != null)
{
car.IsDeleted = true;
await context.SaveChangesAsync();
}
論理削除は、監査や復元が必要な業務システムでよく使われます。
次の記事で見るグローバルフィルターと組み合わせると、通常の検索から削除済みデータを自動的に除外できます。
まとめ
EF Core の更新と削除では、エンティティが追跡されているかどうかが重要です。
追跡中なら、プロパティ変更や Remove() をして SaveChanges() するだけで SQL が生成されます。
非追跡エンティティでは、Attach()、Update()、プロパティ単位の変更指定などを使い分けます。
削除では外部キー制約やカスケード削除にも注意が必要です。
次回は、グローバルフィルター、生 SQL、投影、データベース生成値など、EF Core の便利な機能を見ていきます。