vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Entity Framework Core で色々な SQL を投げてみる 3

はじめに

ここまで SQL ゼロからはじめるデータベース操作 を参考に、色々な SELECT 文を投げてみました。

本当はこの後 CROSS JOIN やウインドウ関数があるのですが、どうやら標準では用意されていない様子。

データを分析するウインドウ関数については Raw SQL で実行するにせよ C# の処理として実行するにせよ、追いかけてみたいとは思います。

が、まずは CRUD の残りの Create(Insert)、 Update 、 Delete を試してみたいと思います。

EntityState

Insert 、 Update 、 Delete はどれも DB の値を変更する処理なわけですが、これが実行されるには次のステップを踏むことになります。

  1. EntityState の値を変更する
  2. SaveChangesAsync (SaveChanges) で 1. を反映する

1.については直接そのようなコードを書くのではなく、 Add (AddRange) 、 Update (UpdateRange) 、 Remove (RemoveRange) を実行したときに自動で変更されます。
(現在の EntityState の内容を直接見たり変更したりすることはできないようです)

EntityState の種類はこの辺りを参照。

この後見ていきますが、レコードの追加や削除は、 Add 、 Remove と List に対して行うのとほぼ同じ処理でできるのですが、 SaveChangesAsync を実行しないと DB には反映されないため、注意が必要です。

実行する

ともあれ、まずはあれこれ実行してみますよ。

[INSERT] レコードの追加

レコードの追加から。

コード

var newBook = new Book
{
    BookId = 996,
    AuthorId = 0,
    Name = "hello"
};
_context.Add(newBook);

await _context.SaveChangesAsync();

発行される SQL

INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
     VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);

[UPDATE] レコードの更新

コード

Book addedBook = await _context.Book.FirstAsync(b => b.BookId == 996);
addedBook.Genre = "Comic";
_context.Update(addedBook);

await _context.SaveChangesAsync();

発行される SQL

SELECT b."BookId", b."AuthorId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail"
      FROM "Book" AS b
      WHERE b."BookId" = 996
      LIMIT 1

UPDATE "Book" SET "AuthorId" = @p0, "Genre" = @p1, "Name" = @p2, "Price" = @p3, "ReleaseDate" = @p4, "Thumbnail" = @p5
     WHERE "BookId" = @p6;

[DELETE] レコードの削除

コード

Book addedBook = await _context.Book.FirstAsync(b => b.BookId == 996);

_context.Remove(addedBook);

await _context.SaveChangesAsync();

発行される SQL

SELECT b."BookId", b."AuthorId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail"
      FROM "Book" AS b
      WHERE b."BookId" = 996
      LIMIT 1

DELETE FROM "Book"
WHERE "BookId" = @p0;

Key が無い場合

UPDATE 、 DELETE はそれぞれ主キーである BookId で対象のレコードを検索していました。

では主キーが無い場合はどうなるのか。

というと、 DbContext (EfCoreNpgsqlSampleContext) の初期化時に InvalidOperationException が発生します。

理由は主キーが見つからないから。
怖い。。。

テーブルで主キーが設定されていることがわかっている場合は問題ありませんが、そうでない場合は Model クラスの主キーとなる要素に [Key] 属性を持たせる必要があります。

Author2.cs ( テーブルでは主キーを未設定)
using System.ComponentModel.DataAnnotations;

namespace EfCoreNpgsqlSample.Models
{
    public class Author2
    {
        // 属性を外すとエラー
        [Key]
        public int AuthorId { get; set; }
        public string Name { get; set; }
    }
}

もちろんテーブルに主キーが設定されている場合も [Key] 属性を付けても問題ないため、主キーの設定有無によらず必ず付けてあげるのが良い気がします。

組み合わせ

先ほど触れた通り、 CREATE や UPDATE は SaveChangesAsync が実行されて初めて実行されます。

ということは、 EntityState を変更する処理を複数実行し、まとめて反映することも可能なのでは?

ということでやってみました。

AddRange でまとめて登録

組み合わせではありませんが、 Add には、 AddRange のような複数データをまとめて追加するメソッドが用意されています ( Update 、 Remove も同様)。

これを使った場合、要素数SQL が発行されるのでしょうか。
それとも、別の方法でデータが追加されるのでしょうか。

コード

List< Book> books = new List< Book>
{
    new Book
    {
        BookId = 996,
        AuthorId = 0,
        Name = "hello"
    },
    new Book
    {
        BookId = 995,
        AuthorId = 0,
        Name = "hello1"
    },
};
await _context.AddRangeAsync(books);
await _context.SaveChangesAsync();

発行される SQL

INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);
INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p7, @p8, @p9, @p10, @p11, @p12, @p13);

シンプルに 2 回 INSERT が実行されました。

Add を複数回実行してから反映する

コード

_context.Add(newBook);
_context.Add(newBook2);

await _context.SaveChangesAsync();

発行される SQL

INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);
INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p7, @p8, @p9, @p10, @p11, @p12, @p13);

結果としては AddRange と同じ内容となりました。

Add したデータを反映前に Remove する

コード

_context.Add(newBook);

_context.Remove(newBook);

await _context.SaveChangesAsync();

INSERT も DELETE も実行はされませんでした。

Add したデータを反映前に Update する

コード

_context.Add(newBook);

_context.Update(newBook);

await _context.SaveChangesAsync();

似たような発想で DB 反映前に Update を実行してみたところ、 UPDATE 対象のレコードが見つからないとエラーになりました。

SQL 実行時に(実行順ではない)優先順位が付けられており、 CREATE より UPDATE が先に実行されたようです。

これは主キーの重複を避けるためだと思われます。

トランザクション

DB の値を変更する処理では、処理の途中でエラーが発生した場合などに、変更が中途半端に反映されたりしないようトランザクションを使ってDBへの反映をコントロールします。

Entity Framework Core では、 DbContext が持つ Database でトランザクションがコントロールされています。

using (IDbContextTransaction transaction = _context.Database.BeginTransaction())
{
    try
    {
        await _context.AddRangeAsync(books);
        await _context.SaveChangesAsync();
    
        // DB に変更が反映される.
        transaction.Commit();
    }
    catch (Exception e)
    {
        // BeginTransaction より前の状態に巻き戻される.
        transaction.Rollback();
    }
}

この例だとほぼトランザクションの意味がないわけですが、複数のテーブルの値を更新する場合などに効力を発揮します。

Commit しない場合

Commit しない場合、 SQL 自体は発行されるのでしょうか。

コード

using (IDbContextTransaction transaction = _context.Database.BeginTransaction())
{
    try
    {
        await _context.AddRangeAsync(books);
        await _context.SaveChangesAsync();
    
        // DB に変更が反映される.
        //transaction.Commit();
    }
    catch (Exception e)
    {
        // BeginTransaction より前の状態に巻き戻される.
        transaction.Rollback();
    }
}

発行される SQL

INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);
INSERT INTO "Book" ("BookId", "AuthorId", "Genre", "Name", "Price", "ReleaseDate", "Thumbnail")
VALUES (@p7, @p8, @p9, @p10, @p11, @p12, @p13);

SQL は発行されました。が、 DB には値が追加されませんでした。

おわりに

だいぶ端折りはしたものの、 Entity Framework Core を使った簡単な DB 操作は何となく触れられたんじゃないかな~、と思います。

Linq 的に書けたり、 BeginTransaction が using でスコープ設定できるところなど、 C# 的に DB 操作が書ける、というのはやはり便利ですね。

次はいよいよ DbContext …と言いたいところなのですが、諸事情により ASP.NET Core のログイン・ログアウトを色々試してみることにする予定です。