原文:Async and Stored Procedures with the Entity Framework in an ASP.NET MVC Application
1.为什么使用异步代码:
一个服务器可用的线程数量是有限的,在高负载的情况下所有的可用线程都可能在使用。在这种情况下,在线程被释放之前服务器不能处理新的请求。在同步代码中,很多线程被占用但是实际上没有做任何操作,因为它们在等待I/O完成。在异步代码中,当一个进程在等待I/O完成的过程中,它的线程将会被释放用于处理其他请求。这样,异步代码可以更高效地使用服务器资源,并且使服务器可以没有延迟地处理更多访问。
在之前的.NET版本中,编写和测试异步代码比较复杂、容易出错并且难以调试。在.NET 4.5中,编写、测试和调试异步代码变得非常简单,通常我们需要编写异步代码,除非我们有一个不这样做的理由。异步代码引入少量的开销,对于低流量情况下性能下降可以忽略不计,而对于高流量情况下性能改善的潜力巨大。
更多关于异步编程信息请查看:Use .NET 4.5’s async support to avoid blocking calls。
2.新建Department控制器:
查看产生的Index方法的代码:
public async Task<ActionResult> Index() { var departments = db.Departments.Include(d => d.Administrator); return View(await departments.ToListAsync()); }
- 方法使用
async
关键字标记,用来告诉编译器为方法体的部分产生回调,并且自动创建返回的Task<ActionResult>
对象。 - 返回类型从
ActionResult
变为Task<ActionResult>
。 await
关键字被用于web service调用。当编译器遇到这个关键字,会在幕后将该方法分为两部分。第一部分以异步操作开始时结束。第二部分被放入操作完成后调用的回调方法中。- 异步版本的ToList扩展被调用。
为什么 departments.ToList
语句被修改而departments = db.Departments语句
没有被修改呢?因为,只有执行查询或者被发送到数据库的命令才会异步执行。departments = db.Departments语句
生成查询,但是查询在 departments.ToList
语句才被执行。因此,只有ToList方法被异步执行。下面的代码中亦是同一原因:
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); }
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>
在Create、Delete、Details和Edit视图,将 InstructorID
的标题改为“Administrator”:
在Create和Edit视图:
<label class="control-label col-md-2" for="InstructorID">Administrator</label>
在Delete和Details视图:
<dt> Administrator </dt>
运行:
在使用EF异步编程需要注意的问题:
- 异步代码是线程不安全的。因此,不要试图使用同一个上下文实例并行处理多个操作。
- 如果想要利用异步代码的性能优势,需要保证我们使用的库包(例如分页)在调用任何EF方法查询数据库时同样使用异步。
3.使用存储过程插入、更新和删除:
在早期的EF版本中,我们可以通过执行原生的SQL查询来使用存储过程查询数据,但是不能使用存储过程执行更新操作。在EF6中很容易配置Code First使用存储过程。
3.1.在DAL\SchoolContext.cs中添加代码:
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(); }
该行代码告诉EF在对Department进行插入、更新和删除操作时使用存储过程。
3.2.在Package Manage Console输入命令:
add-migration DepartmentSP
打开Migrations\<timestamp>_DepartmentSP.cs,可以看到:
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.3.在Package Manage Console输入命令:
update-database
3.4.运行:
Code First创建默认的存储过程名。如果我们使用一个已经存在的数据库,为了使用数据库中已经定义的存储过程,我们需要指定存储过程的名字。更多信息请参考: Entity Framework Code First Insert/Update/Delete Stored Procedures。
如果我们想要指定产生的存储过程做什么,我们可以编辑迁移中的Up方法中产生存储过程的代码。通过这种方式,我们修改的代码不管是迁移运行时还是当部署后迁移自动在产品数据库运行都会起作用。
如果我们想要修改一个在早期的迁移中创建的存储过程,我们可以使用Add-Migration命令来产生一个空白的迁移,然后手动编写调用AlterStoredProcedure方法的代码。
4.部署到Azure:
本节操作需要完成了之前可选的部署到Azure。如果我们遇到迁移错误,并且决定通过删除本地项目中的数据库来解决此问题,那么可以跳过本节。
4.1.在Visual Studio中选中项目右键,Solution Explorer选中Publish。
4.2.点击Publish。
4.3.测试程序是否正常工作。
其他的EF资源:ASP.NET Data Access - Recommended Resources。