教程:在 ASP.NET MVC 应用中将异步和存储过程与 EF 配合使用

在前面的教程中,你学习了如何使用同步编程模型读取和更新数据。 本教程介绍如何实现异步编程模型。 异步代码可以帮助应用程序更好地执行,因为它可以更好地利用服务器资源。

本教程还介绍了如何使用存储过程对实体执行插入、更新和删除操作。

最后,将应用程序重新部署到 Azure,以及自首次部署以来实现的所有数据库更改。

下图是一些将会用到的页面。

“部门”页

创建部门

在本教程中,你将了解:

  • 了解异步代码
  • 创建部门控制器
  • 使用存储过程
  • 部署到 Azure

先决条件

为何使用异步代码

Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使服务器资源能够更高效地使用,并且服务器能够处理更多的流量,而不会延迟。

在早期版本的 .NET 中,编写和测试异步代码非常复杂、容易出错且难以调试。 在 .NET 4.5 中,编写、测试和调试异步代码要容易得多,除非有理由不这样做,否则通常应该编写异步代码。 异步代码确实引入了少量的开销,但对于低流量情况,性能命中率微乎其微,而对于高流量情况,潜在的性能改进是实质性的。

有关异步编程的详细信息,请参阅 使用 .NET 4.5 的异步支持以避免阻塞调用

创建部门控制器

创建与之前控制器相同的部门控制器,但这次选中“ 使用异步控制器操作 ”复选框。

以下突出显示显示了向同步代码添加的内容,使 Index 该方法成为异步代码:

public async Task<ActionResult> Index()
{
    var departments = db.Departments.Include(d => d.Administrator);
    return View(await departments.ToListAsync());
}

应用了四项更改,使 Entity Framework 数据库查询能够异步执行:

  • 该方法使用 async 关键字进行标记,该关键字指示编译器为方法正文的各个部分生成回调,并自动创建 Task<ActionResult> 返回的对象。
  • 返回类型已从ActionResultTask<ActionResult>更改为 。 此 Task<T> 类型表示使用类型 T的结果正在进行的工作。
  • 关键字 await 已应用于 Web 服务调用。 当编译器看到此关键字时,它会在后台将该方法拆分为两个部分。 第一部分以异步启动的操作结束。 第二部分将放入回调方法中,该方法在操作完成时调用。
  • 调用扩展方法的 ToList 异步版本。

为什么语句 departments.ToList 已修改,但未 departments = db.Departments 修改该语句? 原因是只异步执行导致查询或命令发送到数据库的语句。 该 departments = db.Departments 语句设置查询,但在调用该方法之前 ToList 不会执行查询。 因此,仅 ToList 异步执行该方法。

Details方法和Delete HttpGet Edit方法中,Find该方法是导致查询发送到数据库的方法,因此是异步执行的方法:

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Department department = await db.Departments.FindAsync(id);
    if (department == null)
    {
        return HttpNotFound();
    }
    return View(department);
}

CreateDeleteConfirmed方法中,它是SaveChanges导致命令执行的方法调用,而不是导致内存中的实体被修改的db.Departments.Add(department)语句。 HttpPost Edit

public async Task<ActionResult> Create(Department department)
{
    if (ModelState.IsValid)
    {
        db.Departments.Add(department);
    await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }

打开 Views\Department\Index.cshtml,并将模板代码替换为以下代码:

@model IEnumerable<ContosoUniversity.Models.Department>
@{
    ViewBag.Title = "Departments";
}
<h2>Departments</h2>
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Budget)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.StartDate)
        </th>
    <th>
            Administrator
        </th>
        <th></th>
    </tr>
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Budget)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.StartDate)
        </td>
    <td>
            @Html.DisplayFor(modelItem => item.Administrator.FullName)
            </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
    </tr>
}
</table>

此代码将标题从索引更改为部门,将管理员名称移到右侧,并提供管理员的完整名称。

在“创建”、“删除”、“详细信息”和“编辑”视图中,将 InstructorID 字段的标题更改为“管理员”,就像在课程视图中将部门名称字段更改为“Department”。

在“创建”和“编辑”视图中,使用以下代码:

<label class="control-label col-md-2" for="InstructorID">Administrator</label>

在“删除”和“详细信息”视图中,使用以下代码:

<dt>
    Administrator
</dt>

运行应用程序,然后单击“ 部门 ”选项卡。

所有操作都与其他控制器相同,但在此控制器中,所有 SQL 查询都以异步方式执行。

在实体框架中使用异步编程时需要注意的一些事项:

  • 异步代码不是线程安全的。 换句话说,不要尝试使用相同的上下文实例并行执行多个操作。
  • 如果你想要利用异步代码的性能优势,请确保你所使用的任何库和包在它们调用导致 Entity Framework 数据库查询方法时也使用异步。

使用存储过程

某些开发人员和 DBA 更喜欢使用存储过程进行数据库访问。 在早期版本的 Entity Framework 中,可以通过执行原始 SQL 查询来使用存储过程检索数据,但不能指示 EF 使用存储过程执行更新操作。 在 EF 6 中,可以轻松地将 Code First 配置为使用存储过程。

  1. DAL\SchoolContext.cs中,将突出显示的代码添加到 OnModelCreating 方法。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Entity<Course>()
            .HasMany(c => c.Instructors).WithMany(i => i.Courses)
            .Map(t => t.MapLeftKey("CourseID")
                .MapRightKey("InstructorID")
                .ToTable("CourseInstructor"));
        modelBuilder.Entity<Department>().MapToStoredProcedures();
    }
    

    此代码指示 Entity Framework 使用存储过程对实体执行插入、更新和删除操作 Department

  2. 在包管理控制台中,输入以下命令:

    add-migration DepartmentSP

    打开 Migrations\<timestamp>_DepartmentSP.cs 在创建插入、更新和删除存储过程的方法中 Up 查看代码:

    public override void Up()
    {
        CreateStoredProcedure(
            "dbo.Department_Insert",
            p => new
                {
                    Name = p.String(maxLength: 50),
                    Budget = p.Decimal(precision: 19, scale: 4, storeType: "money"),
                    StartDate = p.DateTime(),
                    InstructorID = p.Int(),
                },
            body:
                @"INSERT [dbo].[Department]([Name], [Budget], [StartDate], [InstructorID])
                  VALUES (@Name, @Budget, @StartDate, @InstructorID)
                  
                  DECLARE @DepartmentID int
                  SELECT @DepartmentID = [DepartmentID]
                  FROM [dbo].[Department]
                  WHERE @@ROWCOUNT > 0 AND [DepartmentID] = scope_identity()
                  
                  SELECT t0.[DepartmentID]
                  FROM [dbo].[Department] AS t0
                  WHERE @@ROWCOUNT > 0 AND t0.[DepartmentID] = @DepartmentID"
        );
        
        CreateStoredProcedure(
            "dbo.Department_Update",
            p => new
                {
                    DepartmentID = p.Int(),
                    Name = p.String(maxLength: 50),
                    Budget = p.Decimal(precision: 19, scale: 4, storeType: "money"),
                    StartDate = p.DateTime(),
                    InstructorID = p.Int(),
                },
            body:
                @"UPDATE [dbo].[Department]
                  SET [Name] = @Name, [Budget] = @Budget, [StartDate] = @StartDate, [InstructorID] = @InstructorID
                  WHERE ([DepartmentID] = @DepartmentID)"
        );
        
        CreateStoredProcedure(
            "dbo.Department_Delete",
            p => new
                {
                    DepartmentID = p.Int(),
                },
            body:
                @"DELETE [dbo].[Department]
                  WHERE ([DepartmentID] = @DepartmentID)"
        );    
    }
    
  3. 在包管理控制台中,输入以下命令:

    update-database

  4. 在调试模式下运行应用程序,单击“ 部门 ”选项卡,然后单击“ 新建”。

  5. 输入新部门的数据,然后单击“ 创建”。

  6. 在 Visual Studio 中 ,查看“输出 ”窗口中的日志,以查看存储过程用于插入新的 Department 行。

    部门插入 SP

Code First 创建默认存储过程名称。 如果使用现有数据库,可能需要自定义存储过程名称,才能使用数据库中已定义的存储过程。 有关如何执行此操作的信息,请参阅 Entity Framework Code First Insert/Update/Delete 存储过程

如果要自定义生成的存储过程执行的操作,可以编辑创建存储过程的迁移 Up 方法的基架代码。 这样,每当迁移运行时,更改都会反映,在部署后迁移在生产环境中自动运行时,将应用于生产数据库。

如果要更改在以前的迁移中创建的现有存储过程,可以使用“添加迁移”命令生成空白迁移,然后手动编写调用 AlterStoredProcedure 方法的代码。

部署到 Azure

本部分要求完成本系列迁移和部署教程中的可选“将应用部署到 Azure”部分。 如果在本地项目中删除数据库时发生了迁移错误,请跳过本部分。

  1. 在 Visual Studio 中,在“解决方案资源管理器”中右键单击项目,并从上下文菜单中选择“发布”。

  2. 单击“发布”。

    Visual Studio 将应用程序部署到 Azure,并在 Azure 中运行的默认浏览器中打开该应用程序。

  3. 测试应用程序以验证它是否正常工作。

    首次运行访问数据库的页时,Entity Framework 将运行使数据库与当前数据模型保持最新所需的所有迁移 Up 方法。 现在,可以使用自上次部署以来添加的所有网页,包括本教程中添加的“部门”页。

获取代码

下载已完成的项目

其他资源

可以在 ASP.NET 数据访问 - 建议的资源中找到 指向其他 Entity Framework 资源的链接。

后续步骤

在本教程中,你将了解:

  • 了解异步代码
  • 创建了部门控制器
  • 已用存储过程
  • 部署到 Azure

请继续学习下一篇文章,了解如何在多个用户同时更新同一实体时处理冲突。