はじめに
この記事のエンティティクラスとは、Entity Framework Core
においてデータベースにマッピングされるクラスのことを指します。
Entity Framework Core 2.1
からエンティティクラスのプロパティを読み取り専用にできるようになりました。
これまで、Entity Framework Core
でエンティティクラスを定義する場合、以下のように書き込みもpublic
なプロパティが必要でした。
class Book {
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
Entity Framework Core 2.1
からは以下のようにセッターをprivate
にできるようになりました。
class Book {
public int Id { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public Book(int id, string title, string author) {
Id = id;
Title = title;
Author = author;
}
}
インスタンスを外部から書き換えられないので、初期化はプロパティ初期化子1でなくコンストラクターで明示的に行います。
プロパティが書き込み専用ならインスタンスの状態が不変になり、内部のデータを外部からの意図せぬ変更から守ることができます。
※ C# 6 から、コンストラクターでのみ値を代入可能なget
アクセサーだけのプロパティを定義できましたが、Entity Framework Core
ではprivate set
がないとマッピングが行われません。
※ ただし、OnModelCreating
のフックで明示的にマッピングするとprivate set
も不要になります。後半に書きます。
プロジェクトの作成
サンプルプロジェクトを作成しプロパティが読み取り専用なエンティティクラスを使用した CRUD 処理の説明したいと思います。
最終的なソースは以下のリポジトリにおきました。
https://github.com/sano-suguru/readonly-model-props
$ dotnet --version
2.1.403
# テンプレートは最低限のコンソールアプリを使用します。
$ dotnet new console -o "readonly-model-props"
$ cd ./readonly-model-props
# DBマイグレーションを省略するためインメモリ DB を使用します。
$ dotnet add package "Microsoft.EntityFrameworkCore.InMemory"
エンティティクラス
エンティティクラスは 「はじめに」 と同じ書籍エンティティを使用します。
クラス外部からはプロパティが読み取り専用になっています。
class Book {
public int Id { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public Book(int id, string title, string author) {
Id = id;
Title = title;
Author = author;
}
}
データベースコンテキスト
データベースはマイグレーションを省略するためインメモリデータベースを使います。
今回は最初からシードデータを持たせておきます。
using Microsoft.EntityFrameworkCore;
class InMemoryDbContext : DbContext {
public DbSet<Book> Books { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.UseInMemoryDatabase(databaseName: "InMemoryDatabase");
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<Book>().HasData(
new Book(id: 1, title: "Flutter入門", author: "掌田 津耶乃"),
new Book(id: 2, title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
new Book(id: 3, title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
);
}
プロパティが読み取り専用の CRUD 処理
プロパティが読み取り専用の場合、インスタンスに対しbook.Title = "FooBar"
のように書き換えることができません。
そのため、通常のEntity Framework Core
の CRUD 処理と勝手が変わってくる部分があります。
抽出クエリ(READ
)
プロパティが読み取り専用でも特別な処理はありません。
using (var context = new InMemoryDbContext()) {
var books = context.Books.AsNoTracking().AsEnumerable();
foreach (var book in books) {
Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
}
}
データベースのデータがすべて抽出されました。
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
※ AsNoTracking
について
上記ではIQueryable<Book>
に対しAsNoTracking
付いています。
通常、インスタンスは Entity Framework Core
によって追跡され、エンティティ内で検出された変更はすべてSaveChanges
メソッドを呼んだときデータベースに保存されます。
AsNoTracking
によって追跡されない場合、変更追跡の情報を設定する必要がないのでクエリが高速化されます。
新規作成クエリ(CREATE
)
プロパティ初期化子が使えないので、Book
インスタンスの初期値はコンストラクタに渡されます。
using (var context = new InMemoryDbContext()) {
var newbook = new Book(
id: 4,
title: "C#プログラミングのイディオム/定石&パターン",
author: "出井 秀行"
);
context.Books.Add(newbook);
// 結果の確認
foreach (var book in context.Books.AsNoTracking()) {
Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
}
}
データベースに書籍が追加されているのが確認できます。
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
4 C#プログラミングのイディオム/定石&パターン 出井 秀行
更新クエリ(UPDATE
)
データベースから抽出した更新対象のインスタンスを直接書き換えることができないため、少し特殊です。
新規にBook
のインスタンスを作成し、その際に更新するプロパティ以外を更新対象のプロパティで初期化します。
作成したインスタンスで更新をかけますが、Entity Framework Core
ではId
は自動で主キーになっているため、一致するId
のデータを更新されます。
using (var context = new InMemoryDbContext()) {
var target = context.Books
.AsNoTracking()
.Single(b => b.Id == 3);
context.Books.Update(
new Book(
id: target.Id,
title: target.Title,
author: "今井 勝信"
)
);
context.SaveChanges();
// 更新の確認
var updated = context.Books.AsNoTracking().Single(b => b.Id == 3);
Console.WriteLine($"{updated.Id} {updated.Title} {updated.Author}");
}
対象(Id == 3
が)のデータが更新されていることが確認できます。
3 IntelliJ IDEAハンズオン 今井 勝信 # 元は 山本 裕介
削除クエリ(DELETE
)
抽出クエリと同様、プロパティが読み取り専用でも特別な処理はありません。
using (var context = new InMemoryDbContext()) {
var target = context.Books.Single(b => b.Id == 1);
context.Books.Remove(target);
context.SaveChanges();
foreach (var book in context.Books.AsNoTracking()) {
Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
}
}
ID == 1
のデータがデータベースから削除されているのが確認できます。
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 今井 勝信
4 C#プログラミングのイディオム/定石&パターン 出井 秀行
プロパティを内部からも読み取り専用にする
public ~ { get; private set }
の場合、クラス内部からはデータを書き換えることが可能でしたが、get
のみの場合はコンストラクタでの初期化のみ可能です。
「はじめに」で触れたprivate set
もなくget
アクセサのみプロパティを持つエンティティクラスを定義したいと思います。
まず、エンティティクラスのプロパティからprivate set
を除きます。
class Book {
- public int Id { get; private set; }
- public string Title { get; private set; }
- public string Author { get; private set; }
+ public int Id { get; }
+ public string Title { get; }
+ public string Author { get; }
public Book(int id, string title, string author) {
Id = id;
Title = title;
Author = author;
}
}
このままではEntity Framework Core
でマッピングが行われないため、
データベースコンテキストのOnModelCreating
で明示的にプロパティのマッピングを定義します。
class InMemoryDbContext : DbContext {
public DbSet<Book> Books { get; set; }
- protected override void OnModelCreating(ModelBuilder modelBuilder) =>
+ protected override void OnModelCreating(ModelBuilder modelBuilder) {
+ modelBuilder.Entity<Book>(book => {
+ book.HasKey(nameof(Book.Id));
+ book.Property(e => e.Title);
+ book.Property(e => e.Author);
+ });
modelBuilder.Entity<Book>().HasData(
new Book(id: 1, title: "Flutter入門", author: "掌田 津耶乃"),
new Book(id: 2, title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
new Book(id: 3, title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
);
+ }
}
これでエンティティクラスのプロパティをget
のみにすることができました。
Id
をコンストラクタから取り除く
今回、エンティティクラスではコンストラクタの引数にId
を初期値として渡せるようにしました。
本来Entity Framework Core
ではId
は自動でインクリメントされ採番されます。
自動で採番されるID
にはコンストラクタから値を渡す必要がありません。
class Book {
public int Id { get; private set; } // private な setter を定義(EF Coreに使われる)
public string Title { get; }
public string Author { get; }
public Book(string title, string author) { // コンストラクタの引数に id を渡さない
Title = title;
Author = author;
}
}
DbContext.OnModelCreating
ではID
のマッピングの定義は不要です。
(エンティティクラス側でprivate set
があるため)
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<Book>(book => {
// book.HasKey(nameof(Book.Id)); 不要
book.Property(e => e.Title);
book.Property(e => e.Author);
});
ID
が自動採番されているのを確認します。
using (var context = new InMemoryDbContext()) {
Book[] newbooks = {
new Book(title: "Flutter入門", author: "掌田 津耶乃"),
new Book(title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
new Book(title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
};
context.Books.AddRange(newbooks);
context.SaveChanges();
foreach (var book in context.Books.AsNoTracking()) {
Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
}
}
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
ただし、Id
をコンストラクタから取り除いた場合、OnModelCreating
のフックでシードデータを設定することはできません。
modelBuilder.Entity<TEntity>().HasData
でシードデータ生成する際、ID
は自動採番されないためです。
Unhandled Exception: System.InvalidOperationException:
The seed entity for entity type 'Book' cannot be added because there was no value provided for the required property 'Id'.
ASP.NET Core MVC
やWeb API
では以下のようにProgram.cs
でシードデータを採番するコードを自分で書くとよいと思います。
public static void Main(string[] args) {
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope()) {
var provider = scope.ServiceProvider;
try {
var context = provider.GetRequiredService<InMemoryDbContext>();
context.Books.AddRange(new[] {
new Book(title: "Flutter入門", author: "掌田 津耶乃"),
new Book(title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
});
Context.SaveChanges();
} catch (Exception ex) {
var logger = provider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "データベース初期化中にエラーが発生しました。");
}
}
host.Run();
}
-
セッターも
public
ならnew Book { Id = 1, Title = "Scala本", Author = "ゴンタロウ" }
のように初期化できる。 ↩