MVC5 Entity Framework学习之读取相关数据

前一篇文章中完成了School 数据模型,接下来你将学习如何读取和显示相关的数据——这里指Entity Framework加载至导航属性中的数据。

下图是完成后的效果图




延迟、预先和显示加载相关数据

Entity Framework可以通过多种方法向实体的导航属性中加载数据

  • 延迟加载(Lazy loading) 当实体第一次被读取时,相关数据并不会被检索。但是,当你第一次访问导航属性时,该导航属性所需的数据会自动加载。这是向数据库发送多个查询语句的结果——一次是读取实体本身,接着是每次与被检索的实体相关的数据。DbContext类默认是启动延迟加载的。


  • 预先加载(Eager Loading) 当实体被读取的同时加载与该实体相关的数据。这通常是在单个连接查询中检索出所有所需要的数据,你可以使用Include方法来指定是否使用预先加载。


  • 显式加载(Explicit Loading) 与延迟加载类似,但需要在代码中显示的指明要检索的数据。当你访问一个导航属性时,它并不会自动加载,你需要获得实体的对象状态管理器条目并调用集合的Collection.Load方法或含有单个属性的Reference.Load方法来手动加载相关数据。(在下面的例子中,如果你希望加载Administrator导航属性,你需要将 Collection(x => x.Courses)替换为Reference(x => x.Administrator))通常你应该在禁用延迟加载的情况下使用显示加载。


因为延迟加载和显式加载都不立即检索属性的值,所以它们也被称为延时加载(deferred loading.)。

性能考量

如果你知道你需要为每一个被检索的实体加载相关数据,预先加载通常具有最佳性能,因为单个查询通常比为每一个实体分别进行查询更有效率。例如在上面的例子中,假设每个department 有十个相关的course,预先加载只需要单个连接查询一次往返数据库就可以检索出所有数据,而延迟加载和显式加载都需要11次查询11次往返数据库才能得到同样的结果。在高延迟的情况下,额外的往返对性能是十分不利的。

另一方面,在某些情况下延迟加载具有更高的效率。预先加载可能会生成SQL Server不能有效处理的复杂的连接查询。或者如果你访问的是你正在处理的实体的集合或子集的导航属性,延迟加载会更有效,因为预先加载会检索那些你并不需要的数据。如果性能是至关重要的,那么你最好测试这两种方法以便选择执行效率更好的那一种。

延迟加载会屏蔽那些导致性能问题的代码。例如,那些没有指定预先或显式加载但是处理实体高并发时在每次迭代中都使用了多个导航属性的代码,其执行效率可能会很低(因为会有大量数据库往返)。一个在开发环境下使用On-Premise SQL server表现良好的应用程序可能会在部署到Windows Azure SQL数据库时由于增加了延迟并使用延迟加载而可能导致性能问题。你应该使用真实的测试负载来分析数据库查询以便决定是否使用延迟加载。

在序列化之前禁用延迟加载

如果你在序列化期间启用了延迟加载,那么你将查询到预期多得多的数据。序列化通常会访问实例的每个属性,而属性访问触发延迟加载,并且这些延迟加载的实体会被序列化,然后序列化过程会访问延迟加载的实体的每一个属性,这可能会导致更多的延迟加载和序列化。为了防止这种失控的连锁反应,你需要在序列化实体之前禁用延迟加载。

通过使用Entity Framework的代理类,序列化同样也是横复杂的。

避免序列化问题的一种方法是序列化数据传输对象(DTO)而不是实体对象。

如果你没有使用DTOs,你可以禁用延迟加载并通过使用禁用代理创建来避免代理问题。

下面是一些别的禁用延迟加载的方法:

  • 对于特定的导航属性,在声明时省略virtual关键字
  • 对于所有的导航属性,将LazyLoadingEnabled设置为false,将下面的代码放在上下文类的构造函数中

    this.Configuration.LazyLoadingEnabled = false;

创建Courses页面并显示Department 名称

Course实体包含了一个导航属性,该导航属性包含有Department实体,如果要在course列表中显示已分配的department名称,你需要获得Course.Department导航属性中的Department实体中的Name属性。

使用"MVC 5 Controller with views, using Entity Framework"框架为Course实体类型新建一个名为CourseController的控制器,就像之前为Student创建控制器那样


打开Controllers\CourseController.cs,查看Index方法

        public ActionResult Index()
        {
            var courses = db.Courses.Include(c => c.Department);
            return View(courses.ToList());
        }
可以看到框架使用Include方法将Department导航属性指定为预先加载。

打开Views\Course\Index.cshtml,使用下面的代码替换

@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewBag.Title = "Courses";
}

<h2>Courses</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.CourseID)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Credits)
        </th>
        <th>
            Department
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.CourseID)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Credits)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Department.Name)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
            @Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
        </td>
    </tr>
}

</table>
这里对框架代码进行了如下更改:

  • 将标题从Index改为Course
  • 添加了用来显示CourseID属性值的Number列,默认情况下,框架不会生成显示主键的代码,因为通常它们对最终用户是没有意义的。但是在本例中,你希望将其显示出来
  • 将Department 列移动到右侧显示,并修改其标题。框架会从Department实体中正确的选择其Name属性并显示,但是在Course 页面中,列标题应该显示为Department而不是Name

注意对于Department列,框架代码显示了加载至Department 导航属性中的Department 实体的Name属性。

<td>
    @Html.DisplayFor(modelItem => item.Department.Name)
</td>
运行项目,选择Courses选项卡,查看数据


创建Instructors页面来显示 Courses 和Enrollments

本节中你将会为Instructor实体创建控制器和试图以便显示Instructors 


此页面通过以下方式来读取和显示相关数据:

  • Instructor列表显示了OfficeAssignment实体中的相关数据,Instructor和OfficeAssignment实体之间是一对一或零的关系,你可以对OfficeAssignment实体使用预先加载。如前所述,当你需要检索主表中所有行的相关数据时,预先加载更有效率。在本例中,你希望显示所有Instructor的office分配情况。
  • 当用户选择一名Instructor时,相关的Course实体也会被显示出来,Instructor和Course实体之间是多对多的关系。你可以对Course实体和相关的Department实体使用预先加载。在本例中,延迟加载可能更有效率,因为你只需要那些已选择的Instructor的Course信息。但是在本例中,仅演示如何对导航属性实体的导航属性使用预先加载。
  • 当用户选择一门Course时,相关的Enrollments 实体中的数据被显示,Course和Enrollment实体是一对多的关系。你会对Enrollment实体和相关的Student实体使用显式加载(显示加载不是必须的,因为已经启用了延迟加载,这里仅作演示)。

为Instructor Index视图创建视图模型

Instructors 页面显示了三个不同的表格,你会创建一个包含三个属性的视图模型,每个属性含有一个表格所需的数据。

打开ViewModels文件夹,创建InstructorIndexData.cs类,使用下面的代码替换

using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.ViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

创建Instructor 控制器和视图

使用 EF read/write actions框架创建InstructorController 控制器


打开Controllers\InstructorController.cs,添加ViewModels命名空间

using ContosoUniversity.ViewModels;
Index 方法中的框架代码指定仅对OfficeAssignment 导航属性使用预先加载

public ActionResult Index()
{
    var instructors = db.Instructors.Include(i => i.OfficeAssignment);
    return View(instructors.ToList());
}
使用下面的代码替换Index方法以便加载其他的相关数据并传递给视图模型

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }

    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

    return View(viewModel);
}

该方法接收一个可选的路由参数(id)和一个查询字符串参数(courseID)用来提供所选instructor 和course的ID值,并传递给视图所需要的数据。参数是由页面上的Select 链接提供的。

上面的代码首先创建了一个视图模型的实例并将instructors列表放入其中,该代码指定对于Instructor.OfficeAssignment和Instructor.Courses导航属性使用预先加载。

var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
    .Include(i => i.OfficeAssignment)
    .Include(i => i.Courses.Select(c => c.Department))
     .OrderBy(i => i.LastName);
第二个Include方法加载了Course实体,并为每个被加载的Course实体预先加载了Course.Department导航属性。

.Include(i => i.Courses.Select(c => c.Department))
如前所述,预先加载不是必须的,但是在这里使用是为了提高程序性能。由于视图总是需要OfficeAssgnment实体,因此在同一个查询中检索它们是更有效率的。当一个instructor 在页面中被选中时,Course实体是必需的,所以仅在页面中经常显示被选择的course 时,预先加载比延迟加载更有效率。

如果一个instructor  ID被选中,被选中的instructor  会从视图模型的instructor  列表中来检索,然后视图模型的Courses实体会通过instructor 的Course导航属性的Courses属性来加载。

if (id != null)
{
    ViewBag.InstructorID = id.Value;
    viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}
Where方法会返回一个集合,但是在本例中,该方法通过传递的参数仅返回了一个Instructor 实体。Single方法可以将集合转换为一个Instructor 实体,使你能够访问该实体的Courses属性。

当你知道集合只含有一个元素时,你可以使用集合的Single方法。当集合为空或者含有多个元素时,Single方法会抛出一个异常。另一中选择是使用SingleOrDefault,如果集合为空,该方法会会返回一个默认值。但在本例中使用SingleOrDefault仍会引发异常(在null引用中查询Courses属性),但异常信息并没有明确指出引起问题的原因。当调用Single方法时,你也可以直接将其当做Where条件而不是分别调用Where及Single方法:

.Single(i => i.ID == id.Value)
而不是:

.Where(I => i.ID == id.Value).Single()

接下来,如果选择了一门course,该course会从视图模型中的course列表中检索。然后视图模型的Enrollments 实体会通过Course的Enrollments 导航属性的Enrollments 属性来加载。

    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

修改Instructor Index视图

打开Views\Instructor\Index.cshtml,使用下面的代码替换

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th></th>
    </tr>

    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.ID == ViewBag.InstructorID)
        {
            selectedRow = "success";
        }
        <tr class="@selectedRow">
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.HireDate)
            </td>
            <td>
                @if (item.OfficeAssignment != null)
                {
                    @item.OfficeAssignment.Location
                }
            </td>
            <td>
                @Html.ActionLink("Select", "Index", new { id = item.ID }) |
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }

    </table>
对代码所做的更改:

  • 更改视图模型类为InstructorIndexData
  • 更改标题为Instructors
  • 添加Office列,以便在item.OfficeAssignent不为空时显示item.OfficeAssignment.Location(因为它们是一对零或一的关系,可能不存在相关的OfficeAssignment实体)

    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td> 

  • 为被选中的instructor的tr元素动态添加class="success"样式,通过使用Bootstrap来为被选中的行设置背景颜色

    string selectedRow = ""; 
    if (item.InstructorID == ViewBag.InstructorID) 
    { 
        selectedRow = "success"; 
    } 
    <tr class="@selectedRow" valign="top"> 

  • 添加一个新的ActionLink,可以 向Index方法发送所选中的instructor ID

运行项目,选择Instructors选项卡,页面上显示了相关OfficeAssignment实体的Location属性值,如果OfficeAssignment实体为空,则什么也不显示。


打开 Views\Instructor\Index.cshtml,在table元素结束标记后面添加如下代码,用来显示被选中instructor的Course列表

@if (Model.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == ViewBag.CourseID)
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}
上面的代码通过读取视图模型中的Course属性来显示course列表,同时它还提供了一个Select链接用来被选中course的ID传递给Index方法。

运行项目,选择一个instructor,你可以看到页面中显示了分配给被选中instructor的course和course的Department


在你刚才添加代码的后面再次添加如下代码,用来显示那些选修被选中course的student列表

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}
上面的代码读取视图模型中的Enrollments属性来显示那些选修被选中course的student列表

运行项目,选择一个instructor,在选择一门course,可以看到页面中显示了选修该course的student列表


指定显示加载

打开nstructorController.cs,查看Index方法中是如何取得被选中course的Enrollment列表的

    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

当检索instructor列表时,你为Courses导航属性和每个Course的Department属性指定了预先加载,然后将Courses集合传递到视图模型中,接下来你就可以访问该集合中每个实体的Enrollments导航属性。由于你没有为Course.Enrollments导航属性指定预先加载,所以在页面中呈现该属性中的数据时使用的是延迟加载。

如果你禁用了延迟加载而没有修改任何代码,那么Enrollments 属性值将会是null而不管course实际含有多少enrollment。在这种情况下,要加载Enrollments属性,你必须要指定是预先加载或显式加载。你已经知道如何使用预先加载,为了演示显式加载,将Index方法使用下面的代码替换

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();

    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }
    
    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        // Lazy loading
        //viewModel.Enrollments = viewModel.Courses.Where(
        //    x => x.CourseID == courseID).Single().Enrollments;
        // Explicit loading
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            db.Entry(enrollment).Reference(x => x.Student).Load();
        }

        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}
在得到Course实体后显示加载course的Enrollments导航属性

db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
然后显示加载与每个Enrollment实体相关的Student实体

db.Entry(enrollment).Reference(x => x.Student).Load();
注意你使用了Collection 属性来加载集合属性,但是对于仅含有一个实体的属性,你应该使用Reference 属性。

运行项目,你会发现页面呈现数据并没有什么不一样的地方,但是其实我们已经更改了数据的检索方式。

原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application

欢迎转载,请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/39136963

还大家一个健康的网络环境,从你我做起

THE END

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。