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