EF CORE 7 の新機能 - ExecuteDelete と ExecuteUpdate を使用したバルク操作

EF CORE 7 の新機能 - ExecuteDelete と ExecuteUpdate を使用したバルク操作

Entity Framework 7 には、以前から要望のあった人気機能がいくつか含まれています。その一つがバルク操作です。

最終更新 2022/09/10 22:55
tim_deschryver
読了目安 5 分
カテゴリ
EF Core
タグ
.NET C# EF Core ORM

原文リンク:https://timdeschryver.dev/blog/new-in-entity-framework-7-bulk-operations-with-executedelete-and-executeupdate

原文著者:tim_deschryver

翻訳:砂漠の果ての狼(Google翻訳による補助)

Entity Framework 7 には、長らく要望されていた人気機能がいくつか含まれています。その一つが一括操作です。Julie Lerman のツイートが目に留まり、実際に試してみることにしました。

ツイート URL:https://twitter.com/julielerman/status/1557743067691569156

https://twitter.com/julielerman/status/1557743067691569156

なぜ?

さて、既にエンティティを更新・削除できるのに、なぜこの機能が必要なのでしょうか?ここでのキーワードは「パフォーマンス」です。これは EF の新バージョンで常に上位に位置するテーマであり、今回も例外ではありません。

追加されたメソッドは、複数の側面でパフォーマンスを向上させます。従来はまずエンティティを取得してすべてをメモリに保存し、それから操作を実行してから SQL にコミットしていました。今ではたった一つの操作でそれを実現でき、一つの SQL コマンドが生成されます。

コードでどのように見えるか見てみましょう。

シナリオの設定

例を詳しく見る前に、まず SQL データベースを構成し、3つのテーブルを投入します。

  • Persons: 人
  • Addresses: 住所(人は1つの住所を持つ)
  • Pets: ペット(人は複数のペットを飼うことができる)
using Microsoft.EntityFrameworkCore;

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);
}

static void SetupAndPopulate(NewInEFContext context)
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();
    context.Persons.AddRange(Enumerable.Range(1, 1_000).Select(i =>
    {
        return new Person
        {
            FirstName = $"{nameof(Person.FirstName)}-{i}",
            LastName = $"{nameof(Person.LastName)}-{i}",
            Address = new Address
            {
                Street = $"{nameof(Address.Street)}-{i}",
            },
            Pets = Enumerable.Range(1, 3).Select(i2 =>
            {
                return new Pet
                {
                    Breed = $"{nameof(Pet.Breed)}-{i}-{i2}",
                    Name = $"{nameof(Pet.Name)}-{i}-{i2}",
                };
            }).ToList()
        };
    }));

    context.SaveChanges();
}

public class NewInEFContext : DbContext
{
    public DbSet<Person> Persons { get; set; }
    public DbSet<Pet> Pets { get; set; }
    public DbSet<Address> Addresses { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options
            .UseSqlServer("Connectionstring");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Address>()
           .Property<long>("PersonId");

        modelBuilder.Entity<Pet>()
            .Property<long>("PersonId");
    }
}

public class Person
{
    public long PersonId { get; set; }
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public Address? Address { get; set; }
    public List<Pet> Pets { get; set; } = new List<Pet>();
}

public class Address
{
    public long AddressId { get; set; }
    public string Street { get; set; } = "";
}

public class Pet
{
    public long PetId { get; set; }
    public string Breed { get; set; } = "";
    public string Name { get; set; } = "";
}

ExecuteDelete と ExecuteDeleteAsync

これで準備が整いましたので、ExecuteDeleteExecuteDeleteAsync について詳しく見ていきましょう。

エンティティのセットを一括削除するには、Where メソッドを使用して削除するエンティティをフィルタリングします(以前と同様)。次に、ExecuteDelete メソッドを呼び出してエンティティのコレクションを削除します。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    context.Pets
           .Where(p => p.Name.Contains("1"))
           .ExecuteDelete();
}

生成される SQL ステートメントも見てみましょう。

DELETE FROM [p]
FROM [Pets] AS [p]
WHERE [p].[Name] LIKE N'%1%'

ご覧の通り、条件に一致するエンティティを削除する単一の SQL ステートメントが生成されます。これらのエンティティはメモリに保存されることもありません。シンプルで効率的です!

カスケード削除

別の例を見てみましょう。住所やペットの参照を持つ人を削除します。人を削除することで、削除ステートメントが外部テーブルにカスケードされるため、住所とペットも削除されます。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    context.Persons
           .Where(p => p.PersonId <= 500)
           .ExecuteDelete();
}

これにより、以下の SQL ステートメントが生成されます。

DELETE FROM [p]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(500 AS bigint)

影響を受けた行数

削除操作が何行に影響を与えたかを確認することもできます。ExecuteDelete は影響を受けた行数を返します。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    var personsDeleted =
        context.Persons
           .Where(p => p.PersonId <= 100)
           .ExecuteDelete();
}

上記の式では、personsDeleted 変数は 100 になります。

ExecuteUpdate と ExecuteUpdateAsync

エンティティの削除方法がわかったので、次は更新方法を探ってみましょう。ExecuteDelete と同様に、最初に更新したいエンティティをフィルタリングし、次に ExecuteUpdate を呼び出します。

エンティティを更新するには、新しい SetProperty メソッドを使用します。SetProperty の最初の引数は、更新するプロパティをラムダ式で選択し、2 番目の引数も同様にラムダ式でそのプロパティの新しい値を指定します。

例として、人の姓を "Updated" に設定してみましょう。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    context.Persons
           .Where(p => p.PersonId <= 1_000)
           .ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated"));
}

これにより、対応する SQL ステートメントが生成されます。

UPDATE [p]
    SET [p].[LastName] = N'Updated'
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)

また、エンティティの値にアクセスして新しい値を作成することもできます。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    context.Persons
           .Where(p => p.PersonId <= 1_000)
           .ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated" + x.LastName));
}

以下の SQL ステートメントが生成されます。

UPDATE [p]
    SET [p].[LastName] = N'Updated' + [p].[LastName]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)

複数の値を一度に更新

SetProperty を複数回呼び出すことで、一度に複数のプロパティを更新することもできます。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    context.Persons
           .Where(p => p.PersonId <= 1_000)
           .ExecuteUpdate(p =>
                p.SetProperty(x => x.LastName, x => "Updated" + x.LastName)
                 .SetProperty(x => x.FirstName, x => "Updated" + x.FirstName));
}

再度、対応する SQL ステートメントです。

UPDATE [p]
    SET [p].[FirstName] = N'Updated' + [p].[FirstName],
    [p].[LastName] = N'Updated' + [p].[LastName]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)

影響を受けた行数

ExecuteDelete と同様に、ExecuteUpdate も影響を受けた行数を返します。

using (var context = new NewInEFContext())
{
    SetupAndPopulate(context);

    var personsUpdated =
        context.Persons
           .Where(p => p.PersonId <= 1_000)
           .ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated"));
}

入れ子になったエンティティの更新はサポートされていないことに注意してください。

Entity Framework 7 のその他の更新

新機能の完全なリストについては、EF 7 計画を参照してください。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2022/06/02

EF Core 6 新機能まとめ(一)

この記事では、EF Core 6の10の新機能をご紹介します。新しい属性アノテーション、テンポラルテーブル、スパースカラムのサポートなど、その他の新機能が含まれます。

続きを読む