第五章 加载实体和导航属性之延缓加载关联实体和在别的LINQ查询操作中使用Include()方法

5-7 在别的LINQ查询操作中使用Include()方法

问题

你有一个LINQ查询,使用了类似这样的操作 group by,join,和where;你想使用Include()方法预先加载额外的实体。另外你想使用Code-First来管理数据访问。

解决方案

假设你有如图5-22所示的概念模型

图5-22 一个简单的包含Club和Event以及它们之间一对多关联的模型

在Visual Studio中添加一个名为Recipe7的控制台应用,并确保引用了实体框架6的库,NuGet可以很好的完成这个任务。在Reference目录上右键,并选择 Manage NeGet Packages(管理NeGet包),在Online页,定位并安装实体框架6的包。这样操作后,NeGet将下载,安装和配置实体框架6的库到你的项目中。

创建一个名为Club和Event的类,复制代码清单5-41中的属性到这个类中,创建实体Club和Event实体。

代码清单5-14. Club、Event 实体类

public class Club
{
    public Club()
    {
        Events = new HashSet<Event>();
    }

    public int ClubId { get; set; }
    public string Name { get; set; }
    public string City { get; set; }

    public virtual ICollection<Event> Events { get; set; }
}

public class Event
{
    public int EventId { get; set; }
    public string EventName { get; set; }
    public DateTime EventDate { get; set; }
    public int ClubId { get; set; }

    public virtual Club Club { get; set; }
}

接下来,创建一个名为Recipe7Context的类,并将代码清单5-51中的代码添加到其中,并确保其派生到DbContext类。

public class Recipe7Context : DbContext
{
    public Recipe7Context()
        : base("Recipe7ConnectionString")
    {
        // 禁用实体框架的模型兼容性
        Database.SetInitializer<Recipe7Context>(null);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Club>().ToTable("Chapter5.Club");
    }

    public DbSet<Club> Clubs { get; set; }
}

接下来添加App.Config文件到项目中,并使用代码清单5-6中的代码添加到文件的ConnectionStrings小节下。


<connectionStrings>
<add name="Recipe7ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>



为了与group by从句组合使用Include()方法,Include()方法必须放在针对父实体的过虑和分组操作之后。如代码清单5-17所示。

代码清单5-17 . 当父实体上应用过虑和分组时,Include()方法的正确位置。

using (var context = new Recipe7Context())
{
    var club = new Club {Name = "Star City Chess Club", City = "New York"};
    club.Events.Add(new Event
        {
            EventName = "Mid Cities Tournament",
            EventDate = DateTime.Parse("1/09/2010"),
            Club = club
        });
    club.Events.Add(new Event
        {
            EventName = "State Finals Tournament",
            EventDate = DateTime.Parse("2/12/2010"),
            Club = club
        });
    club.Events.Add(new Event
        {
            EventName = "Winter Classic",
            EventDate = DateTime.Parse("12/18/2009"),
            Club = club
        });

    context.Clubs.Add(club);

    context.SaveChanges();
}

using (var context = new Recipe7Context())
{
    var events = from ev in context.Events
                 where ev.Club.City == "New York"
                 group ev by ev.Club
                 into g
                 select g.FirstOrDefault(e1 => e1.EventDate == g.Min(evt => evt.EventDate));

    var eventWithClub = events.Include("Club").First();

    Console.WriteLine("The next New York club event is:");
    Console.WriteLine("\tEvent: {0}", eventWithClub.EventName);
    Console.WriteLine("\tDate: {0}", eventWithClub.EventDate.ToShortDateString());
    Console.WriteLine("\tClub: {0}", eventWithClub.Club.Name);
}

Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();

代码清单5-17的输出如下:

The next New York club event is:
    Event: Winter Classic
    Date: 12/18/2009
    Club: Star City Chess Club


原理

我们创建了一个俱乐部(Club)和三个Events(活动)实体对象。在查询中,我们获取New York 的俱乐部中的所有活动。按俱乐部分组,并查找日期中最早的活动。注意,LINQ扩展方法FirstOrDefault(),巧妙地嵌入到了Select投影操作中。然而变量events只是一个表达式,它还没有在数据库中执行任何操作。

接下来,我们凭借Include()方法预先加载关联实体Club对象的信息,我们将第一个LINQ查询变量,events,作为第二个LINQ查询的输入。这是LINQ组合查询的一个示例。将一个复杂的LINQ查询转换成一系列的短小查询。前面的查询变量是后面查询的数据源。

注意,我们使用First()方法只是为获取第一个Event实例,这样将返回一个Event类型,而不是Event对象的集合。实体框架6包含了一个新的名为 IQueryableExtensions的接口,它公布了Include()方法的原型,它能接受一个基于字符串或是强类型的查询路径作为参数。IQueryableExtensions替换了EF4和EF5中的DbExtension类。

很多开发人员觉得Include()方法很让人迷惑,在一些情况下,智能感知不能有效地提示(因为表达式类型)。在一些情况下,它会在运行时悄悄地被忽略掉。特别地,除非编译器无法确定其结果类型,否则不会给出警告或提示。很多问题都在运行时才会暴露出来,这会使问题更难解决。这里有一些使用Include()方法的准则:

1、Include()方法是一个在IQueryable<T>上的扩展方法;

2、Include()方法只能应用在最终的查询结果集上,当它被在subquery(子查询)、join(连接)或者嵌套从句中,当生成命令树时,它将被忽略掉。在幕后,实体框架会把你的LINQ to Entites查询转换成一棵命令树,然后数据库提供者(database provider)将其处理并构建成一个用于数据库的SQL查询(译注:这一点很重要,我在上面吃过亏,直到现在才弄明白)

3、Include()方法只能应用 在实体类型的结果集上,如果表达式将结果投影到一个非实体类型的类型上,Include()方法将被忽略。

4、在Include()方法和最外面的操作之间,不能改变结果集的类型。例如,一个group by从句,改变结果集的类型。

5、用于Include()方法的查询路径表达式必须从最外层操作返回的类型中的导航属性开始,查询路径不能从任意点开始。

让我们看看,代码清单5-17是如何运行这些规则的,这个查询,将活动(events)按赞助的俱乐部分组,group by将结果类型从Event改变成一个分组结果集。第四条规则告诉我们,需要在group by从句改变结果类型之后再调用Include()。我们在结尾处调用Include()方法。 如果我们过早应用Include()方法,像这样 from ev in context.Events.Include,那么,Include()方法将被悄悄地从命令树上移除并不在起用。

5-8 延缓加载(Deferred Loading)相关实体

问题

你有一个实体的实例,你想在一个单独的查询中延缓加载其中两个或多个关联实体。这里尤其重要的是,我们如何使用Load()方法来避免查询相同的对象两次。另外你想使用Code-First来管理数据访问。

解决方案

假设你有如图5-23所示的概念模型.

图5-23 一个包含职员(employee),他的部门(department)和部门所在的公司(company)的模型

在Visual Studio中添加一个名为Recipe8的控制台应用,并确保引用了实体框架6的库,NuGet可以很好的完成这个任务。在Reference目录上右键,并选择 Manage NeGet Packages(管理NeGet包),在Online页,定位并安装实体框架6的包。这样操作后,NeGet将下载,安装和配置实体框架6的库到你的项目中。

接下来我们创建三个实体对象:Company,Departmet和Employee,复制代码清单5-18中的属性到这三个类中。

代码清单5-18. 实体类

public class Company
{
    public Company()
    {
        Departments = new HashSet<Department>();
    }

    public int CompanyId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Department> Departments { get; set; }
}


public class Department
{
    public Department()
    {
        this.Employees = new HashSet<Employee>();
    }

    public int DepartmentId { get; set; }
    public string Name { get; set; }
    public int CompanyId { get; set; }

    public virtual Company Company { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }
}

public class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }

    public virtual Department Department { get; set; }
}

接下来,创建一个名为Recipe8Context的类,并将代码清单5-19中的代码添加到其中,并确保其派生到DbContext类。

public partial class Recipe8Context : DbContext
{
    public Recipe8Context()
        : base("Recipe8ConnectionString")
    {
        // 禁用实体框架的模型兼容性
        Database.SetInitializer<Recipe8Context>(null);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Company>().ToTable("Chapter5.Company");
        modelBuilder.Entity<Employee>().ToTable("Chapter5.Employee");
        modelBuilder.Entity<Department>().ToTable("Chapter5.Department");
    }

    public DbSet<Company> Companies { get; set; }
    public DbSet<Department> Departments { get; set; }
    public DbSet<Employee> Employees { get; set; }
}

接下来添加App.Config文件到项目中,并使用代码清单5-20中的代码添加到文件的ConnectionStrings小节下。


<connectionStrings>
<add name="Recipe8ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>



在图5-23所示的模型中,一个职员(Employee)被关联到一个确切的部门(Department)。每个部门被关联到一个确切公司(Company)。

给定一个Employee的实例,你想加载他的部门以及部门所在的公司。是什么让这个问题变得有点特别呢? 我们已经有了一个Employee的实例,我们想避免再一次到数据库中获取Emplyee对象的副本,这种情况,我们可以使用Include()方法来获取关联实体Company和Department。也许,在真实的情况中,Employee的获取和实例化需要非常高的代价。

我们可以使用Load()方法,两次去加载关联实体,一次加载Department实例,一次去加载Company实例。然而,这会产生两次数据库交互。为了在一个查询中加载关联实体的实例,我们可以使用Include()方法和包含Department,Company查询路径,重新在Emlpoyee实体集上查询。或者组合使用Reference()和Query()方法。代码清单5-21演示了这些方法。

代码清单5-21.将数据插入到模型并使用两种稍有不同的方法加载关联实体

using (var context = new Recipe8Context())
{
    var company = new Company {Name = "Acme Products"};
    var acc = new Department {Name = "Accounting", Company = company};
    var ship = new Department {Name = "Shipping", Company = company};
    var emp1 = new Employee {Name = "Jill Carpenter", Department = acc};
    var emp2 = new Employee {Name = "Steven Hill", Department = ship};
    context.Employees.Add(emp1);
    context.Employees.Add(emp2);
    context.SaveChanges();
}

// 第一种方法
using (var context = new Recipe8Context())
{
    // 假设我们已经拥有一个employee
    var jill = context.Employees.First(o => o.Name == "Jill Carpenter");

    // 获取Jill的部门和公司, 但我们需要重新加载employee
    var results = context.Employees.Include("Department.Company")
                         .First(o => o.EmployeeId == jill.EmployeeId);
    Console.WriteLine("{0} works in {1} for {2}", jill.Name, jill.Department.Name,
                      jill.Department.Company.Name);
}

//更有效的方法, 不用再加载employee
using (var context = new Recipe8Context())
{
    // 假设我们已经拥有一个employee
    var jill = context.Employees.Where(o => o.Name == "Jill Carpenter").First();


    //凭借Entry、Reference,Query和Include方法获取Department和Company数据,不用去查询底层的Employee表
    context.Entry(jill).Reference(x => x.Department).Query().Include(y => y.Company).Load();

    Console.WriteLine("{0} works in {1} for {2}", jill.Name, jill.Department.Name,
                      jill.Department.Company.Name);
}

Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();


代码清单5-21的输出如下:

Jill Carpenter works in Accounting for Acme Products
Jill Carpenter works in Accounting for Acme Products


原理

如果我们还没有Employee实体的实例,我们可以简单地使用Include()方法和一个查询路径 Department.Company来实现 。这是我们这前使用的方法。它的缺点是,它会获取employee实体的所有列。有很多情况下,这是一个昂贵的操作。因为我们已经加载对象到上下文中,再一次去数据库获取所有的列并传输到上下文中,这是一个浪费!

在第二个查询中,我们使用上下文对象DbContext公布的Entry()方法访问Employee对象并对其执行操作。然后我们链式调用Reference()方法和DbReferenceEntity类的Query()方法,返回一个从数据库中加载关联对象Deparment的查询。另外,我们链式调用Include()方法来拉取关联对象Company的信息。正如所期望的那样,这个查询获取了Department和Company的数据,它没有去获取Employees的数据,因为这些数据已经存在于上下文对象中了。

《Entity Framework 6 Recipes》中文翻译系列