MVC5+EF6--12 Entity Framework高级功能

近期学习MVC5+EF6,找到了Microsoft的原文,一个非常棒的系列,Getting Started with Entity Framework 6 Code First using MVC 5,网址:http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application。

    这个系列的原文,可以上面网址找到。我也从网上找到了相关的译文。申明,这些译文,我不是原创,而是从网上找来的,为避免下次还要网上查询,现将这些译文整理放在上面。以后找时间将原文地址附上。

MVC5+EF6--12 Entity Framework高级功能

原文网址:

在之前的文章中,你已经学习了如何实现每个层次结构一个表继承。本节中你将学习使用Entity Framework Code First来开发ASP.NET web应用程序时可以利用的高级功能。

在本节中你将重用之前已经创建的页面,接下来你需要新建一个页面并使用原始SQL来批量更新数据库中所有Course的学分。

                       

在Department Edit页面中添加新的验证逻辑并使用非跟踪查询。

 

执行原始SQL查询

Entity FrameworkCode First API包含有可以让你直接向数据库发送SQL命令的方法。以下几种方法可以实现这种功能:

  • 使用DbSet.SqlQuery方法来进行查询并返回实体类型,返回的对象类型必须是预期的DbSet对象,它们会被数据库上下文自动跟踪,除非你禁用跟踪功能。
  • 使用Database.SqlQuery方法来进行查询并返回非实体类型。返回的数据不会被数据库上下文跟踪,即使你使用该方法来检索实体类型。
  • 使用Database.ExecuteSqlCommand执行非查询类型命令。

使用Entity Framework的优势之一是它可以避免代码和实现存取数据的特定方法具有较高的耦合度,它通过自动生成SQL查询和命令来实现这一点,可以让你不必手工编写大量的代码。但在特殊情况下,当你需要执行特定的SQL查询时,你必须手工编写它们。

当你在web应用程序中执行SQL命令时,你必须采取必要的预防措施来保护你的站点不受SQL注入攻击。要做到这一点,其中一种方法就是使用参数化的查询来确保通过web页面提交的字符串不会被解释为SQL命令。在本节中,你将学习如何使用参数化的查询来处理用户输入。

执行查询并返回实体

DbSet<TEntity>类提供了一个方法,你可以使用该方法来执行查询并返回一个实体类型TEntity。接下来你需要修改Department控制器中的Details方法以便观察该方法是如何工作的。

打开DepartmentController.cs,使用db.Departments.SqlQuery方法替换db.Departments.Find 方法

public async Task<ActionResult> Details(int? id)
{
  if (id == null)
  {
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
 
  // Commenting out original code to show how to use a raw SQL query.
  //Department department = await db.Departments.FindAsync(id);
 
  // Create and execute raw SQL query.
  string query = "SELECT * FROM Department WHERE DepartmentID = @p0";
  Department department = await db.Departments.SqlQuery(query, id).SingleOrDefaultAsync();
  
  if (department == null)
  {
    return HttpNotFound();
  }
  return View(department);
}

运行项目,选择Departments选项卡,点击Details链接,验证新代码是否工作正常。

 

执行查询并返回其他类型的对象

之前你为About页面添加了一个学生统计功能用来显示每年的学生入学数量。这里使用了LINQ来进行操作:

var data = from student in db.Students
  group student by student.EnrollmentDate into dateGroup
  select new EnrollmentDateGroup()
  {
      EnrollmentDate = dateGroup.Key,
      StudentCount = dateGroup.Count()
  };

如果你希望通过直接编写SQL语句来进行查询而不是使用LINQ,要做到这一点,你需要执行一个能够返回非实体类型对象的查询,这意味着你需要使用Database.SqlQuery方法。

打开HomeController.cs,使用下面的代码替换

public ActionResult About()
{
  // Commenting out LINQ to show how to do the same thing in SQL.
  //IQueryable<EnrollmentDateGroup> = from student in db.Students
  //                        group student by student.EnrollmentDate into dateGroup
  //                        select new EnrollmentDateGroup()
  //                        {
  //                                    EnrollmentDate = dateGroup.Key,
  //                                    StudentCount = dateGroup.Count()
  //                        };
 
  // SQL version of the above LINQ code.

  string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "

    + "FROM Person "

    + "WHERE Discriminator = ‘Student‘ "

    + "GROUP BY EnrollmentDate";

  IEnumerable<EnrollmentDateGroup> data = db.Database.SqlQuery<EnrollmentDateGroup>(query);

 

  return View(data.ToList());

}

运行项目,打开About页面,显示的数据和之前的是一样的。

 

执行Update查询

假设Contoso University administrator希望能够在数据库中执行批量操作,例如修改每一门Course的学分。但是如果学校有大量Course的话,针对每一门Course分别进行更新无疑效率是非常低下。在本节中,你将创建一个web页面来使用户能够选择是否修改所有Course的学分,你可以通过执行SQL Update语句来实现这一功能。

 

打开CourseController.cs,添加HttpGet和HttpPost UpdateCourseCredits方法

public ActionResult UpdateCourseCredits()
{
  return View();
}
 
[HttpPost]
public ActionResult UpdateCourseCredits(int? multiplier)
{
  if (multiplier != null)
  {
    ViewBag.RowsAffected = db.Database.ExecuteSqlCommand("UPDATE Course SET Credits = Credits * {0}", multiplier);
  }
  return View();
}

当控制器处理HttpGet请求时,ViewBag.RowsAffected变量中不会包含任何值,视图中会显示一个空的文本框和一个提交按钮。

当Update 按钮被点击时,HttpPost方法被调用,multiplier含有文本框中输入的值,接下来执行更新Course的SQL语句并将返回的受影响的行数赋值给ViewBag.RowsAffected变量。当视图获取到该变量的值后将其显示出来。

 

打开CourseController.cs,在UpdateCourseCredits方法上单击右键,选择Add View

 

打开Views\Course\UpdateCourseCredits.cshtml,使用下面的代码替换

@model ContosoUniversity.Models.Course
 
@{
  ViewBag.Title = "UpdateCourseCredits";
}
 
<h2>Update Course Credits</h2>
 
@if (ViewBag.RowsAffected == null)
{
  using (Html.BeginForm())
  {
    <p>
      Enter a number to multiply every course‘s credits by: @Html.TextBox("multiplier")
    </p>
    <p>
      <input type="submit" value="Update" />
    </p>
  }
}
@if (ViewBag.RowsAffected != null)
{
  <p>
    Number of rows updated: @ViewBag.RowsAffected
  </p>
}
<div>
  @Html.ActionLink("Back to List", "Index")
</div>

运行项目,选择Courses选项卡,运行UpdateCourseCredits方法

 

点击Update,查看返回的受影响的Course数量

 

点击Back to List ,查看修改的学分

 

非跟踪查询

当数据库上下文检索到数据行并创建实体对象并将其呈现时,默认情况下它会跟踪内存中的实体是否与数据库中的同步。内存中的数据作为缓存并在更新实体时被使用,这种缓存在web应用程序中通常不是必需的,因为上下文实例的生命期通常是短暂的(每个请求都会创建一个新实例并最终销毁它),并且上下文经常在读取实体并在再次使用它们之前就将它们销毁了。

可以使用AsNoTracking方法来禁用内存实体对象的跟踪功能。在以下几种典型场景中,你可能需要禁用跟踪功能:

  • 一个查询需要检索大量的数据,而禁用跟踪可能会显著提高性能。
  • 你希望附加一个实体以便更新它,但是之前基于不同的目的你已经获取了同一个实体对象,由于该实体已经被数据库上下文跟踪,所以你无法附加你希望更改的实体。要处理这种情况,其中一种方法是在查询中使用AsNoTracking选项。

在本节中你将会实现上面第二个场景的业务逻辑。具体来说,你将强制执行一条一名instructor 不能作为多个department的administrator 的业务规则。(基于到目前你已经完成的Department页面的功能,可能已经存在多个department具有同一个administrator的情况,在生产环境中,你需要执行一条新的规则来处理已经存在的数据,但是在本示例中不是必需的。 )

打开DepartmentController.cs,添加一个新方法,并在Edit和Create方法中来调用它以确保没有多个department具有同一个administrator

private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
  if (department.InstructorID != null)
  {
    Department duplicateDepartment = db.Departments
      .Include("Administrator")
      .Where(d => d.InstructorID == department.InstructorID)
      .FirstOrDefault();
    if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
    {
      string errorMessage = String.Format(
        "Instructor {0} {1} is already administrator of the {2} department.",
        duplicateDepartment.Administrator.FirstMidName,
        duplicateDepartment.Administrator.LastName,
        duplicateDepartment.Name);
      ModelState.AddModelError(string.Empty, errorMessage);
    }
  }
}

在HttpPost Edit方法中的try代码块中添加代码以便在没有验证错误的情况下调用该方法

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
  [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, PersonID")]
   Department department)
{
  try
  {
    if (ModelState.IsValid)
    {
      ValidateOneAdministratorAssignmentPerInstructor(department);
    }
 
    if (ModelState.IsValid)
    {
      db.Entry(department).State = EntityState.Modified;
      db.SaveChanges();
      return RedirectToAction("Index");
    }
  }
  catch (DbUpdateConcurrencyException ex)
  {
    var entry = ex.Entries.Single();
    var clientValues = (Department)entry.Entity;

运行项目,打开Department Edit页面,将某一department的administrator更改为已经是另一个department的administrator的instructor,查看错误信息

 

再次运行Department Edit页面,更改Budget,点击Save,你会看到页面中显示了由ValidateOneAdministratorAssignmentPerInstructor方法引发的错误信息

 

异常信息:

Attaching an entity of type ‘ContosoUniversity.Models.Department‘ failed because another entity of the same type already has the same primary key value. This can happen when using the ‘Attach‘ method or setting the state of an entity to ‘Unchanged‘ or ‘Modified‘ if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the ‘Add‘ method or the ‘Added‘ entity state to track the graph and then set the state of non-new entities to ‘Unchanged‘ or ‘Modified‘ as appropriate.

该错误是由以下一系列事件引起的:

  • Edit方法调用了ValidateOneAdministratorAssignmentPerInstructor方法用来检索由Kim Abercrombie作为administrator的所有department,这会导致English      department 被读取,由于此读取操作,该English      department实体正在被数据库上下文跟踪。
  • Edit方法尝试设置由模型绑定器创建的English department实体的标志位,这会导致上下文尝试附加该实体。但是上下文无法附加由模型绑定器创建的该实体,因为上下文正在跟踪English department的另一个实体。

解决这一问题的其中一个方法是保持跟踪内存中通过验证查询检索到的department 实体的上下文,但这样做是没有意义的,因为你不需要更新该实体或重新从内存中读取它。

打开DepartmentController.cs,在ValidateOneAdministratorAssignmentPerInstructor方法中指定为非跟踪

Department duplicateDepartment = db.Departments
   .Include("Administrator")
   .Where(d => d.PersonID == department.PersonID)
   .AsNoTracking()
   .FirstOrDefault();

再次尝试修改department的Budget,这一次操作将会成功。

检查发送到数据库的SQL

有时候,查看实际被发送到数据库的SQL是非常有用的,之前你已经学习了如何使用拦截器来实现这一功能,接下来将向你展示如何在不使用拦截器的情况下实现该功能。作为尝试,你将通过添加诸如预先加载、过滤及排序功能来检查将要发生的事情。

打开Controllers/CourseController,修改Index方法,临时禁用预先加载

public ActionResult Index()
{
    var courses = db.Courses;
    var sql = courses.ToString();
    return View(courses.ToList());
}

然后在return语句上设置一个断点,并按下F5在调试模式下运行该项目,打开Course Index页面,运行到断点时,检查query变量,你将看到发送到SQL Server的查询语句,它是一个简单的select语句。

{SELECT 
[Extent1].[CourseID] AS [CourseID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Credits] AS [Credits], 
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}

点击放大器图标,在Text Visualizer中查看查询语句

 

 

接下来你需要向Course Index页面添加一个下拉列表以便用户可以用来筛选特定的department。你可以使用标题来进行排序,并将Department 导航属性指定为预先加载。

打开CourseController.cs,修改Index方法:

public ActionResult Index(int? SelectedDepartment)
{
  var departments = db.Departments.OrderBy(q => q.Name).ToList();
  ViewBag.SelectedDepartment = new SelectList(departments, "DepartmentID", "Name", SelectedDepartment);
  int departmentID = SelectedDepartment.GetValueOrDefault();
 
  IQueryable<Course> courses = db.Courses
    .Where(c => !SelectedDepartment.HasValue || c.DepartmentID == departmentID)
    .OrderBy(d => d.CourseID)
    .Include(d => d.Department);
  var sql = courses.ToString();
  return View(courses.ToList());
}

仍然在return上设置断点。

该方法接收SelectedDepartment 参数中的选中的下拉列表中的值,如果没有任何选项被选择,该参数为null。

一个包含所有department的SelectList集合被传递给视图的下拉列表。传递给SelectList构造函数的参数指定了值字段名,文本字段名和被选中的选项。

对于Course仓库的Get方法,代码为Department导航属性指定了的筛选器表达式,排序和延迟加载。如果下拉下表中没有选中任何选项,筛选器表达式总是返回true(也就是说SelectedDepartment 值为null)。

打开Views\Course\Index.cshtml中,在table开始标记之前,添加一个下拉列表和一个提交按钮。

@using (Html.BeginForm())
{
    <p>Select Department: @Html.DropDownList("SelectedDepartment","All")   
    <input type="submit" value="Filter" /></p>
}

运行项目,打开Course Index页面,在一次遇到断点时继续运行以便显示页面,从下拉列表中选择一个department并点击Filter

 

第一次运行到断点时,代码正在为下拉列表查询department数据,跳过此次断点并在下次断点处查看query变量。

SELECT 
  [Project1].[CourseID] AS [CourseID], 
  [Project1].[Title] AS [Title], 
  [Project1].[Credits] AS [Credits], 
  [Project1].[DepartmentID] AS [DepartmentID], 
  [Project1].[DepartmentID1] AS [DepartmentID1], 
  [Project1].[Name] AS [Name], 
  [Project1].[Budget] AS [Budget], 
  [Project1].[StartDate] AS [StartDate], 
  [Project1].[InstructorID] AS [InstructorID], 
  [Project1].[RowVersion] AS [RowVersion]
  FROM ( SELECT 
    [Extent1].[CourseID] AS [CourseID], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Credits] AS [Credits], 
    [Extent1].[DepartmentID] AS [DepartmentID], 
    [Extent2].[DepartmentID] AS [DepartmentID1], 
    [Extent2].[Name] AS [Name], 
    [Extent2].[Budget] AS [Budget], 
    [Extent2].[StartDate] AS [StartDate], 
    [Extent2].[InstructorID] AS [InstructorID], 
    [Extent2].[RowVersion] AS [RowVersion]
    FROM  [dbo].[Course] AS [Extent1]
    INNER JOIN [dbo].[Department] AS [Extent2] ON [Extent1].[DepartmentID] = [Extent2].[DepartmentID]
    WHERE @p__linq__0 IS NULL OR [Extent1].[DepartmentID] = @p__linq__1
  )  AS [Project1]
  ORDER BY [Project1].[CourseID] ASC

你可以看到查询中包含了一个JOIN连接查询来加载Department和Course数据。

删除代码中的var sql = conrses.ToString();

仓库和工作 单元 模式

许多开发人员实现仓库和工作单元模式作为包装器,这些模式倾向于在应用程序的数据访问层和业务逻辑层之间创建了一个抽象层。实现这些模式有助于将应用程序从数据存储的更改中隔离出来,并且可以促进自动化的单元测试或测试驱动开发(TDD)。但是,使用EF编写代码来实现这些模式并不是最佳的选择。有以下几个原因:

  • EF上下文类本身将你的代码从特定的数据存储中隔离。
  • EF上下文类可以作为工作单元类来进行数据库更新,就像使用EF所做的那样。
  • Entity Framework 6中的特性可以让你无需编写仓库代码就可实现TDD。

代理类

当Entity Framework创建实体实例时(例如,当你执行一个查询时),它总是创建作为动态生成的派生对象的实例并作为实体对象的代理。例如下面的两个调试器截图,在第一张截图中,你可以看到在实例化该实体后预期为Student类型的student变量,在第二张截图中,你可以看到在使用EF从数据库读取student 实体之后的代理类。

 

 

当实体的属性被访问时,该代理类会重写实体的一些虚属性用来为执行动作自动插入钩子,该机制的功能之一就是用于延迟加载。

大多数时候你不用关心这种代理的使用,但也有例外:

  • 某些情况下,你可能希望阻止Entity      Framework创建代理实例。例如,通常你希望对一个POCO类而不是代理类的实体进行序列化。其中一种避免序列化问题的方法是序列化数据传输对象(DTOs)而不是实体对象,另一种方法就是禁用代理创建。
  • 当你使用new运算符实例化一个实体类时,你得到的不是代理实例,这意味着你无法使用诸如延迟加载和自动跟踪功能。通常你并不需要使用延迟加载,因为你正在创建一个并不在数据库中的新的实体,并且如果你显式地将实体标记为Added,你通常不需要变更跟踪。然而,如果你需要使用延迟加载和变更跟踪,你可以通过使用DbSet类的Create方法来创建一个新的实体实例代理。
  • 你可能希望从一个代理对象得到一个真实的实体类型,你可以使用ObjectContext类的GetObjectType方法来获得代理类型实例的实际实体类型。

自动变化检测

Entity Framework通过比较实体的当前值和原始值来确定该实体是否被更改,原始值在实体被查询或附加时被存储。一些会导致自动变化监测的方法如下:

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.Savechanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

如果你正在跟踪大量的实体,并且你在一个循环中多次调用这些方法,通过使用AutoDetectChangesEnabled属性来暂时禁用自动变化监测可以获得得程序性能的显著提升。

自动验证

当你调用SaveChanges方法时,在默认情况下,Entity Framework会在更新数据到数据库之前验证所有被更改的实体中的所有属性。如果你更新了大量的实体并且已经对数据进行了验证,该操作是不必要的,你可以通过暂时禁用验证来减少保存这些更改的处理时间,你可以使用ValidateOnSaveEnabled属性来做到这一点。

Entity Framework Power Tools

Entity Framework Power Tools是一个Visual Studio扩展,你可以使用它来创建数据模型图。该工具还有一些其它功能,比如基于现有数据库表来生成实体类。安装该工具后,你会在上下文菜单中看到一些附加选项,例如,当你在Solution Explorer上右键单击时,你会发现一个生成图表的选项。当你正在使用Code First时你是无法修改图表中的数据模型的,但是你可以移动它们以使它更容易理解。

 

 

Entity Framework源代码

你可以从 http://entityframework.codeplex.com /获得Entity Framework 6的源代码,除了源代码,你还可以获得每晚构建、问题跟踪、特性规范、设计会议笔记等功能,你可以提交bug并贡献你自己的增强功能。

虽然源代码是开放的,但Entity Framework 完全是由微软提供支持的产品。微软Entity Framework 团队会不断地接收反馈并测试所有的代码更改以确保每个发布版本的质量。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。