Lazy Loading:延迟加载。Eager Loading:贪婪加载。
首先通过一个简单的控制台应用程序例子说明延迟加载:
订单主表和订单从表,并且订单主表和订单从表是一对多的关系,代码如下图所示:
//订单主表public class Order{public int Id { get; set; }public DateTime OrderDate { get; set; }//导航属性public virtual List<OrderDetail> OrderDetail { get; set; }}
//订单从表public class OrderDetail{public int Id { get; set; }public string ProductName { get; set; }public int Quantity { get; set; }public int Price { get; set; }public int XOrderId { get; set; }//导航属性public virtual Order Order { get; set; }}
Fluent API 代码如下:
public class kTStoreModel : DbContext{public kTStoreModel(): base("name=kTStoreModel"){}protected override void OnModelCreating(DbModelBuilder modelBuilder){modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();modelBuilder.Entity<OrderDetail>().HasRequired(od => od.Order).WithMany(o => o.OrderDetail).HasForeignKey(x => x.XOrderId);}public virtual DbSet<Order> Order { get; set; }public virtual DbSet<OrderDetail> OrderDetail { get; set; }}
运行程序建立表结构后,在数据表中插入一些测试数据,并修改Main函数方法如下图所示:
static void Main(string[] args)
{using (kTStoreModel db = new kTStoreModel()){foreach (Order order in db.Order){Console.WriteLine("订单编号:{0},订单时间:{1}", order.Id, order.OrderDate);foreach (OrderDetail detail in order.OrderDetail){Console.WriteLine("\t名称:{0},数量:{1},价格:{2}",detail.ProductName,detail.Quantity,detail.Price);}}Console.ReadKey();}
}
再次运行程序,结果如下图所示:
上述例子演示了一个简单的查询功能, 接下去修改Main函数中的代码如下图所示:
using (kTStoreModel db = new kTStoreModel())
{db.Database.Log = Console.Write;var cs = db.Order;Console.WriteLine("\n\n开始执行SQL》》\n\n");Order order = cs.First();Console.WriteLine("\n\n开始执行导航属性 》》\n\n");List<OrderDetail> details = order.OrderDetail;
}
Console.ReadKey();
运行程序,执行结果如下图所示:
可以看到,生成了两条SQL:第一条SQL是查询Order数据表的,第二条SQL是查询OrderDetail表的。载入完整的关联数据容易造成应用程序的执行瓶颈。因此EntityFramework通过所谓的Lazy Loading避免一次载入过多的的数据内容,第一层循环并未触及OrderDetail相关的数据,只有到第二层循环时才会载入。
使用Eager Loading
如果想采用Eager Loading预先知道加载的所有数据,包含导航属性关联的内容,DbSet中的Include方法支持这种加载机制,修改Main函数中的方法如下图所示:
using (kTStoreModel db = new kTStoreModel()){db.Database.Log = Console.Write;Console.WriteLine("\n\n开始执行SQL》》\n\n");var cs = db.Order.Include("OrderDetail").Select(x => x).ToList();Console.WriteLine("\n\n开始执行导航属性 》》\n\n");var details = cs.First().OrderDetail;}Console.ReadKey();
运行结果如下:
从上图中可以看到程序一次性加载了所有的数据,在执行导航属性的阶段不用再去执行另外的SQL语句了。
调整Virual属性
由于EF默认为延迟加载,因此在没有额外设置情况下都是将导航属性设置为vriual,如此一来当明确读取这个关联属性时,户籍自动将相关数据逐一取出。如下图代码所示的情况,会执行两段SQL:
using (kTStoreModel db = new kTStoreModel())
{Order order = db.Order.First();Console.WriteLine("订单编号:{0},订单时间:{1}", order.Id, order.OrderDate);foreach (OrderDetail detail in order.OrderDetail){Console.WriteLine("\t名称:{0},数量:{1},价格:{2}", detail.ProductName, detail.Quantity, detail.Price);}Console.ReadKey();
}
如果删除Order实体类中的导航属性的Virual声明,将不会生成第二段查询OrderDetail的SQL,foreach循环将会报错。
强制调用Include方法可以让我们在一开始就取出所有的数据,但是我只需要特定数据对象时,这就会有性能浪费的问题。这种情况下,如果想要自行取出关联的数据,可以进一步调用Load方法,调整代码如下图所示:
using (kTStoreModel db = new kTStoreModel())
{db.Database.Log = Console.Write;Order order = db.Order.First();db.Entry(order).Collection(c => c.OrderDetail).Load();Console.WriteLine("订单编号:{0},订单时间:{1}", order.Id, order.OrderDate);foreach (OrderDetail detail in order.OrderDetail){Console.WriteLine("\t名称:{0},数量:{1},价格:{2}", detail.ProductName, detail.Quantity, detail.Price);}Console.ReadKey();
}
运行效果下图所示:
LazyLoadingEnable属性
除了修改实体类定义以外,比较弹性的做法是通过配置来达到相同的效果。通过配置以下代码,关闭了Lazy Loading操作,因此不会再自动加入关联的数据。
using (kTStoreModel db = new kTStoreModel())
{db.Configuration.LazyLoadingEnabled = false;//关闭延迟加载// etc.....
}
Include方法与对象加载
可以通过Include完整的载入导航属性的关联数据,对于更复杂的关联数据,可以串联Include方法加载更完整的数据。
假设存在如下几个实体类:
public class Order
{public int Id {get;set;}public DateTime OrderDate {get;set;}public int CustomerId{get;set;}//导航属性public virual ICollection<OrderDetail> OrderDetails {get;set;}public virual Customer Customer{get;set;}
}public class OrderDetail
{public int Id{get;set;}public int OrderId {get;set;}public int ProductId{get;set;}public int Quantity {get;set;}public int Price{get;set;}//导航属性public virtual Order Order{get;set;}public virtual Product Product{get;set;}
}public class Product
{public int Id{get;set;}public string Name {get;set;}public int Price {get;set;}//导航属性public virtual ICollection<OrderDetail> OrderDetails{get;set;}
}public class Customer
{public int Id {get;set;}public string ContactName{get;set;}public string Phone{get;set;}public string Email{get;set;}//导航属性public virual ICollection<Order> Orders {gets;set;}
}
如果需要以客户数据为主体,按顺序输出此客户的所有订单数据以及每一笔订单的商品明细与对应的价格,可以参考如下程序:
static void Main(string[] args)
{using(var context = new KTStoreModel()){context.Database.Log = Console.Write;var customer = context.Customers;foreach(var c in customer){Consolem.WriteLine("\tOrder_Id:{0}",o.Id);foreach(var o in c.Orders){Console.WriteLine("\tOrder_Id:{0}",o.Id);foreach (var od in o.OrderDetail){Console.WriteLine("\t\tProduct_Id:{0}\tPrice:{1}",od.ProductId,od.Price);}}Console.Read();}}
}
不考虑性能问题,以上的程序代码即可完成关联数据的列举并以嵌套结构输出。但是每次嵌套循环会产生大量SQL,这还不是一个好的选择。修改程度代码如下图所示:
using(var context = new KTStoreModel()){context.Database.Log = Console.Write;var customer = context.Customers.Include("Orders.OrderDetails").Include("Orders.OrderDetails.Product");foreach(var c in customer){Consolem.WriteLine("\tOrder_Id:{0}",o.Id);foreach(var o in c.Orders){Console.WriteLine("\tOrder_Id:{0}",o.Id);foreach (var od in o.OrderDetail){Console.WriteLine("\t\tProduct_Id:{0}\tPrice:{1}",od.ProductId,od.Price);}}Console.Read();}}
更进一步,使用Where筛选数据可以进一步避免返回不需要的数据,如下图所示:
var customer = context.Customers.Include("Orders.OrderDetails").Include("Orders.OrderDetails.Product").Where(c => c.Id < 5)