zoukankan      html  css  js  c++  java
  • MVC5与EF6结合教程(12):高级 Entity Framework 方案

    原文:https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/advanced-entity-framework-scenarios-for-an-mvc-web-application

    在上一教程中,您实现了每个层次结构一个表继承。 本教程介绍了几个主题,这些主题可帮助你了解开发使用实体框架 Code First 的 ASP.NET web 应用程序的基础知识。 前几部分都提供了分步说明,指导你完成代码并使用 Visual Studio 完成任务。下面的部分将介绍几个主题,其中包含简短的简介,并提供有关详细信息的资源链接。

    对于大多数这些主题,你将使用已创建的页面。 若要使用原始 SQL 执行批量更新,您将创建一个新页,用于更新数据库中所有课程的信用额度:

    Update_Course_Credits_initial_page

    在本教程中,你将了解:

    • 执行原始 SQL 查询
    • 执行无跟踪查询
    • 检查发送到数据库的 SQL 查询

    你还将了解:

    • 创建抽象层
    • 代理类
    • 自动脏值检测
    • 自动验证
    • 实体框架 Power Tools
    • 实体框架源代码

    一、必备组件

    二、执行原始 SQL 查询

    实体框架 Code First API 包括使你能够将 SQL 命令直接传递到数据库的方法。 有下列选项:

    • 对于返回实体类型的查询,请使用DbSet. SqlQuery方法。 返回的对象必须是DbSet对象所需的类型,并且数据库上下文会自动跟踪这些对象,除非关闭跟踪。 (有关AsNoTracking方法,请参阅以下部分。)
    • 对于返回非实体类型的查询,请使用SqlQuery方法。 数据库上下文不会跟踪返回的数据,即使你使用该方法来检索实体类型也是如此。
    • 对于非查询命令,请使用ExecuteSqlCommand

    使用 Entity Framework 的优点之一是它可避免你编写跟数据库过于耦合的代码 它会自动生成 SQL 查询和命令,使得你无需自行编写。 但在某些情况下,你需要运行已手动创建的特定 SQL 查询,并且这些方法使你能够处理此类异常。

    在 Web 应用程序中执行 SQL 命令时,请务必采取预防措施来保护站点免受 SQL 注入攻击。 一种方法是使用参数化查询,确保不会将网页提交的字符串视为 SQL 命令。 在本教程中,将用户输入集成到查询中时会使用参数化查询。

    1、调用返回实体的查询

    TEntity DbSet<TEntity> 类提供一个方法,该方法可用于执行返回类型为的实体的查询。 若要查看其工作原理,请更改Details Department控制器的方法中的代码。

    DepartmentController.cs中,在Details db.Departments.FindAsync方法中,将方法调用db.Departments.SqlQuery替换为方法调用,如以下突出显示的代码所示:

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

    为了验证新代码是否正常工作,请选择“院系”选项卡,然后选择其中某一院系的“详细信息”。 确保所有数据都按预期方式显示。

    2、调用返回其他类型对象的查询

    之前你在“关于”页面创建了一个学生统计信息网格,显示每个注册日期的学生数量。 HomeController.cs中执行此操作的代码使用 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。 为此,您需要运行一个返回除 entity 对象之外的内容的查询,这意味着您需要使用SqlQuery方法。

    HomeController.cs中,将About方法中的 LINQ 语句替换为 SQL 语句,如以下突出显示的代码所示:

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

    运行 "关于" 页。 验证它是否显示与以前相同的数据。

    3、调用更新查询

    假设 Contoso 大学管理员希望能够在数据库中执行大容量更改,如更改每个课程的信用额度。 如果该大学提供了大量课程,那么将所有课程作为实体来检索并单独更改就非常低效。 在本节中,您将实现一个网页,该网页使用户能够指定更改所有课程的信用额度的因素,并通过执行 SQL UPDATE语句进行更改。

    CourseController.cs中, UpdateCourseCreditsHttpGetHttpPost添加方法:

    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变量中将不会返回任何内容,并且该视图将显示一个空的文本框和一个提交按钮。

    multiplier 单击HttpPost "更新" 按钮时,将调用方法,并在文本框中输入值。 然后,该代码执行更新课程的 SQL,并将受影响的行数返回到ViewBag.RowsAffected变量中的视图。 当视图获取该变量中的值时,它将显示更新的行数,而不是文本框和提交按钮。

    CourseController.cs中,右键单击其中一个方法UpdateCourseCredits ,然后单击 "添加视图"。 此时将显示 "添加视图" 对话框。 保留默认值,然后选择 "添加"。

    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方法,然后在浏览器地址栏中 URL 的末尾添加"/ UpdateCourseCredits"到 (例如: http://localhost:50205/Course/UpdateCourseCredits)。 在文本框中输入数字:

    Update_Course_Credits_initial_page_with_2_entered

    单击Update 你会看到受影响的行数。

    单击“返回列表”可以查看课程列表,其中学分已替换为修改后的数字。

    有关原始 SQL 查询的详细信息,请参阅 MSDN 上的原始 Sql 查询

    三、非跟踪查询

    当数据库上下文检索表行并创建表示它们的实体对象时,默认情况下,它会跟踪内存中的实体是否与数据库中的内容同步。 更新实体时,内存中的数据充当缓存并使用该数据。 在 Web 应用程序中,此缓存通常是不必要的,因为上下文实例通常生存期较短(创建新的实例并用于处理每个请求),并且通常在再次使用该实体之前处理读取实体的上下文。

    您可以使用AsNoTracking方法禁用内存中实体对象的跟踪。 可能想要执行的典型方案包括以下操作:

    • 查询检索到关闭跟踪可能会明显提高性能的大量数据。
    • 您希望附加实体以便对其进行更新,但您之前检索到不同用途的同一实体。 由于数据库上下文已跟踪了该实体,因此无法附加要更改的实体。 解决这种情况的一种方法是将AsNoTracking选项与前面的查询一起使用。

    有关演示如何使用AsNoTracking方法的示例,请参阅本教程的早期版本 此版本的教程不会在编辑方法中对已创建模型联编程序的实体设置修改标志,因此不需要AsNoTracking

    四、检查发送到数据库的 SQL

    有时能够以查看发送到数据库的实际 SQL 查询对于开发者来说是很有用的。 在前面的教程中,你已了解如何在侦听器代码中执行此操作;现在,你将看到一些无需编写侦听器代码即可执行此操作的方法。 若要尝试此操作,你将看到一个简单的查询,然后在你添加预先加载、筛选和排序选项时,查看该查询所发生的情况。

    在 controller /CourseController中,将Index方法替换为以下代码,以便暂时停止预先加载:

    public ActionResult Index()
    {
        var courses = db.Courses;
        var sql = courses.ToString();
        return View(courses.ToList());
    }
    

    现在,在return语句上设置一个断点(将光标置于该行上)。 F5在调试模式下运行项目,然后选择 "课程索引" 页。 当代码到达断点时,检查sql变量。 你会看到发送到 SQL Server 的查询。 这是一个简单Select的语句。

    {SELECT 
    [Extent1].[CourseID] AS [CourseID], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Credits] AS [Credits], 
    [Extent1].[DepartmentID] AS [DepartmentID]
    FROM [Course] AS [Extent1]}
    

    单击放大镜可查看文本可视化工具中的查询。

    现在,你将向 "课程索引" 页添加一个下拉列表,以便用户可以筛选特定部门。 您将按标题对课程进行排序,并指定预先加载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。

    将包含所有部门的集合传递给下拉列表的视图。SelectList 传递给SelectList构造函数的参数指定值字段名称、文本字段名称和选定项。

    对于Course存储Get库的方法,代码指定筛选器表达式、排序顺序Department以及预先加载导航属性。 如果未在下拉列表true中选择任何内容(即为 null), SelectedDepartment则筛选表达式始终返回。

    ViewsCourseIndex.cshtml中,紧跟在开始table标记之前,添加以下代码以创建下拉列表和 "提交" 按钮:

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

    仍设置断点后,运行 "课程索引" 页。 在代码第一次命中断点时继续,以便在浏览器中显示该页。 从下拉列表中选择一个部门,然后单击 "筛选器"。

    这次,第一个断点将用于下拉列表的部门查询。 跳过该操作, query并在下一次代码到达断点时查看变量,以便查看查询Course现在的外观。 你将看到如下所示的内容:

    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数据一起加载,并且它包含WHERE一个子句。

    var sql = courses.ToString()删除行。

    五、创建抽象层

    许多开发人员编写代码实现存储库和工作模式单元以作为使用 Entity Framework 代码的包装器。 这些模式用于在应用程序的数据访问层和业务逻辑层之间创建抽象层。 实现这些模式可让你的应用程序对数据存储介质的更改不敏感,而且很容易进行自动化单元测试和进行测试驱动开发 (TDD)。 不过,出于以下几个原因,编写附加代码来实现这些模式并非总是最佳选择:

    • EF 上下文类可以为使用 EF 的数据库更新充当工作单位类。
    • 对于使用 EF 进行的数据库更新,EF 上下文类可充当工作单元类。
    • 实体框架6中引入的功能使得无需编写存储库代码即可更轻松地实现 TDD。

    有关如何实现存储库和工作单元模式的详细信息,请参阅本系列教程的实体框架5版本 有关在实体框架6中实现 TDD 的方式的信息,请参阅以下资源:

    六、代理类

    当实体框架创建实体实例时(例如,在执行查询时),它通常会将其创建为作为实体代理的动态生成的派生类型的实例。 例如,请参阅以下两个调试器映像。 在第一个图像中,你会看到student在实例化实体Student后,变量是预期类型。 在第二个图中,使用 EF 从数据库中读取 student 实体后,会看到代理类。

    代理类之前

    代理类之后

    此代理类将重写实体的某些虚拟属性,以插入挂钩,以便在访问属性时自动执行操作。 此机制用于的一个函数是延迟加载。

    大多数情况下,无需注意代理的这种使用,但有一些例外:

    • 在某些情况下,你可能想要阻止实体框架创建代理实例。 例如,在序列化实体时,通常需要 POCO 类,而不是代理类。 避免序列化问题的一种方法是序列化数据传输对象(Dto)而不是实体对象,如使用带有实体框架的 WEB API教程中所示。 另一种方法是禁用代理创建
    • 如果使用new运算符实例化实体类,则不会获得代理实例。 这意味着不会获得延迟加载和自动更改跟踪等功能。 这通常是正常的;通常不需要延迟加载,因为你要创建的是不在数据库中的新实体,并且如果你将实体显式标记为Added,则通常不需要更改跟踪。 但是,如果确实需要延迟加载并且需要更改跟踪,则可以使用DbSet类的create方法创建新的实体实例,其中包含代理。
    • 你可能需要从代理类型获取实际的实体类型。 您可以使用ObjectContext类的GetObjectType方法来获取代理类型实例的实际实体类型。

    有关详细信息,请参阅 MSDN 上的使用代理

    七、自动脏值检测

    Entity Framework 通过比较的实体的当前值与原始值来判断更改实体的方式 (因此需要发送更新到数据库)。 查询或附加该实体时会存储的原始值。 如下方法会导致自动脏值:

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

    如果正在跟踪大量实体,并且在循环中多次调用这些方法之一,则使用AutoDetectChangesEnabled属性暂时关闭自动更改检测可能会显著提高性能。 有关详细信息,请参阅 MSDN 上的自动检测更改

    八、自动验证

    在调用SaveChanges方法时,默认情况下,实体框架在更新数据库之前验证所有已更改实体的所有属性中的数据。 如果已更新大量实体,并且已验证数据,则不需要执行此操作,并且可以通过暂时关闭验证来使保存更改的过程更少。 可以使用ValidateOnSaveEnabled属性执行此操作。 有关详细信息,请参阅 MSDN 上的验证

    九、实体框架 Power Tools

    实体框架 Power Tools是一个 Visual Studio 外接程序,用于创建这些教程中所示的数据模型图。 这些工具还可以执行其他功能,例如基于现有数据库中的表生成实体类,以便可以将数据库与 Code First 一起使用。 安装这些工具后,上下文菜单中将显示一些附加选项。 例如,在解决方案资源管理器中右键单击上下文类时,将看到和实体框架选项。 这使您能够生成关系图。 使用时 Code First 无法更改关系图中的数据模型,但可以移动它们以使其更易于理解。

    EF 关系图

    十、实体框架源代码

    GitHub上提供了实体框架6的源代码。 您可以为 bug 提供文件,并且您可以向 EF 源代码提供您自己的增强功能。

    尽管源代码已打开,但实体框架完全支持作为 Microsoft 产品。 Microsoft Entity Framework 团队将控制接受哪些贡献和测试所有的代码更改,以确保每个版本的质量。

    十一、鸣谢

    • Tom Dykstra 编写了本教程的原始版本,共同创作了 EF 5 更新,并编写了 EF 6 更新。 Tom 是 Microsoft Web 平台和工具内容团队的高级编程编写器。
    • Rick Anderson(twitter @RickAndMSFT)大部分工作正在更新 ef 5 和 MVC 4 的教程,并共同创作了 ef 6 更新。 Rick 是 Microsoft 的高级编程编写人员,侧重于 Azure 和 MVC。
    • Rowan 莎莎和实体框架团队的其他成员协助进行代码评审,并帮助调试在我们更新 EF 5 和 ef 6 教程时出现的许多迁移问题。

    十二、常见错误疑难解答

    1、无法创建/隐藏副本

    错误消息:

    如果文件已存在,则<无法>创建/卷影复制 "filename"。

    解决方案

    请等待几秒钟,然后刷新页面。

    2、更新-无法识别数据库

    错误消息(来自 PMC Update-Database中的命令):

    术语 "更新-数据库" 未被识别为 cmdlet、函数、脚本文件或可运行程序的名称。 检查名称的拼写,如果包含路径,请验证路径是否正确,然后重试。

    解决方案

    退出 Visual Studio。 重新打开项目,然后重试。

    3、验证失败

    错误消息(来自 PMC Update-Database中的命令):

    对一个或多个实体的验证失败。 有关更多详细信息,请参阅 "EntityValidationErrors" 属性。

    解决方案

    此问题的一个原因是在Seed方法运行时出现验证错误。 有关调试Seed方法的提示,请参阅播种和调试实体框架(EF)数据库。

    4、HTTP 500.19 错误

    错误消息:

    HTTP 错误 500.19-内部服务器错误无法访问请求的页面,因为该页的相关配置数据无效。

    解决方案

    获取此错误的一种方法是使用解决方案的多个副本,每个副本使用相同的端口号。 通常可以通过退出 Visual Studio 的所有实例,然后重新启动正在处理的项目,来解决此问题。 如果这不起作用,请尝试更改端口号。 右键单击项目文件,然后单击 "属性"。 选择 " Web " 选项卡,然后在 "项目 Url " 文本框中更改端口号。

    5、定位 SQL Server 实例出错

    错误消息:

    建立到 SQL Server 的连接时出现与网络相关或特定于实例的错误。 未找到或无法访问服务器。 请验证实例名称是否正确,SQL Server 是否已配置为允许远程连接。 (提供程序:SQL 网络接口,错误:26 - 定位指定服务器/实例出错)

    解决方案

    请检查连接字符串。 如果手动删除了数据库,请在构造字符串中更改数据库的名称。

    十三、获取代码

    下载完成的项目

    十四、其他资源

    有关如何使用实体框架处理数据的详细信息,请参阅MSDN 上的 EF 文档页ASP.NET 数据访问-建议的资源

    有关如何在生成 web 应用程序后对其进行部署的详细信息,请参阅 MSDN Library 中的ASP.NET Web 部署-推荐资源

    有关与 MVC 相关的其他主题(例如身份验证和授权)的信息,请参阅ASP.NET MVC-推荐的资源

    十五、后续步骤

    在本教程中,你将了解:

    • 已执行原始 SQL 查询
    • 执行无跟踪查询
    • 已检查发送到数据库的 SQL 查询

    还了解了:

    • 创建抽象层
    • 代理类
    • 自动脏值检测
    • 自动验证
    • 实体框架 Power Tools
    • 实体框架源代码

    这将完成本系列教程,介绍如何在 ASP.NET MVC 应用程序中使用实体框架。 如果要了解有关 EF Database First 的信息,请参阅 DB 第一系列教程。

  • 相关阅读:
    【转并修改】VS2013 MVC Web项目使用内置的IISExpress支持局域网内部机器(手机、PC)访问、调试
    ASP.NET MVC4 UEditor 的上传图片配置路径
    转:Java图形化界面设计——布局管理器之FlowLayout(流式布局)其他请参考转载出处网址
    转载:win7JDK环境配置
    转载:java保留2位小数
    转载:遍历Map的四种方法
    架构模式: 远程过程调用
    架构模式: 外部配置化
    架构模式: 微服务的基底
    架构模式: 服务部署平台
  • 原文地址:https://www.cnblogs.com/springsnow/p/13264895.html
Copyright © 2011-2022 走看看