演習 - データを操作する
前の演習では、エンティティ クラスとデータベース コンテキストを作成しました。 その後、EF Core の移行機能を使ってデータベース スキーマを作成しました。
この演習では、PizzaService
の実装を完了します。 このサービスでは、EF Core を使って、データベースに対する CRUD 操作を実行します。
CRUD 操作のコードを作成する
PizzaService
の実装を完了するには、Services\PizzaService.cs で次の手順のようにします。
例で示されているように次の変更を行います。
using ContosoPizza.Data;
ディレクティブを追加します。using Microsoft.EntityFrameworkCore;
ディレクティブを追加します。- コンストラクターの前に、
PizzaContext
のクラス レベルのフィールドを追加します。 - コンストラクター メソッドのシグネチャを、
PizzaContext
パラメーターを受け取るように変更します。 - コンストラクター メソッドのコードを、パラメーターをフィールドに代入するように変更します。
using ContosoPizza.Models; using ContosoPizza.Data; using Microsoft.EntityFrameworkCore; namespace ContosoPizza.Services; public class PizzaService { private readonly PizzaContext _context; public PizzaService(PizzaContext context) { _context = context; } /// ... /// CRUD operations removed for brevity /// ... }
前に Program.cs に追加した
AddSqlite
メソッドの呼び出しにより、依存関係挿入にPizzaContext
が登録されました。PizzaService
インスタンスが作成されるときに、PizzaContext
がコンストラクターに挿入されます。GetAll
メソッドを次のコードに置き換えます。public IEnumerable<Pizza> GetAll() { return _context.Pizzas .AsNoTracking() .ToList(); }
上のコードでは以下の操作が行われます。
Pizzas
コレクションには、ピザ テーブルのすべての行が含まれます。AsNoTracking
拡張メソッドは、変更の追跡を無効にするよう EF Core に指示します。 この操作は読み取り専用なので、AsNoTracking
を指定することでパフォーマンスを最適にできます。- すべてのピザは、
ToList
で返されます。
GetById
メソッドを次のコードに置き換えます。public Pizza? GetById(int id) { return _context.Pizzas .Include(p => p.Toppings) .Include(p => p.Sauce) .AsNoTracking() .SingleOrDefault(p => p.Id == id); }
上のコードでは以下の操作が行われます。
Create
メソッドを次のコードに置き換えます。public Pizza Create(Pizza newPizza) { _context.Pizzas.Add(newPizza); _context.SaveChanges(); return newPizza; }
上のコードでは以下の操作が行われます。
newPizza
は有効なオブジェクトと見なされます。 EF Core ではデータの検証が行われないため、ASP.NET Core ランタイムまたはユーザー コードですべての検証を処理する必要があります。Add
メソッドは、newPizza
エンティティを EF Core のオブジェクト グラフに追加します。SaveChanges
メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
AddTopping
メソッドを次のコードに置き換えます。public void AddTopping(int pizzaId, int toppingId) { var pizzaToUpdate = _context.Pizzas.Find(pizzaId); var toppingToAdd = _context.Toppings.Find(toppingId); if (pizzaToUpdate is null || toppingToAdd is null) { throw new InvalidOperationException("Pizza or topping does not exist"); } if(pizzaToUpdate.Toppings is null) { pizzaToUpdate.Toppings = new List<Topping>(); } pizzaToUpdate.Toppings.Add(toppingToAdd); _context.SaveChanges(); }
上のコードでは以下の操作が行われます。
- 既存の
Pizza
とTopping
オブジェクトへの参照は、Find
を使って作成されます。 Topping
オブジェクトは、.Add
メソッドでPizza.Toppings
コレクションに追加されます。 存在しない場合は、新しいコレクションが作成されます。SaveChanges
メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
- 既存の
UpdateSauce
メソッドを次のコードに置き換えます。public void UpdateSauce(int pizzaId, int sauceId) { var pizzaToUpdate = _context.Pizzas.Find(pizzaId); var sauceToUpdate = _context.Sauces.Find(sauceId); if (pizzaToUpdate is null || sauceToUpdate is null) { throw new InvalidOperationException("Pizza or sauce does not exist"); } pizzaToUpdate.Sauce = sauceToUpdate; _context.SaveChanges(); }
上のコードでは以下の操作が行われます。
- 既存の
Pizza
とSauce
オブジェクトへの参照は、Find
を使って作成されます。Find
は、主キーによるレコードのクエリに最適化されたメソッドです。Find
では、データベースのクエリを実行する前にまず、ローカル エンティティ グラフが検索されます。 Pizza.Sauce
プロパティは、Sauce
オブジェクトに設定されます。Sauce
プロパティがPizza
で設定されていることは EF Core によって検出されるため、Update
メソッドを呼び出す必要はありません。SaveChanges
メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
- 既存の
DeleteById
メソッドを次のコードに置き換えます。public void DeleteById(int id) { var pizzaToDelete = _context.Pizzas.Find(id); if (pizzaToDelete is not null) { _context.Pizzas.Remove(pizzaToDelete); _context.SaveChanges(); } }
上のコードでは以下の操作が行われます。
Find
メソッドにより、主キー (この場合はId
) でピザが取得されます。Remove
メソッドを使用すると、EF Core のオブジェクト グラフのpizzaToDelete
エンティティが削除されます。SaveChanges
メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
すべての変更を保存して、
dotnet build
を実行します。 エラーが発生した場合はすべて修正します。
データベースのシード
PizzaService
の CRUD 操作のコードを作成しましたが、データベースに正しいデータが含まれていると、"読み取り" 操作のテストが簡単になります。 起動時にデータベースをシードするよう、アプリを変更することにします。
警告
データベースのシードを行うこのコードは競合状態を考慮していないため、変更を軽減せずに分散環境で使うときは注意してください。
Data フォルダーに、DbInitializer.cs という名前の新しいファイルを追加します。
次のコードを Data\DbInitializer.cs に追加します。
using ContosoPizza.Models; namespace ContosoPizza.Data { public static class DbInitializer { public static void Initialize(PizzaContext context) { if (context.Pizzas.Any() && context.Toppings.Any() && context.Sauces.Any()) { return; // DB has been seeded } var pepperoniTopping = new Topping { Name = "Pepperoni", Calories = 130 }; var sausageTopping = new Topping { Name = "Sausage", Calories = 100 }; var hamTopping = new Topping { Name = "Ham", Calories = 70 }; var chickenTopping = new Topping { Name = "Chicken", Calories = 50 }; var pineappleTopping = new Topping { Name = "Pineapple", Calories = 75 }; var tomatoSauce = new Sauce { Name = "Tomato", IsVegan = true }; var alfredoSauce = new Sauce { Name = "Alfredo", IsVegan = false }; var pizzas = new Pizza[] { new Pizza { Name = "Meat Lovers", Sauce = tomatoSauce, Toppings = new List<Topping> { pepperoniTopping, sausageTopping, hamTopping, chickenTopping } }, new Pizza { Name = "Hawaiian", Sauce = tomatoSauce, Toppings = new List<Topping> { pineappleTopping, hamTopping } }, new Pizza { Name="Alfredo Chicken", Sauce = alfredoSauce, Toppings = new List<Topping> { chickenTopping } } }; context.Pizzas.AddRange(pizzas); context.SaveChanges(); } } }
上のコードでは以下の操作が行われます。
DbInitializer
クラスとInitialize
メソッドは、どちらもstatic
として定義されています。Initialize
は、パラメーターとしてPizzaContext
オブジェクトを受け取ります。- 3 つのテーブル
Pizza
、Sauce
、Topping
のいずれにもレコードが存在しない場合、オブジェクトが作成されます。 AddRange
を使って、Pizza
オブジェクト (およびそのナビゲーション プロパティSauce
とTopping
) をオブジェクト グラフに追加します。SaveChanges
を使って、オブジェクト グラフの変更をデータベースにコミットします。
DbInitializer
クラスはデータベースをシード処理する準備ができていますが、Program.cs から呼び出す必要があります。 次の手順では、DbInitializer.Initialize
を呼び出す IHost
の拡張メソッドを作成します。
Data フォルダーに、Extensions.cs という名前の新しいファイルを追加します。
次のコードを Data\Extensions.cs に追加します。
namespace ContosoPizza.Data; public static class Extensions { public static void CreateDbIfNotExists(this IHost host) { { using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; var context = services.GetRequiredService<PizzaContext>(); context.Database.EnsureCreated(); DbInitializer.Initialize(context); } } } }
上のコードでは以下の操作が行われます。
CreateDbIfNotExists
メソッドは、IHost
の拡張機能として定義されています。PizzaContext
サービスへの参照を作成します。EnsureCreated で、データベースが存在することを確認します。
重要
データベースが存在しない場合は、
EnsureCreated
によって新しいデータベースが作成されます。 新しいデータベースは移行用に構成されていないため、このメソッドは注意して使う必要があります。DbIntializer.Initialize
メソッドが呼び出されます。 パラメーターとして、PizzaContext
オブジェクトが渡されます。
最後に、Program.cs で、
// Add the CreateDbIfNotExists method call
コメントを次のコードに置き換えて、新しい拡張メソッドを呼び出します。app.CreateDbIfNotExists();
このコードは、アプリが実行するたびに、前に定義した拡張メソッドを呼び出します。
すべての変更を保存して、
dotnet build
を実行します。
基本的な CRUD 操作と起動時のデータベースのシードを行うために必要なすべてのコードを記述しました。 次の演習では、アプリでそれらの操作をテストします。