【EF6学习笔记】(十二)EF高级应用场景

本篇原文连接:Advanced Entity Framework Scenariosios

本篇主要讲一些使用Code First创建ASP.NET WEB应用的时候除了基础的方式之外的一些扩展方式方法:web

一、Performing Raw SQL Queries (执行真正的SQL语句)sql

二、Performing no-tracking queries (执行无跟踪的SQL语句)数据库

三、Examining SQL sent to the database (检查发到数据库的SQL语句)api

 

Performing Raw SQL Queries缓存

EF Code First 有API 可让用户直接把SQL语句发给数据库去执行,有如下几种选择:并发

一、DbSet.SqlQuery  执行查询语句后返回实体类型(即DbSet对象所预期的);同时被数据库上下文自动跟踪(除非手动关闭,可参看AsNoTracking 方法)mvc

二、Database.SqlQuery 能够用来执行查询语句后返回不是实体类型的,同时也不会被数据库上下文跟踪,即使是返回的是实体类型;app

三、Database.ExecuteSqlCommand 执行非查询的SQL语句async

用EF的一个好处就是一些重复的语句能够不用本身再写,它能够为你生成一些SQL语句,能够解放你不用本身再写;

可是也有一些场景,须要你本身运行本身手动建立的SQL或者方法来处理一些特殊异常状况。

在WEB网页应用的时候,必须预防SQL注入攻击。最好的办法就是用参数化查询语句,而不是拼接字符串。

Calling a Query that Returns Entities

DbSet<TEntity>类提供了一个方法能够用来执行一个SQL语句,并返回一个实体类型。

简单的例子:

复制代码
        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            //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);
        }
复制代码

Calling a Query that Returns Other Types of Objects

在前面一个章节设计了Home/About 里面的代码:

复制代码
        public ActionResult About()
        {
            IQueryable<EnrollmentDateGroup> data = from student in db.Students
                                                   group student by student.EnrollmentDate into dateGroup
                                                   select new EnrollmentDateGroup()
                                                   {
                                                       EnrollmentDate = dateGroup.Key,
                                                       StudentCount = dateGroup.Count()
                                                   };
            return View(data.ToList());
        }
复制代码

能够替换为:

复制代码
        public ActionResult About()
        {
            //IQueryable<EnrollmentDateGroup> data = from student in db.Students
            //                                       group student by student.EnrollmentDate into dateGroup
            //                                       select new EnrollmentDateGroup()
            //                                       {
            //                                           EnrollmentDate = dateGroup.Key,
            //                                           StudentCount = dateGroup.Count()
            //                                       };

            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());
        }
复制代码

运行起来,效果是同样的。

Calling an Update Query

假设须要一个页面用来批量调整 Course的 Credits;

在Course控制器增长两个Action:

复制代码
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();
}
复制代码

当Get UpdateCourseCredits请求的时候,就给用户一个编辑框和一个提交按钮;
当用户输入后,点击提交后,返回带有更新了多少条记录的显示信息的页面;

为UpdateCourseCredits建一个空的视图,而后用下面代码代替:

复制代码
@model EFTest.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>
复制代码

把应用执行起来后,进入Course页面,而后在后面加UpdateCourseCredits

访问如 http://XXXXXXX/Course/UpdateCourseCredits 这个URL:

而后输入2后,点Update:

会显示一共多少记录被更新;而后返回Course主页面看到全部的都乘以2了:

更多关于执行原生SQL语句的内容见:  Raw SQL Queries

No-Tracking Queries

 当一个数据库上下文检索数据表行并创建一个实体对象用来表示它的时候,默认是跟踪在内存里的实体和在数据库里的保持同步。

在内存里的数据就像缓存用来更新实体数据。而缓存在WEB应用中不是必须的,由于上下文实例是典型的短生命周期的,每次请求会新建一个,用完就disposed,在实体数据被再使用的时候,上一个上下文实例已经被disposed了。

因此能够用AsNoTracking方法来不跟踪内存里的实体对象。

典型的场景包括:

一、一次查询很大量的数据,若是要跟踪,就是极大的下降性能;

二、当准备附加一个实体计划更新时,若是由于其余缘由,已经检索同一个实体,而由于该实体被数据库上下文所跟踪,因此你就不能够附加这个实体。惟一的办法就是在以前的检索中采用AsNoTracking方式。

这里原文说到之前的教程中作了这个测试,并提供了连接;

我在这里按照之前的教程作这个测试:

先为Department 控制器加一个私有方法来检查数据库,是否是这个Administrator已经对应了一个Department,若是已经对应,则直接提示错误;

复制代码
private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
    if (department.PersonID != null)
    {
        var duplicateDepartment = db.Departments
            .Include("Administrator")
            .Where(d => d.PersonID == department.PersonID)
            .FirstOrDefault();
        if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
        {
            var 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);
        }
    }
}
复制代码

把Edit HttpPost Action 改为如下简单的方式,原先检查并发的先注释掉:

复制代码
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")] 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;
            }
            ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID);
            return View(department);
        }
复制代码

下面进行测试:先编辑一个Department,而后把Administrator选为和另外一个Department同样,这个时候就不会继续执行SaveChange的操做了,而是直接报错:

若是返回到部门主页,再点击编辑,此次随便编辑其余值,好比Budget :

就会出现错误:

就是由于附件这个实体的时候,前面检查Administrator的时候已经去数据库查询全部Administrator 为 Kapoor, Candace 的Dempartment,致使Economics部门已经被读取,并已经跟踪;

当Edit Action尝试为 MVC自动绑定的Department 实体 更改标识的时候,会致使这个错误,由于这个Department 已经被读取,并被跟踪。

要解决这个问题,也很简单,在ValidateOneAdministratorAssignmentPerInstructor 方法中,加入AsNoTracking(),以下:

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

这样改好就没有问题了。

 Examining SQL sent to the database

有时候须要可以看到实际发到数据库的SQL语句,对于调试程序有很大帮助,前面的教程讲了采用拦截的方式,把一些信息拦截到Output去查看;

下面准备说一个简单的方案来查看实际发到数据库的SQL语句:

把Course的Index Action变为如下简单的方式:

复制代码
        public ActionResult Index()
        {

            var courses = db.Courses;
            var sql = courses.ToString();
            return View(courses.ToList());

            //var courses = db.Courses.Include(c => c.Department);
            //return View(courses.ToList());
        }
复制代码

而后在 return语句加上断点: 选中行点F9便可:

而后按F5执行应用,点击进入Course\Index页面,会停留在这一行:

而后光标停留在上一行sql上面,就能够看到sql 的值:

点那个放大镜图标,能够看的详细点:

 下面为Course Index页面加一个Department 下拉式的过滤;

修改Course Index Action 为:

复制代码
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());
}
复制代码

再在Index视图 Table元素前面加上:

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

效果如图:

能够在return语句加上断点:

这个时候运行到这里的时候,就能够看sql的值:(加入了Department的查询语句)

Repository and unit of work patterns

仓库和单元工做模式是不少开发者会去写代码实现的,在数据访问层和业务逻辑层之间加入一个虚拟层;

这种模式能够帮助应用与数据存储变化隔离开,可促进实现单元测试或者TDD test-driven development;

然而,在使用EF的状况下,再额外写代码实现这个模式已经不是最好的方式了,主要是如下缘由:

一、EF 的上下文类已经实现应用代码和数据交互部分的隔离;

二、EF的上下文类能够做为单元工做类来进行数据库更新;

三、EF6 可让这种模式实现起来更简单,而不用再本身写仓库代码;

更多学习仓库和单元工做模式,能够参考:the Entity Framework 5 version of this tutorial series.

EF6如何实现TDD,能够参考:

How EF6 Enables Mocking DbSets more easily

Testing with a mocking framework

Testing with your own test doubles

Proxy classes 代理类

 当EF建立一个实体实例的时候,它通常会为这个实体动态生成一个衍生类型做为代理,而后再建立这个代理的实例;

能够看原文中的两个图,第1个图,能够看到Student申明的时候是Student 类型,但到第2步,从数据库读取数据后,就能够看到代理类:

代理类重载了一些虚拟导航属性,用一些执行动做的钩子来填充,当这些虚拟属性被访问的时候,能够自动执行;这种机制主要是为了 延时加载;

大部分状况下,不须要关注代理类的工做,可是如下状况是特例:

一、在一些场景下,须要阻止EF产生代理类,好比你准备序列化实体的时候,确定是但愿是POCO类,而不是代理类;有一种办法来解决这个序列化问题,就是用序列化DTO而不是序列化实体对象;如Using Web API with Entity Framework;(通常在WEB API里序列化实体要求比较多); 另外也能够直接关闭代理类:disable proxy creation.

二、当直接用new来实例化一个实体的时候,你获得的不是代理类,这觉得着你得不到延时加载、数据变化跟踪这些功能;这个应该也不是大问题,毕竟这个不是从数据库里取的数据,一般不须要延时加载,若是你明肯定义它为Added ,你也不须要数据跟踪。 然而,你若是须要延时加载、数据变化跟踪,能够用DbSet的Create方法来新建代理类;

三、若是你须要从一个代理类实例获取真正的实体类型,可使用ObjectContextGetObjectType方法从一个代理类实例获取实体类型;

更多代理类信息参考:Working with Proxies

Automatic change detection 自动变化侦测

EF经过比较当前值和原始值来肯定哪些实体变化了(于是要更新到数据库里),原始值是在被检索或者附加的时候存储,一些方法引起自动变化侦测: 

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

若是在跟踪不少实体,而且在一个循环操做中,要Call上面这些方法不少次的话,那么临时性关闭数据变化侦测(经过使用AutoDetectChangesEnabled这个属性值)是能够带来很大的性能提高的;

更多信息见:Automatically Detecting Changes

 Automatic validation 自动数据校验

 EF默认会在SaveChange的时候校验数据,而若是是很大量的数据,并且已经被校验过了,那么能够经过临时性关闭自动校验(经过使用ValidateOnSaveEnabled这个属性值)来提升性能;

更多信息见: Validation

相关文章
相关标签/搜索