MVC5 Entity Framework学习之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变量中不会包括不论什么值,视图中会显示一个空的文本框和一个提交button。

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


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


打开ViewsCourseUpdateCourseCredits.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)。

打开ViewsCourseIndex.cshtml中,在table開始标记之前,加入一个下拉列表和一个提交button。

@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时你是无法改动图表中的数据模型的,可是你能够移动它们以使它更easy理解。



Entity Framework源码

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

尽管源码是开放的,但Entity Framework 全然是由微软提供支持的产品。微软Entity Framework 团队会不断地接收反馈并測试全部的代码更改以确保每一个公布版本号的质量。

原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application

欢迎转载。请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/39560037

项目源代码:https://github.com/johnsonz/MvcContosoUniversity

还大家一个健康的网络环境,从你我做起

THE END

原文地址:https://www.cnblogs.com/lcchuguo/p/5234753.html