演習 - データを操作する

完了

前の演習では、エンティティ クラスとデータベース コンテキストを作成しました。 その後、EF Core の移行機能を使ってデータベース スキーマを作成しました。

この演習では、PizzaService の実装を完了します。 このサービスでは、EF Core を使って、データベースに対する CRUD 操作を実行します。

CRUD 操作のコードを作成する

PizzaService の実装を完了するには、Services\PizzaService.cs で次の手順のようにします。

  1. 例で示されているように次の変更を行います。

    1. using ContosoPizza.Data; ディレクティブを追加します。
    2. using Microsoft.EntityFrameworkCore; ディレクティブを追加します。
    3. コンストラクターの前に、PizzaContext のクラス レベルのフィールドを追加します。
    4. コンストラクター メソッドのシグネチャを、PizzaContext パラメーターを受け取るように変更します。
    5. コンストラクター メソッドのコードを、パラメーターをフィールドに代入するように変更します。
    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 がコンストラクターに挿入されます。

  2. GetAll メソッドを次のコードに置き換えます。

    public IEnumerable<Pizza> GetAll()
    {
        return _context.Pizzas
            .AsNoTracking()
            .ToList();
    }
    

    上のコードでは以下の操作が行われます。

    • Pizzas コレクションには、ピザ テーブルのすべての行が含まれます。
    • AsNoTracking 拡張メソッドは、変更の追跡を無効にするよう EF Core に指示します。 この操作は読み取り専用なので、AsNoTracking を指定することでパフォーマンスを最適にできます。
    • すべてのピザは、ToList で返されます。
  3. GetById メソッドを次のコードに置き換えます。

    public Pizza? GetById(int id)
    {
        return _context.Pizzas
            .Include(p => p.Toppings)
            .Include(p => p.Sauce)
            .AsNoTracking()
            .SingleOrDefault(p => p.Id == id);
    }
    

    上のコードでは以下の操作が行われます。

    • Include 拡張メソッドは、一括読み込みを使って ToppingsSauce のナビゲーション プロパティを結果に含めることを指定するラムダ式を受け取ります。 この式がないと、EF Core はそれらのプロパティに null を返します。
    • SingleOrDefault メソッドからは、ラムダ式に一致するピザが返されます。
      • 一致するレコードがない場合は、null が返されます。
      • 複数のレコードが一致する場合は、例外がスローされます。
      • ラムダ式には、Id プロパティが id パラメーターと等しいレコードが記述されています。
  4. 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 に指示します。
  5. 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();
    }
    

    上のコードでは以下の操作が行われます。

    • 既存の PizzaSauce オブジェクトへの参照は、Find を使って作成されます。 Find は、主キーによるレコードのクエリに最適化されたメソッドです。 Find では、データベースのクエリを実行する前にまず、ローカル エンティティ グラフが検索されます。
    • Pizza.Sauce プロパティは、Sauce オブジェクトに設定されます。
    • Sauce プロパティが Pizza で設定されていることは EF Core によって検出されるため、Update メソッドを呼び出す必要はありません。
    • SaveChanges メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
  6. 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();
    }
    

    上のコードでは以下の操作が行われます。

    • 既存の PizzaTopping オブジェクトへの参照は、Find を使って作成されます。
    • Topping オブジェクトは、.Add メソッドで Pizza.Toppings コレクションに追加されます。 存在しない場合は、新しいコレクションが作成されます。
    • SaveChanges メソッドは、オブジェクトの変更をデータベースに保存するよう EF Core に指示します。
  7. 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 に指示します。
  8. すべての変更を保存して、dotnet build を実行します。 エラーが発生した場合はすべて修正します。

データベースのシード

PizzaService の CRUD 操作のコードを作成しましたが、データベースに正しいデータが含まれていると、読み取り操作のテストが簡単になります。 起動時にデータベースをシードするよう、アプリを変更することにします。

警告

データベースのシードを行うこのコードは競合状態を考慮していないため、変更を軽減せずに分散環境で使うときは注意してください。

  1. Data フォルダーに、DbInitializer.cs という名前の新しいファイルを追加します。

  2. 次のコードを 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 つのテーブル PizzaSauceTopping のいずれにもレコードが存在しない場合、オブジェクトが作成されます。
    • AddRange を使って、Pizza オブジェクト (およびそのナビゲーション プロパティ SauceTopping) をオブジェクト グラフに追加します。
    • SaveChanges を使って、オブジェクト グラフの変更をデータベースにコミットします。

DbInitializer クラスはデータベースをシード処理する準備ができていますが、Program.cs から呼び出す必要があります。 次の手順では、DbInitializer.Initialize を呼び出す IHost の拡張メソッドを作成します。

  1. Data フォルダーに、Extensions.cs という名前の新しいファイルを追加します。

  2. 次のコードを 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 オブジェクトが渡されます。

  3. 最後に、Program.cs で、// Add the CreateDbIfNotExists method call コメントを次のコードに置き換えて、新しい拡張メソッドを呼び出します。

    app.CreateDbIfNotExists();
    

    このコードは、アプリが実行するたびに、前に定義した拡張メソッドを呼び出します。

  4. すべての変更を保存して、dotnet build を実行します。

基本的な CRUD 操作と起動時のデータベースのシードを行うために必要なすべてのコードを作成しました。 次の演習では、アプリでそれらの操作をテストします。

自分の知識をチェックする

1.

読み取り専用クエリを記述するとします。 オブジェクト グラフの変更の追跡が不要であることを EF Core にどのように示しますか?