第六章 SportsStore: 修改和删除数据

目前,SportsStore应用程序可以将产品对象存储在数据库中,可执行查询来获取它们。大多数应用程序还要求能够在数据存储之后对其进行更改,包括完全删除对象。在本章中,我添加了对Product对象更新和删除的支持。我还将描述在您自己的项目中添加这些特性时可能遇到的问题,并解释如何解决这些问题。

 
6.1 本章准备工作
在本章中,我将继续使用SportsStore项目。它是我们在第4章中创建的,并在第5章中为它添加了EF Core。在SportsStore项目文件夹中运行清单6-1所示的命令来删除和重建数据库,这将保证您从示例中获得预期的结果。
 
小贴士:您可以从本书的GitHub知识库中下载本章的SportsStore项目和其他章节的项目:
Listing 6-1. Deleting and Re-creating the Database
dotnet ef database drop --force
dotnet ef database update

使用dotnet run启动应用程序并导航到http://localhost:5000;您将看到如图6-1所示的内容。

使用表6-1中的数据值填写HTML表单,表6-1将为本章的示例提供数据。

 

当您添加了这三个产品的详细信息后,您应该会看到如图6-2所示的结果。

 

 
6.2 对象更新
EF Core支持许多不同的对象更新方式,我将在第12和21章中进行描述。在本章中,我将从最简单的技术开始,其中由MVC模型绑定器创建的对象完全替换存储在数据库中的对象。
 
6.2.1 修改仓储类
首先,我更改了IRepository接口,以添加应用程序其余部分可用于检索和更新现有对象的方法,如清单6-2所示。
 1 //Listing 6-2. Adding Methods in the IRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 namespace SportsStore.Models
 4 {
 5     public interface IRepository
 6     {
 7         IEnumerable<Product> Products { get; }
 8         Product GetProduct(long key);
 9         void AddProduct(Product product);
10         void UpdateProduct(Product product);
11     }
12 }

GetProduct方法将根据主键值提供单个Product对象。UpdateProduct方法接收产品对象,但不返回结果。在清单6-3中,我将新方法添加到存储库实现类中。

 1 //Listing 6-3. Adding a Method in the DataRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 namespace SportsStore.Models
 5 {
 6     public class DataRepository : IRepository
 7     {
 8         private DataContext context;
 9         public DataRepository(DataContext ctx) => context = ctx;
10         public IEnumerable<Product> Products => context.Products.ToArray();
11         public Product GetProduct(long key) => context.Products.Find(key);
12         public void AddProduct(Product product)
13         {
14             context.Products.Add(product);
15             context.SaveChanges();
16         }
17         public void UpdateProduct(Product product)
18         {
19             context.Products.Update(product);
20             context.SaveChanges();
21         }
22     }
23 }

  数据库上下文的Products属性返回的DbSet<Product>类型对象,为我需要实现的新方法提供了以下功能:Find方法接受一个主键值,并查询数据库对应表。Update方法接受产品对象并以它更新数据库,更新数据库中具有相同主键的对象(记录)。与更改数据库的所有操作一样,我必须在更新方法之后调用SaveChanges方法。

小贴士:不要忘记调用SaveChanges方法,这可能很尴尬,但它很快成为第二天性。这种方法意味着你可以设置多个改变通过调用上下文对象的方法,然后调用一个SaveChanges方法将多个改变一并发送到数据库执行。我将在第24章详细解释这是如何工作的。
 
 
 
6.2.2 修改控制器创建一个视图
下一步是更新Home控制器,写一些Action(操作)方法允许用户选择要编辑的产品对象,并将更改发送到应用程序,如清单6-4所示。我还注释掉了清除控制台的行,以便EF Core为新方法执行的SQL查询更容易看到。
 1 //Listing 6-4. Adding Actions in the HomeController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     public class HomeController : Controller
 7     {
 8         private IRepository repository;
 9         public HomeController(IRepository repo) => repository = repo;
10         public IActionResult Index()
11         {
12             //System.Console.Clear();
13             return View(repository.Products);
14         }
15         [HttpPost]
16         public IActionResult AddProduct(Product product)
17         {
18             repository.AddProduct(product);
19             return RedirectToAction(nameof(Index));
20         }
21         public IActionResult UpdateProduct(long key)
22         {//Get请求UpdateProduct视图——即编辑页面
23             return View(repository.GetProduct(key));
24         }
25         [HttpPost]
26         public IActionResult UpdateProduct(Product product)
27         {//POST请求,更新数据
28             repository.UpdateProduct(product);
29             return RedirectToAction(nameof(Index));
30         }
31     }
32 }

您可以看到Action(操作)方法如何映射到仓储类提供的功能,以及如何再映射到数据库上下文类。为了向控制器新加的操作提供视图,我在Views/Home文件夹下添加了一个UpdateProduct.cshtml 文件。其内容如清单6-5所示。

 1 Listing 6-5. The Contents of the UpdateProduct.cshtml in the Views/Home Folder
 2 @model Product
 3 <h3 class="p-2 bg-primary text-white text-center">Update Product</h3>
 4 <form asp-action="UpdateProduct" method="post">
 5     <div class="form-group">
 6         <label asp-for="Id"></label>
 7         <input asp-for="Id" class="form-control" readonly />
 8     </div>
 9     <div class="form-group">
10         <label asp-for="Name"></label>
11         <input asp-for="Name" class="form-control" />
12     </div>
13     <div class="form-group">
14         <label asp-for="Category"></label>
15         <input asp-for="Category" class="form-control" />
16     </div>
17     <div class="form-group">
18         <label asp-for="PurchasePrice"></label>
19         <input asp-for="PurchasePrice" class="form-control" />
20     </div>
21     <div class="form-group">
22         <label asp-for="RetailPrice"></label>
23         <input asp-for="RetailPrice" class="form-control" />
24     </div>
25     <div class="text-center">
26         <button class="btn btn-primary" type="submit">Save</button>
27         <a asp-action="Index" class="btn btn-secondary">Cancel</a>
28     </div>
29 </form>

  视图为用户提供了一个HTML表单,该表单可用于更改产品对象的属性,Id属性除外,Id属性用作主键。一旦数据库分配了主键,就不能轻易修改它们。如果需要不同的键值,删除该键值对应的对象再创建一个新对象会更简单。出于这个原因,我向input元素添加了readonly属性,该属性只显示Id属性的值,但不允许对其进行更改。

  为了将更新功能集成到应用程序的其他部分,我为Index视图显示的每个Product对象添加一个按钮元素,如清单6-6所示。我还添加了一个列用来显示Id属性。

 1     Listing 6-6. Integrating Updates in the Index.cshtml File in the Views/Home Folder
 2     @model IEnumerable<Product>
 3     <h3 class="p-2 bg-primary text-white text-center">Products</h3>
 4     <div class="container-fluid mt-3">
 5         <div class="row">
 6             <div class="col-1 font-weight-bold">Id</div>
 7             <div class="col font-weight-bold">Name</div>
 8             <div class="col font-weight-bold">Category</div>
 9             <div class="col font-weight-bold text-right">Purchase Price</div>
10             <div class="col font-weight-bold text-right">Retail Price</div>
11             <div class="col"></div>
12         </div>
13         <form asp-action="AddProduct" method="post">
14             <div class="row p-2">
15                 <div class="col-1"></div>
16                 <div class="col"><input name="Name" class="form-control" /></div>
17                 <div class="col"><input name="Category" class="form-control" /></div>
18                 <div class="col">
19                     <input name="PurchasePrice" class="form-control" />
20                 </div>
21                 <div class="col">
22                     <input name="RetailPrice" class="form-control" />
23                 </div>
24                 <div class="col">
25                     <button type="submit" class="btn btn-primary">Add</button>
26                 </div>
27             </div>
28         </form>
29         <div>
30             @if (Model.Count() == 0) {
31             <div class="row">
32                 <div class="col text-center p-2">No Data</div>
33             </div>
34             } else {
35             @foreach (Product p in Model) {
36             <div class="row p-2">
37                 <div class="col-1">@p.Id</div>
38                 <div class="col">@p.Name</div>
39                 <div class="col">@p.Category</div>
40                 <div class="col text-right">@p.PurchasePrice</div>
41                 <div class="col text-right">@p.RetailPrice</div>
42                 <div class="col">
43                     <a asp-action="UpdateProduct" asp-route-key="@p.Id"
44                        class="btn btn-outline-primary">
45                         Edit
46                     </a>
47                 </div>
48             </div>
49             }
50             }
51         </div>
52     </div>

使用dotnet run启动应用程序并导航到http://localhost:5000;您将看到新的元素,它们显示主键并为每个产品提供一个Edit按钮,如图6-3所示。

单击足球产品的Edit按钮,将Purchase Price字段的值更改为16.50,然后单击Save按钮。浏览器将form表单数据发送到Home控制器上的UpdateProduct操作方法,该方法接收由MVC模型绑定器创建的产品对象(就是根据form表单数据name-value创建产品对象的)。将产品对象传递给数据库上下文类的更新方法,当调用SaveChanges方法后,表单数据值将存储在数据库中,如图6-4所示。

如果检查应用程序生成的日志消息,可以看到执行的Action操作方法生成什么样的SQL命令发送到数据库服务器。当你点击编辑按钮时,EF Core通过以下命令查询数据库中指定产品对象(Soccer Ball )的详细信息:

...
SELECT TOP(1) [e].[Id], [e].[Category], [e].[Name], [e].[PurchasePrice],
[e].[RetailPrice]
FROM [Products] AS [e]
WHERE [e].[Id] = @__get_Item_0
...

清单6-6中使用的Find方法被翻译为SELECT语句查询一条产品记录,该命令使用TOP关键字指定。单击Save按钮时,Entity Framework Core使用以下命令更新数据库:

...
UPDATE [Products] SET [Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2,
[RetailPrice] = @p3
WHERE [Id] = @p4;
...

Update方法被转换成SQL的Update语句,存储从HTTP请求接收到的表单值。

 

6.2.3 只更新值变化了的属性

执行更新的基本程序结构已经OK了,但是结果是低效的,因为实体EF Core没有基线来找出发生了什么变化,因此别无选择,只能存储所有属性。我可以演示给你看:单击其中一个产品的Edit按钮,然后单击Save而不做任何更改。即使没有新的数据值,应用程序生成的日志消息显示,Entity Framework Core生成的UPDATE语句会发送Product类定义的所有属性的值。(即Update这条记录的所有列,而不是哪个属性值被修改了,Update哪个列)

...
UPDATE [Products] SET [Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2,
[RetailPrice] = @p3
WHERE [Id] = @p4;
...

实体框架核心有一个变化侦测功能,它可以识别是哪些属性值发生了变化。对于Product这样简单的数据模型类(属性简单而且数量很少)这都无所谓,但是对于更复杂的数据模型,变化侦测可能非常重要。

  变化侦测功能需要一个基线,可以将从用户接收的数据与之前从数据库获取的进行比较。提供基线的方式有很多种,我将在第12章中介绍,但是我在本章使用最简单的方式,即先查询数据库以获取现有数据。在清单6-7中,我更新了repository实现类,这样它就可以在数据库中查询存储的产品对象,并使用它来避免更新没有做更改的属性。

小贴士:查询的成本必须与避免不必要更新相权衡,但是这种方法简单可靠,并且可以很好地使用EF Core提供的功能来阻止两个用户尝试更新同一笔数据,如第20章所述。

 1 //Listing 6-7. Avoiding Unnecessary Updates in the DataRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 namespace SportsStore.Models
 5 {
 6     public class DataRepository : IRepository
 7     {
 8         private DataContext context;
 9         public DataRepository(DataContext ctx) => context = ctx;
10         public IEnumerable<Product> Products => context.Products.ToArray();
11         public Product GetProduct(long key) => context.Products.Find(key);
12         public void AddProduct(Product product)
13         {
14             context.Products.Add(product);
15             context.SaveChanges();
16         }
17         public void UpdateProduct(Product product)
18         {
19             Product p = GetProduct(product.Id);//先查询出来
20             p.Name = product.Name;
21             p.Category = product.Category;
22             p.PurchasePrice = product.PurchasePrice;
23             p.RetailPrice = product.RetailPrice;
24             // context.Products.Update(product);
25           context.SaveChanges();
26         }
27     }
28 }

  这段代码连接了应用程序中的两个不同功能。EF Core会对创建的实体对象(从数据库查询出来的数据)执行更改跟踪,而MVC模型绑定器则从HTTP数据创建对象。

  译者述:上面代码中形参product是MVC模型绑定器根据HTTP POST请求提交的数据(即HTML的form表单数据)创建的Product实体对象(和数据库没半毛钱关系),局部变量p是上下文根据从数据库查询的数据创建的实体对象(即数据库表中行记录)。

  这两个实体对象的来源(上下文—数据库、MVC绑定器—HTML表单)没有集成在一起,如果不小心将它们分离,就会出现问题。利用更改跟踪的最安全方法是先查询数据库,然后从HTTP数据中复制值,如我在上面清单6-7中所做的那样。当SaveChanges方法被调用时,EF Core将确定实体对象的哪些值被更改,并仅更新数据库中该行记录的那些列。

小贴士请注意,我已经注释掉了对Update方法的调用,当查询提供基线数据时,Update方法是不要的。

要看一下上面代码怎样工作的,请使用dotnet run启动应用程序,导航到http://localhost:5000,并单击Kayak产品的Edit按钮。将零售价值更改为300并单击Save按钮。查看应用程序生成的日志消息,您将看到实体框架核心发送到数据库的Update语句,只Update了值更改的列。

1 ...
2 UPDATE [Products] SET [RetailPrice] = @p0
3 WHERE [Id] = @p1;
4 ...

 

译者注:对于复杂的实体类建议采用这种方式,哪些属性值修改了更新哪些属性。对于简单的实体类怎样更新无所谓。对于不容易获知用户改了哪些属性的情况,干脆更新所有属性更方便。

 

6.2.4 批量更新

在需要一次操作更改多个对象的管理员应用程序中,常常需要批量更新。更新的确切性质将有所不同,但是批量更新的常见原因包括纠正数据输入错误或将对象重新分配到新的分类,这两种情况如果改一个对象(一行)提交一条SQL请求,应用程序循环请求Update数据库记录会非常耗时。使用EF Core可以轻松地执行批量更新,但还需要一些努力,才能使EF Core的批量更新与应用程序的ASP.NET Core MVC部分顺利地配合。

 

6.2.4.1 修改视图和控制器

为了添加对执行批量更新的支持,我更新了Index视图,使其包含一个对应于UpdateAll 操作(Action方法)的Edit All按钮。我还添加了一个名为UpdateAll的ViewBag属性。如清单6-8所示。当if 条件为真时,将显示出来一个名为InlineEditor.cshtml的部分视图。

 1 Listing 6-8. Supporting Bulk Updates in the Index.cshtml File in the Views/Home Folder
 2 @model IEnumerable<Product>
 3 <h3 class="p-2 bg-primary text-white text-center">Products</h3>
 4 <div class="container-fluid mt-3">
 5     @if (ViewBag.UpdateAll != true)
 6     {
 7         <div class="row">
 8             <div class="col-1 font-weight-bold">Id</div>
 9             <div class="col font-weight-bold">Name</div>
10             <div class="col font-weight-bold">Category</div>
11             <div class="col font-weight-bold text-right">Purchase Price</div>
12             <div class="col font-weight-bold text-right">Retail Price</div>
13             <div class="col"></div>
14         </div>
15         <form asp-action="AddProduct" method="post">
16             <div class="row p-2">
17                 <div class="col-1"></div>
18                 <div class="col"><input name="Name" class="form-control" /></div>
19                 <div class="col"><input name="Category" class="form-control" /></div>
20                 <div class="col">
21                     <input name="PurchasePrice" class="form-control" />
22                 </div>
23                 <div class="col">
24                     <input name="RetailPrice" class="form-control" />
25                 </div>
26                 <div class="col">
27                     <button type="submit" class="btn btn-primary">Add</button>
28                 </div>
29             </div>
30         </form>
31         <div>
32             @if (Model.Count() == 0)
33             {
34                 <div class="row">
35                     <div class="col text-center p-2">No Data</div>
36                 </div>
37             }
38             else
39             {
40                 @foreach (Product p in Model)
41                 {
42                     <div class="row p-2">
43                         <div class="col-1">@p.Id</div>
44                         <div class="col">@p.Name</div>
45                         <div class="col">@p.Category</div>
46                         <div class="col text-right">@p.PurchasePrice</div>
47                         <div class="col text-right">@p.RetailPrice</div>
48                         <div class="col">
49                             <a asp-action="UpdateProduct" asp-route-key="@p.Id"
50                                class="btn btn-outline-primary">
51                                 Edit
52                             </a>
53                         </div>
54                     </div>
55                 }
56             }
57         </div>
58         <div class="text-center">
59             <a asp-action="UpdateAll" class="btn btn-primary">Edit All</a>
60         </div>
61     }
62     else
63     {
64         @Html.Partial("InlineEditor", Model)
65     }
66 </div>

我在Views/Home文件夹下添加了一个InlineEditor.cshtml文件,在里面创建了分部视图。内容如清单6-9所示。

 1 Listing 6-9. The Contents of the InlineEditor.cshtml File in the Views/Home Folder
 2 @model IEnumerable<Product>
 3 <div class="row">
 4     <div class="col-1 font-weight-bold">Id</div>
 5     <div class="col font-weight-bold">Name</div>
 6     <div class="col font-weight-bold">Category</div>
 7     <div class="col font-weight-bold">Purchase Price</div>
 8     <div class="col font-weight-bold">Retail Price</div>
 9 </div>
10 @{ int i = 0; }
11 <form asp-action="UpdateAll" method="post">
12     @foreach (Product p in Model)
13     {
14         <div class="row p-2">
15             <div class="col-1">
16                 @p.Id
17                 <input type="hidden" name="Products[@i].Id" value="@p.Id" />
18             </div>
19             <div class="col">
20                 <input class="form-control" name="Products[@i].Name"
21                        value="@p.Name" />
22             </div>
23             <div class="col">
24                 <input class="form-control" name="Products[@i].Category"
25                        value="@p.Category" />
26             </div>
27             <div class="col text-right">
28                 <input class="form-control" name="Products[@i].PurchasePrice"
29                        value="@p.PurchasePrice" />
30             </div>
31             <div class="col text-right">
32                 <input class="form-control" name="Products[@i].RetailPrice"
33                        value="@p.RetailPrice" />
34             </div>
35         </div>
36         i++;
37     }
38     <div class="text-center m-2">
39         <button type="submit" class="btn btn-primary">Save All</button>
40         <a asp-action="Index" class="btn btn-outline-primary">Cancel</a>
41     </div>
42 </form>

  部分视图创建一组表单元素,其名称遵循对象集合的MVC约定,因此Id属性的名称为Products[0].Id, Products[1].Id
等等。为输入元素设置名称需要一个计数器,这会产生Razor和c#表达式的尴尬组合。

  在清单6-10中,我向Home控制器添加了操作方法,它允许用户启动批量编辑过程并提交数据。

 1 //Listing 6-10. Adding Action Methods in the HomeController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     public class HomeController : Controller
 7     {
 8         private IRepository repository;
 9         public HomeController(IRepository repo) => repository = repo;
10         public IActionResult Index()
11         {
12             //System.Console.Clear();
13             return View(repository.Products);
14         }
15         [HttpPost]
16         public IActionResult AddProduct(Product product)
17         {
18             repository.AddProduct(product);
19             return RedirectToAction(nameof(Index));
20         }
21         public IActionResult UpdateProduct(long key)
22         {
23             return View(repository.GetProduct(key));
24         }
25         [HttpPost]
26         public IActionResult UpdateProduct(Product product)
27         {
28             repository.UpdateProduct(product);
29             return RedirectToAction(nameof(Index));
30         }
31         public IActionResult UpdateAll()
32         {
33             ViewBag.UpdateAll = true;
34             return View(nameof(Index), repository.Products);
35         }
36 
37         [HttpPost]
38         public IActionResult UpdateAll(Product[] products)
39         {
40             repository.UpdateAll(products);
41             return RedirectToAction(nameof(Index));
42         }
43     }
44 }

UpdateAll方法的POST版本接受一个Product数组,MVC模型绑定器将从表单数据创建这些对象,并将其传递给同名的仓储方法。

 

6.2.4.2 修改仓储

在清单6-11中,我向仓储接口添加了一个新方法,该方法将执行批量更新。

 1 //Listing 6-11. Adding a Method in the IRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 namespace SportsStore.Models
 4 {
 5     public interface IRepository
 6     {
 7         IEnumerable<Product> Products { get; }
 8         Product GetProduct(long key);
 9         void AddProduct(Product product);
10         void UpdateProduct(Product product);
11         void UpdateAll(Product[] products);
12     }
13 }

为了完成这个功能,我在仓储实现中添加了一个UpdateAll方法,该方法使用从HTTP请求接收的数据更新数据库,如清单6-12所示。

 1 //Listing 6-12. Performing a Bulk Edit in the DataRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 namespace SportsStore.Models
 5 {
 6     public class DataRepository : IRepository
 7     {
 8         private DataContext context;
 9         public DataRepository(DataContext ctx) => context = ctx;
10         public IEnumerable<Product> Products => context.Products.ToArray();
11         public Product GetProduct(long key) => context.Products.Find(key);
12         public void AddProduct(Product product)
13         {
14             context.Products.Add(product);
15             context.SaveChanges();
16         }
17         public void UpdateProduct(Product product)
18         {
19             Product p = GetProduct(product.Id);
20             p.Name = product.Name;
21             p.Category = product.Category;
22             p.PurchasePrice = product.PurchasePrice;
23             p.RetailPrice = product.RetailPrice;
24             //context.Products.Update(product);
25             context.SaveChanges();
26         }
27         public void UpdateAll(Product[] products)
28         {
29             context.Products.UpdateRange(products);
30             context.SaveChanges();
31         }
32     }
33 }

DbSet<T>类提供了处理单个对象和对象集合的方法。在本例中,我使用了UpdateRange方法,它是Update方法的集合副本。调用SaveChanges方法时,Entity Framework Core将发送一系列SQL UPDATE命令来更新服务器。使用dotnet run启动应用程序,导航到http://localhost:5000,然后单击Edit All按钮以显示批量编辑特性,如图6-5所示

 

6.2.4.3 对批量更新做变化侦测

清单6-12中的代码没有使用实体框架核心更改侦测特性,这意味着所有产品对象的所有属性都将被更新。为了只更新被更改的值,我修改了repository类中的UpdateAll方法,如清单6-13所示。

 1 //Listing 6-13. Using Change Detection in the DataRepository.cs File in the Models Folder
 2 ...
 3 public void UpdateAll(Product[] products)
 4 {
 5     //context.Products.UpdateRange(products);
 6     Dictionary<long, Product> data = products.ToDictionary(p => p.Id);
 7     IEnumerable<Product> baseline =
 8         context.Products.Where(p => data.Keys.Contains(p.Id));//是HTTP请求传来的数组元素的Id
 9     foreach (Product databaseProduct in baseline)
10     {
11         Product requestProduct = data[databaseProduct.Id];
12         databaseProduct.Name = requestProduct.Name;
13         databaseProduct.Category = requestProduct.Category;
14         databaseProduct.PurchasePrice = requestProduct.PurchasePrice;
15         databaseProduct.RetailPrice = requestProduct.RetailPrice;
16     }
17     context.SaveChanges();
18 }
19 ...

执行更新的过程可能比较复杂。我首先创建一个从MVC模型绑定器接收到的产品对象的字典,使用Id属性作为键。我使用键的集合来查询数据库中相应的对象,如下图所示:

我遍历(列举)SQL查询的对象并从HTTP请求对象中复制属性值(来对前者的属性赋值)。当调用SaveChanges方法,EF Core执行更改侦测,仅更新那些更改的值。使用dotnet run启动应用程序,导航到http://localhost:5000,然后单击Edit All按钮。将第一个产品的Name字段改为Green Kayak Lifejacket Retail Price字段 改为50。单击Save All按钮并检查应用程序生成的日志消息。要获取用于变更侦测的基线数据,EF Core将此查询发送到数据库:

当foreach准备遍历前,先查询Id=1,2,3的记录,存到baseline集合里。SQL的where子句里的1,2,3当然是HTTP POST请求里面传递进来的所有对象的Id。(即HTTP提交了3个对象,Id分别是1,2,3)

...
SELECT [p].[Id], [p].[Category], [p].[Name], [p].[PurchasePrice], [p].[RetailPrice]
FROM [Products] AS [p]
WHERE [p].[Id] IN (1, 2, 3)
...

  如上SQL,从该结果集数据创建的对象用于更改侦测(习惯把这些对象叫做baseline基线)。Entity Framework Core根据这些对象,再结合从HTTP传进来的那些对象,计算出哪些属性具有新值,并向数据库发送两个Update命令。

...
UPDATE [Products] SET [Name] = @p0
WHERE [Id] = @p1;
...
UPDATE [Products] SET [RetailPrice] = @p2
WHERE [Id] = @p3;
...

  可以看到,第一个命令更改了Name值,第二个命令更改了RetailPrice值,这与使用应用程序的MVC部分所做的更改相对应。

 

 

6.3 删除数据

从数据库中删除对象是一个简单的过程,但是随着数据模型的增长,这个过程会变得更加复杂,正如我在第7章中解释的那样。在清单6-14中,我向仓储接口添加了Delete方法。

 1 //Listing 6-14. Adding a Method in the IRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 namespace SportsStore.Models
 4 {
 5     public interface IRepository
 6     {
 7         IEnumerable<Product> Products { get; }
 8         Product GetProduct(long key);
 9         void AddProduct(Product product);
10         void UpdateProduct(Product product);
11         void UpdateAll(Product[] products);
12         void Delete(Product product);
13     }
14 }

在清单6-15中,我更新了repository实现类,以添加对Delete方法的支持

 1 //Listing 6-15. Deleting Objects in the DataRepository.cs File in the Models Folder
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 namespace SportsStore.Models
 5 {
 6     public class DataRepository : IRepository
 7     {
 8         private DataContext context;
 9         public DataRepository(DataContext ctx) => context = ctx;
10         public IEnumerable<Product> Products => context.Products.ToArray();
11         public Product GetProduct(long key) => context.Products.Find(key);
12         // ...other methods omitted for brevity...
13         public void Delete(Product product)
14         {
15             context.Products.Remove(product);
16             context.SaveChanges();
17         }
18     }
19 }

  DbSet<T>类中具有从数据库中删除一个或多个对象的Remove和RemoveRange方法。与修改数据库的其他操作类似,在调用SaveChanges方法之前,不会删除(操作)任何数据。

  在整个应用程序中,我向Home控制器添加了一个操作方法(一个Action方法),该方法从HTTP请求中接受要删除的产品对象的详细信息(形参接受一个Product对象),并传递给仓储,如清单6-16所示。

 1 //Listing 6-16. Adding an Action Method in the HomeController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     public class HomeController : Controller
 7     {
 8         private IRepository repository;
 9         public HomeController(IRepository repo) => repository = repo;
10         // ...other action methods omitted for brevity...
11         [HttpPost]
12         public IActionResult Delete(Product product)
13         {
14             repository.Delete(product);
15             return RedirectToAction(nameof(Index));//重定向到Index
16         }
17     }
18 }

为了完成这个功能,我为Home控制器使用的Index视图显示的每个产品对象添加了一个form表单元素,以便用户可以触发删除,如清单6-17所示。

小贴士:该表单包含现有的edit按钮元素,这样两个按钮将在浏览器中并排显示。

 1 Listing 6-17. Adding a Form in the Index.cshtml File in the Views/Home Folder
 2 @model IEnumerable<Product>
 3 <h3 class="p-2 bg-primary text-white text-center">Products</h3>
 4 <div class="container-fluid mt-3">
 5     @if (ViewBag.UpdateAll != true)
 6     {
 7         <div class="row">
 8             <div class="col-1 font-weight-bold">Id</div>
 9             <div class="col font-weight-bold">Name</div>
10             <div class="col font-weight-bold">Category</div>
11             <div class="col font-weight-bold text-right">Purchase Price</div>
12             <div class="col font-weight-bold text-right">Retail Price</div>
13             <div class="col"></div>
14         </div>
15         <form asp-action="AddProduct" method="post">
16             <div class="row p-2">
17                 <div class="col-1"></div>
18                 <div class="col"><input name="Name" class="form-control" /></div>
19                 <div class="col"><input name="Category" class="form-control" /></div>
20                 <div class="col">
21                     <input name="PurchasePrice" class="form-control" />
22                 </div>
23                 <div class="col">
24                     <input name="RetailPrice" class="form-control" />
25                 </div>
26                 <div class="col">
27                     <button type="submit" class="btn btn-primary">Add</button>
28                 </div>
29             </div>
30         </form>
31         <div>
32             @if (Model.Count() == 0)
33             {
34                 <div class="row">
35                     <div class="col text-center p-2">No Data</div>
36                 </div>
37             }
38             else
39             {
40                 @foreach (Product p in Model)
41                 {
42                     <div class="row p-2">
43                         <div class="col-1">@p.Id</div>
44                         <div class="col">@p.Name</div>
45                         <div class="col">@p.Category</div>
46                         <div class="col text-right">@p.PurchasePrice</div>
47                         <div class="col text-right">@p.RetailPrice</div>
48                         <div class="col">
49                             <form asp-action="Delete" method="post">
50                                 <a asp-action="UpdateProduct" asp-route-key="@p.Id"
51                                    class="btn btn-outline-primary">
52                                     Edit
53                                 </a>
54                                 <input type="hidden" name="Id" value="@p.Id" />
55                                 <button type="submit" class="btn btn-outline-danger">
56                                     Delete
57                                 </button>
58                             </form>
59                         </div>
60                     </div>
61                 }
62             }
63         </div>
64         <div class="text-center">
65             <a asp-action="UpdateAll" class="btn btn-primary">Edit All</a>
66         </div>
67     }
68     else
69     {
70         @Html.Partial("InlineEditor", Model)
71     }
72 </div>

注意,form表单中只为Id属性写了一个input元素。这就是EF Core从数据库中删除对象所需要的一切,即使需要删除的是Product对象对应的一整行数据。我没有发送没用的其他数据,只发送了主键值,MVC模型绑定器将使用该值创建一个Product对象,并保留该对象的所有其他属性为null或该属性类型的默认值。

注意:这是允许给应用程序其余部分泄漏多少实现细节的另一个例子。为了删除操作,只发送Id值是有效且简单的,但这么干确实依赖于对EF Core工作原理的了解,这就产生了对数据存储方式的依赖性。另一种选择是不去了解实体框架核心的工作原理,但这意味着为将被忽略的属性发送值,这将增加应用程序所需的带宽。一些设计决策是明确的,但另一些则需要在次优选择之间做出艰难的选择。

 

  要测试删除特性,请使用dotnet run启动应用程序,导航到http://localhost:5000,并单击足球项的delete按钮。Product对象将从数据库中删除,如图6-6所示。

 

 

6.4 常见错误和解决方案

用于更新和删除数据的实体框架核心功能相当简单,尽管在将这些功能与MVC模型绑定器从HTTP请求创建的对象一起使用时会遇到一些困难。在下面的部分中,我将描述您最可能遇到的问题,并解释如何解决它们。

 

6.4.1 对象未更新或删除

Objects Are Not Updated or Deleted

如果应用程序貌似正常工作,但对象没有被修改,那么要首先要检查是否忘记在仓储实现类中调用SaveChanges方法。EF Core只会在调用SaveChanges方法之后更新数据库,如果您忘记了,它会悄悄地丢弃更改。

 

6.4.2 引用未指向对象实例

The “Reference Not Set to an Instance of an Object” Exception

此异常是由于试图Update主键属性为null或0的对象而引起的。最常见的原因是忘记在form表单(用于更新对象)中包含主键属性的值。虽然不能更改主键值,但必须确保作为HTML表单的一部分提供值。如果不想让用户看到主键值,可以使用隐藏的input元素(hidden )。

 

6.4.3 无法跟踪实体类的对象

The “Instance of Entity Type Cannot be Tracked” Exception
这是先使用EF Core从数据库查询出一个对象后,调用EF Core的Update方法对MVC模型绑定器创建的同一个对象更新时抛出的异常。数据库上下文类跟踪创建的对象,用于进行更改侦测。当您试图引入由MVC框架创建的冲突对象时,EF Core无法处理。

所以,没有查询baseline基线数据的情况下,你只能使用Update方法。为了避免这个问题,可以从MVC模型绑定器创建的对象中复制属性值,分别赋值给EF Core创建的对象,正如清单6-7所示。

 

译者总结:MVC模型绑定器根据HTTP POST请求传递的数据创建实体对象,传给Action操作方法形参,这个对象显然没有被EF Core跟踪。因为EF Core只可能去跟踪由它自己从数据库查询到的数据(实体对象)。所以如果先用EF Core查询获取一个实体对象;又从MVC模型绑定器得到同一个(同Id的)对象,并试图Update后者,会弹异常。要么你就应该使用清单6-7所示的那种方式。

6.4.4 属性具有临时值

The “Property Has a Temporary Value” Exception

当您试图向应用程序发送HTTP请求以删除对象,但忘记包含主键属性的值时,将发生此异常。MVC模型绑定器将创建一个对象,其主键值是属性类型的默认值。这种默认值只用于在等待数据库服务器存储新对象分配主键值之前,指示一个临时值。

要防止此异常,请确保包含在HTML form表单中提供主键值的input元素。可以将此input元素的类型设置为hidden,以防止用户编辑更改其值。

6.4.5 更新结果是0值

Updates Result in Zero Values

如果更新将数值属性设置为0,可能的原因是HTML表单不包含此属性的值,或者MVC模型绑定器无法将用户输入的值解析为属性数据类型。

要解决第一个问题,请确保数据模型类定义的所有属性都有值。要解决第二个问题,请使用MVC验证特性在无法处理数据值时进行部分更新。

6.5 本章小结

在本章中,我添加了对更新和删除SportsStore应用程序中的对象的支持。我向您展示了如何修改单个对象和执行批量更新,以及如何为EF Core提供baseline基线数据,供其更改侦测功能使用。

我还向您展示了如何删除数据,这对于单类数据模型来说很简单,但是随着数据模型的增长,它会变得更加复杂。在下一章中,我将扩展SportsStore应用程序的数据模型。

posted on 2019-01-08 11:31  困兽斗  阅读(313)  评论(0)    收藏  举报

导航