为ASP.NET MVC应用程序使用异步及存储过程

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第九篇:为ASP.NET MVC应用程序使用异步及存储过程

原文: Async and Stored Procedures with the Entity Framework in an ASP.NET MVC Application

在之前的教程中,您已经学习了如何使用同步编程模型来读取和更新数据。在本教程中您将看到如何实现异步编程模型。因为能够更好地使用服务器资源,异步代码可以帮助应用程序更好地执行。

在本教程中您还会看到如何使用存储过程对实体进行插入、更新和删除操作。

下面的插图显示了您将要编写的页面:

为什么要使用麻烦的异步代码

一个WEB服务器只有很有限的可用线程,并且在高负载的情况下,可能所有的线程都会在使用中。当发生这种情况时,服务器将无法处理新的请求,直到有线程被释放。在同步代码的情况下,多个线程可能会关联起来,但实际上它们并不作任何工作而只是在等待IO完成。使用异步代码,当一个进程正在等待IO完成时,它的线程可以服务器腾出从而用于处理其他请求。因此,异步代码可以更高效地使用服务器资源,并且服务器能够在不延迟的情况下处理更多的流量。

在较早版本的.NET中,编写和测试异步代码是一件复杂、容易出错且难以调试的工作。在.Net 4.5中,编写、测试和调试异步代码变得简单起来,你应当总是使用异步代码,除非有理由不允许你这样做。异步代码会花费很少量的开销,但对于低流量情况下性能的损失是微不足道的。对于高流量的情况下,潜在的性能提示是巨大的。

有关异步编程的更多信息,请参阅 Use .NET 4.5’s async support to avoid blocking calls 。

创建系控制器

使用之前你创建其他控制器相同的方式来创建一个系控制器,但这次我们选择使用异步控制器操作选项。

下面的代码中,高亮部分显示了异步方法和同步方法的不同之处:

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

我们应用了四个更改来启用实体框架数据库执行异步查询:

  • 该方法使用了async关键字,它告诉编译器生成回调方法体的部分,并自动创建Task<ActionResult>返回对象。

  • 返回类型从ActionResult更改为Task<ActionResult>。Task<T>类型表示正在进行的任务具有类型为T的结果。

  • await关键字被应用到web服务调用。当编译器看到有此关键字时,在后台将该方法分为两个部分。第一部分结束于异步操作启动,第二部分被放入一个操作完成时的回调方法。

  • 调用了ToList扩展方法的异步版本。

为什么只修改departments.ToList语句而不是departments= db.Departments语句?原因是只有被发送的数据库执行的查询或语句才能够使用异步执行。departments=db.Departments语句设置了一个查询,但直到调用ToList方法时该查询都不会执行。因此,只有ToList方法是异步执行的。

在Details方法和Httpget的Edit和Delete方法中,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);
        }

在Create,HttpPost的Edit和DeleteConfirmed方法中,是SaveChanges方法导致命令执行,而像db.Department.Add(department)方法只是导致实体在内存中的修改。

        public async Task<ActionResult> Create([Bind(Include="DepartmentID,Name,Budget,StartDate,InstructorID")] Department department)
        {
            if (ModelState.IsValid)
            {
                db.Departments.Add(department);
                await db.SaveChangesAsync();
                return RedirectToAction("Index");
            }

            ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "LastName", department.InstructorID);
            return View(department);
        }

打开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字段的标题更改为"系主任",类似之前你在课程视图中将系名称字段更改为"系"中那样。

在创建和编辑视图使用下面的代码:

   @Html.DisplayFor(model => model.Department.Name)

在删除和详细视图使用下面的代码:

        <dt>
            Administrator
        </dt>

运行该应用程序,并点击系选项卡。

程序正常地运行,就跟其他的控制器一样。但在此控制器中,所有SQL查询都是异步执行的。

当您在实体框架中使用异步编程要注意的一些事情:

  • 异步代码不是线程安全的。换言之,不要使用同一个上下文实例以并行方式来执行多个操作。

  • 如果你想要利用异步代码的性能优势,请确保您正在使用的所有库软件包(例如分页),在包中进行的数据库查询等任何实体框架方法也使用异步执行。

用于插入、更新和删除的存储过程

一些开发人员和DBA更愿意使用存储过程来访问数据库。早期版本的实体框架中,您可以使用执行原始SQL查询的方式来检索数据来执行存储过程,但您不能使用存储过程来用于更新操作。在实体框架6中,您可以很容易地配置Code First来使用存储过程。

在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();
        }

此代码指示实体框架使用存储过程来进行Department实体的插入、更新和删除操作。

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

add-migration DepartmentSP

打开Migrations\<时间戳>_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)"
            );
            
        }

在软件包管理器控制台中,输入以下命令:

update-database 

在调试模型下运行该应用程序,单击系选项卡,然后单击创建。为一个新系输入相关数据然后单击创建。

在VS中的输出窗口查看日志。

Code First使用默认名创建了存储过程。如果您正在使用现有的数据库,您可能需要自定义存储过程的名称,有关如何操作的信息,请参阅 Entity Framework Code First Insert/Update/Delete Stored Procedures 。

如果你想要自定义存储过程,你可以编辑迁移中的脚手架代码里的Up方法来创建存储过程。使用这种方法时您的更改将在应用迁移时或部署到生产环境后自动进行。

如果你想修改一个在之前的迁移中已经创建的的存储过程,你可以使用Add-Migration命令来生成一个空白的迁移,然后手动编写代码调用AlterStoredProcedure方法。

部署到Windows Azure

本章跳过……

总结

在本教程中,您看到了如何提高服务器效率。通过编写异步执行代码以及使用存储过程来进行插入、更新和删除操作。在接下来的教程中,您将看到当多个用户试图编辑同一条纪录时如何防止数据丢失。

作者信息

Tom Dykstra - Tom Dykstra 是微软Web平台及工具团队的高级程序员,作家。