练习 - 与数据交互

已完成

在上一个练习中,你创建了实体类和数据库上下文。 然后,使用 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 集合包含 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 扩展方法采用 lambda 表达式 来指定将 ToppingsSauce 导航属性包含在结果中(通过使用预先加载)。 如果不使用此表达式,EF Core 会为这些属性返回 null。
    • SingleOrDefault 方法返回与 lambda 表达式匹配的披萨。
      • 如果没有记录匹配,则返回 null
      • 如果多个记录匹配,则会引发异常。
      • lambda 表达式描述 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 对象。
    • Update 方法调用是不必要的,因为 EF Core 检测到你在 Pizza 上设置了 Sauce 属性。
    • 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 创建的。
    • 使用 .Add 方法将 Topping 对象添加到 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 对象。
    • 如果三个表中的任何一个都没有记录,则创建 PizzaSauceTopping 对象。
    • Pizza 对象(及其 SauceTopping 导航属性)通过使用 AddRange 添加到对象图中。
    • 对象图更改通过使用 SaveChanges 提交到数据库。

DbInitializer 类已准备好为数据库设定种子,但需要从 Program.cs 调用它。 以下步骤创建调用 DbInitializer.InitializeIHost 的扩展方法:

  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 指示不需要进行对象图更改跟踪?