前回までは、Entity Framework Core の役割、基本部品、クエリ、マイグレーションを見てきました。
今回からは、もう少し実践的なデータ操作へ進みます。
まずは レコードの追加 です。
EF Core では、オブジェクトを作り、コンテキストに追加し、SaveChanges() を呼び出すことで INSERT が実行されます。
ただし、単一レコード、複数レコード、関連オブジェクト、既存データとの関連付けでは、少しずつ考え方が違います。
サンプルモデル
この記事では、車の在庫と注文を例にします。
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 List<Order> Orders { get; set; } = new();
}
public sealed class Order
{
public int Id { get; set; }
public int CarId { get; set; }
public string CustomerName { get; set; } = "";
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
public Car? Car { get; set; }
}
コンテキストです。
using Microsoft.EntityFrameworkCore;
public sealed class AutoLotContext : DbContext
{
public AutoLotContext(DbContextOptions<AutoLotContext> options)
: base(options)
{
}
public DbSet<Car> Cars => Set<Car>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.ToTable("Inventory")
.HasMany(car => car.Orders)
.WithOne(order => order.Car)
.HasForeignKey(order => order.CarId);
modelBuilder.Entity<Order>()
.ToTable("Orders");
}
}
エンティティ状態を確認する
EF Core は、各エンティティに状態を持たせています。
追加処理で重要なのは Added です。
Car car = new Car
{
Make = "Honda",
Color = "Blue",
PetName = "Vega"
};
Console.WriteLine(context.Entry(car).State);
context.Cars.Add(car);
Console.WriteLine(context.Entry(car).State);
Add() する前は、コンテキストに追跡されていない状態です。
Add() すると Added になり、SaveChanges() 時に INSERT 対象になります。
await context.SaveChangesAsync();
Console.WriteLine(context.Entry(car).State);
Console.WriteLine(car.Id);
保存後は Unchanged になり、自動採番された Id も反映されます。
単一レコードを追加する
もっとも基本的な追加処理です。
await using AutoLotContext context = CreateContext();
Car car = new Car
{
Make = "Toyota",
Color = "Black",
PetName = "Night"
};
context.Cars.Add(car);
await context.SaveChangesAsync();
Console.WriteLine($"追加された ID: {car.Id}");
Add() は、対象エンティティを追加予定として登録します。
実際にデータベースへ INSERT が送られるのは SaveChangesAsync() のタイミングです。
Attach との違い
Attach() は、「このオブジェクトは既にデータベースに存在している」とコンテキストへ伝えるために使います。
Car existing = new Car
{
Id = 1,
Make = "Honda",
Color = "Blue",
PetName = "Vega"
};
context.Cars.Attach(existing);
Console.WriteLine(context.Entry(existing).State);
通常、状態は Unchanged になります。
つまり SaveChanges() を呼んでも INSERT はされません。
新規追加したいなら Add()、既存データとして追跡させたいなら Attach() です。
複数レコードをまとめて追加する
複数件を追加する場合は、AddRange() が便利です。
Car[] cars =
{
new Car { Make = "Ford", Color = "Red", PetName = "Rusty" },
new Car { Make = "BMW", Color = "Silver", PetName = "Silver Fox" },
new Car { Make = "Mazda", Color = "White", PetName = "Snow" }
};
context.Cars.AddRange(cars);
await context.SaveChangesAsync();
EF Core は、追加されたエンティティをまとめて処理します。
ただし、大量データの投入では、EF Core の通常追加より一括投入向けの手段を検討した方がよい場合もあります。
自動採番列の扱い
SQL Server の IDENTITY 列のように、データベース側で主キーが生成される場合、追加前の Id は通常 0 です。
Car car = new Car
{
Make = "Nissan",
Color = "Green",
PetName = "Leafy"
};
Console.WriteLine(car.Id); // 0
context.Cars.Add(car);
await context.SaveChangesAsync();
Console.WriteLine(car.Id); // データベースで生成された値
SaveChanges() 後に、生成されたキーがエンティティへ反映されます。
この仕組みにより、追加した親エンティティの ID を使って関連データを扱えます。
オブジェクトグラフを追加する
関連するオブジェクトをまとめて追加できます。
Car car = new Car
{
Make = "Subaru",
Color = "Blue",
PetName = "Sky",
Orders =
{
new Order { CustomerName = "Alice" },
new Order { CustomerName = "Bob" }
}
};
context.Cars.Add(car);
await context.SaveChangesAsync();
Car と Order がまとめて Added になり、保存時に関連を保ったまま INSERT されます。
ただし、既存エンティティと新規エンティティが混ざる場合は注意が必要です。
既存データを誤って Added にすると、重複 INSERT しようとして失敗します。
既存の車に注文を追加する
既存の車へ新しい注文を追加する例です。
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
if (car == null)
{
return;
}
car.Orders.Add(new Order
{
CustomerName = "Charlie"
});
await context.SaveChangesAsync();
追跡中の Car に対して Order を追加しているため、EF Core は新しい注文だけを INSERT します。
外部キーだけで追加することもできます。
Order order = new Order
{
CarId = 1,
CustomerName = "Diana"
};
context.Orders.Add(order);
await context.SaveChangesAsync();
単に関連付けるだけなら、外部キーを直接指定する方がシンプルな場合もあります。
多対多のデータを追加する
多対多の例として、車と装備を考えます。
public sealed class Feature
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<Car> Cars { get; set; } = new();
}
Car にもコレクションを追加します。
public List<Feature> Features { get; set; } = new();
設定です。
modelBuilder.Entity<Car>()
.HasMany(car => car.Features)
.WithMany(feature => feature.Cars);
新しい車と新しい装備をまとめて追加します。
Car car = new Car
{
Make = "Tesla",
Color = "White",
PetName = "Spark",
Features =
{
new Feature { Name = "Navigation" },
new Feature { Name = "Autopilot" }
}
};
context.Cars.Add(car);
await context.SaveChangesAsync();
EF Core は中間テーブルのデータも生成します。
既存の装備を関連付ける場合は、既存データを取得または Attach() してから関連付けます。
Feature navigation = await context.Set<Feature>()
.SingleAsync(feature => feature.Name == "Navigation");
Car car = new Car
{
Make = "Audi",
Color = "Gray",
PetName = "Cloud"
};
car.Features.Add(navigation);
context.Cars.Add(car);
await context.SaveChangesAsync();
サンプルデータを投入する
学習用や開発用にサンプルデータを投入する関数を作っておくと便利です。
static async Task SeedAsync(AutoLotContext context)
{
if (await context.Cars.AnyAsync())
{
return;
}
context.Cars.AddRange(
new Car { Make = "Honda", Color = "Blue", PetName = "Vega" },
new Car { Make = "Ford", Color = "Red", PetName = "Rusty" },
new Car { Make = "Toyota", Color = "Black", PetName = "Night" });
await context.SaveChangesAsync();
}
AnyAsync() で既にデータがあるか確認し、重複投入を避けています。
サンプルデータを削除する
開発中にデータをクリアしたい場合もあります。
static async Task ClearSampleDataAsync(AutoLotContext context)
{
context.Orders.RemoveRange(context.Orders);
context.Cars.RemoveRange(context.Cars);
await context.SaveChangesAsync();
}
外部キー制約があるため、子テーブルから削除するのが安全です。
本番データに対して実行しないよう、開発専用の処理として分けておきましょう。
まとめ
EF Core でレコードを追加する基本は、エンティティを作り、Add() または AddRange() し、SaveChanges() を呼ぶことです。
ただし、既存データを扱う Attach()、自動採番、関連オブジェクト、多対多の関連付けでは、エンティティ状態を意識する必要があります。
次回は、追加したデータをさまざまな形で問い合わせる方法を見ていきます。