DbContext 查询

使用LINQ to Entities来写查询语句

  Entity Framework查询是使用的.NET Framework功能Language Integrated Query,AKA LINQ。LINQ与.NET的编程体验是紧密集成在一起的,它提供了强类型的查询,何谓强类型,各位自行补脑。与弱类型的查询相比,它提供了编译时检查来保证你的查询通过验证以及IntelliSense。

  LINQ是一个通用的查询框架,并不仅仅是针对Entity Framework或者数据库的,LINQ提供程序负责把LINQ查询转换成对数据的查询并返回结果,对Entity Framework来说这个提供程序就是LINQ to Entities,它负责把LINQ查询转换成对数据库的SQL查询,你提供给EF关于你的模型的信息以及模型如何映射到数据的的信息都会用来执行这个转换,一旦查询返回,EF就负责把结果数据返回给你的模型(拷贝数据给你的模型实例)。

注意除了LINQ,EF也支持基于文本的查询,这就是Entity SQL或者叫ESQL。ESQL通常使用在更高级的场景中:需要在运行时动态构造查询。当然有同学也会提到表达式树,是的。因为ESQL是基于文本的,所以当应用程序需要创建对在运行时尚未知道的模型的查询语句时也是有用的。鉴于ESQL不太常用,它不是直接由DbContext API提供的,如果你的应用程序需要使用ESQL,你需要通过ObjectContext API使用IObjectContextAdapter接口。

 

下面为了演示DbContext查询方面的功能我们建立下面几个Model以及Context

 1 [Table("Locations", Schema = "baga")] 

   publicclass Destination
 2 {
 3     public Destination()
 4     {
 5       this.Lodgings = new List<Lodging>();
 6     }
 7 
 8     [Column("LocationID")]
 9     public int DestinationId { getset; }

10     [Required, Column("LocationName")]
11     [MaxLength(200)]
12     public string Name { getset; }
13     public string Country { getset; }

14     [MaxLength(500)]
15     public string Description { getset; }

16     [Column(TypeName = "image")]
17     public byte[] Photo { getset; }
18     public string TravelWarnings { getset; }
19     public string ClimateInfo { getset; }
20 
21     public virtual List<Lodging> Lodgings { getset; }
22 } 

 

 1 public class Lodging
 2 {
 3     public int LodgingId { getset; }
 4     [Required]
 5     [MaxLength(200)]
 6     [MinLength(10)]
 7     public string Name { getset; }
 8     public string Owner { getset; }
 9     public decimal MilesFromNearestAirport { getset; }
10 
11     [Column("destination_id")]
12     public int DestinationId { getset; }
13     public Destination Destination { getset; }
14     public List<InternetSpecial> InternetSpecials { getset; }
15     public Nullable<int> PrimaryContactId { getset; }

16     [InverseProperty("PrimaryContactFor")]
17     [ForeignKey("PrimaryContactId")]
18     public Person PrimaryContact { getset; }
19     public Nullable<int> SecondaryContactId { getset; }

20     [InverseProperty("SecondaryContactFor")]
21     [ForeignKey("SecondaryContactId")]
22     public Person SecondaryContact { getset; }
23 }

 

 1 public class BreakAwayContext : DbContext
 2 {
 3     public DbSet<Destination> Destinations { getset; }
 4     public DbSet<Lodging> Lodgings { getset; }
 5     public DbSet<Trip> Trips { getset; }
 6     public DbSet<Person> People { getset; }
 7     public DbSet<Reservation> Reservations { getset; }
 8     public DbSet<Payment> Payments { getset; }
 9     public DbSet<Activity> Activities { getset; }

10 } 

查询所有数据 

  最简单的查询就是对一个给定的实体类型返回所有数据。同等于SELECT * FROM MYTABLE 查询语句。EF会为你转换LINQ查询成SQL查询。

  获取所有数据并不需要整的去写一个查询,你只需要简单的迭代DbSet提供的内容,EF会发送一个查询到数据库来找寻DbSet其中所有的数据。

  Example 2-4 查询所有的destionations

   private static void PrintAllDestinations()

 {
      using (var context = new BreakAwayContext())
      {
           foreach (var destination in context.Destinations)
            {
                Console.WriteLine(destination.Name);
            }
        }
 }

  执行之后明了的显示了表内所有的地点的名称。

  当代码开始迭代Destionations中的内容的时候,EF发布了对数据库的SQL查询来返回所需数据:

  SELECT

 

  [Extent1].[LocationID] AS [LocationID],
  [Extent1].[LocationName] AS [LocationName],
  [Extent1].[Country] AS [Country],
  [Extent1].[Description] AS [Description],
  [Extent1].[Photo] AS [Photo]
  FROM [baga].[Locations] AS [Extent1]

  细心的朋友可以看到Destionation Model特性跟语句表名及架构的对应关系。 这个SQL语句与我们平常写的不太像,这是因为EF内置了通用的查询语句生成算法,其不仅仅要考虑简单查询还需要考虑的非常复杂的场景。

  重点:每次你迭代DbSet的时候EF都会执行一次对数据库的查询,如果你不停的查询相同的数据会对性能造成影响。为了避免这样,你可以使用ToList方法拷贝结果到一个List中,然后再迭代这个List的内容,而不会造成每次都查询数据库。

  迭代DbSet只会返回存在于数据库中的数据,任何存在内存中的对象处在等待被保存到数据库中的状态时候都不会返回,如果需要这些对象也被返回到查询结果中,你可以使用"Query Local Data"技术,后续会讲到。 

使用LINQ排序、筛选... 

  先看个例子

  Example 2-6 对destinations的查询结果排序 :

 1 private static void PrintDestinationNameOnly()
 2 {
 3       using (var context = new BreakAwayContext())
 4       {
 5         var query = from d in context.Destinations
 6                     where d.Country == "Australia"
 7                     orderby d.Name
 8                     select d.Name;
 9 
10         foreach (var name in query)
11         {
12           Console.WriteLine(name);
13         }
14       }
15 }

  上面的语句有点类似于SQL,这种写法我们有个名字叫LINQ query sytenx,很好理解:查询Destionatinos结果按Name排序,记住一点,EF不会立即执行数据库查询直到程序中首次使用返回的结果。

   还有一种相同功能的写法:

   var query = context.Destinations
   .Where(d => d.Country == "Australia")
   .OrderBy(d => d.Name)
   .Select(d => d.Name);

   这个方法使用了lambda表达式来定义了查询。

   LINQ是很强大的查询语句,上面的示例只是触碰到了表皮而已,我这篇文章主要侧重于DbContext,所以关于LINQ更多的功能及用法可以查看MSDN:

   http://msdn.microsoft.com/zh-cn/library/bb399367.aspx

 查询单个对象

  通过给予的key来查询单个对象是最常用的使用场景,DbContext API 通过提供DbSet.Find方法来使它非常简单,这里有个注意点:如果Find方法找不到指定key的实体,它将返回null。

  Find方法的一个优点,前一篇博客已有介绍,在这里它还有一个功能:它能够查询最近的是添加状态的数据(内存中),这个数据还没有被保存到数据库中。

  Find使用下面几个简单的规则来定位到对象:

  1.在内存中查询已经存在的从数据库中加载到内存中的实体,或者附加到上下文中的实体(通过Attach方法)。

  2.查询内存中是添加状态还没有保存到数据库中的实体。

  3.查询数据库中还没有被加载到内存中的实体。

  Find方法的使用在这就不举例了。

  Find方法的使用在这边有个特殊的情况要说明下:也即复合主键的实体的查询如何使用Find方法。

  举例:context.DemoEntity.Find("first key","second key");

  这里参数中多个key值的顺序要按照数据库列的顺序(DB First)或者模型的定义的主键字段的顺序(Modal First、Code First)来传入。

  也有很多场景下查询单个实体不能使用Find方法的情况,比如:不是通过key来查询的、想要在查询中包含关联数据的。要实现这些功能,你就必须要构建标准的LINQ查询语句,然后使用Single方法来返回单个结果。示例如下:

  var query = from d in context.Destinations

      where d.Name="demoName"

              select d;

  var result = query.Single();

  如果查询返回无返回结果,调用Single方法会报错,我们可以替代使用名如其意的SingleOrDefault方法:如果无返回结果就返回空。

  SingleOrDefault方法使用与Find方法相同的数据库查询来查询数据库中的数据。这个SQL 选择TOP 2 结果来保证只有一个匹配:

  SELECT TOP (2)

  [Extent1].[LocationID] AS [LocationID],

  [Extent1].[LocationName] AS [LocationName],

  [Extent1].[Country] AS [Conuntry],

  ...

  FROM [baga].[Locations] AS [Extent1]

  WHERE  [Extent1].[LocationName]='demoName'

  如果两行数据被查找到,Single跟SingleOrDefault方法会发生异常,如果你只是想要第一个结果,而且并不关心是否会超过一个结果,你可以使用First或者FirstOrDefault.

查询本地数据(Querying Local Data)

  使用Querying Local Data的其中一个原因是:为了避免多次查询数据库,并且你已经知道数据已经被加载到内存中了,在上一篇文章中我们已经知道了其中的一个方法就是使用ToList方法拷贝结果到一个List。这个方法在我们在同一个代码段内使用这个数据是可以的。但如果我们在应用程序内传递这个List时,事情就变的有点麻烦了。比如:我们在应用程序初始化的时候就从数据库库中查询并加载了所有的Destinations,之后应用程序的不同区域对数据进行不同的查询。在一些地方我们想要呈现所有的Destinations,在另外一些地方我们想要对名字进行排序,在另外一些地方我们想要对字段Country进行筛选,相较于在应用内传递Destinations列表数据,我们更愿意利用数据上下文(context)在跟踪所有实例的事实,并查询本地数据。

  另一个原因:如果你想返回的查询结果包含最近是添加状态的数据(还没有被保存到数据库中)。对LINQ查询执行ToList方法总是会发送查询请求到数据库。这就意味着前一句的需求得不到满足,而使用本地查询,就可以包含状态是添加状态并且没有保存到数库的数据。

  通过DbSet的Local属性我们可以操作内存中的数据。Local会返回所有从数据库中加载到内存中的数据,并包含是添加、修改、删除状态单并没有保存到数据库中的数据。 

  我们通过一个例子来演示下它的用法:查询有多少个Destinations在内存中并且可被查询。

  Example 2-17

  1 public static void GetLocalDestinationCount()

2 {
3    using(var context = new BreakAwayContext())
4   {
5     var count = context.Destinations.Local.Count;
6     Console.WriteLine("Destinations in memory:{0}",count);
7   }      

8 } 

  运行程序我们可以看到如下输出:

  Destinations in memory:0

  我们得到0的结果是因为我们还没有运行任何查询来从数据库中获取Destinations,并且我们也没有增加任何新的Destination对象,让我们更新下上的方法,在读取Local.Count之前查询下数据库。

  Example 2-18

   1 public static void GetLocalDestinationCount()

 2 {
 3     using(var context = new BreakAwayContext())
 4    {
 5      foreach(var des in context.Destinations)
 6      {
 7        Console.WriteLine(des.Name);
 8      }
 9      var count = context.Destinations.Local.Count;
10      Console.WriteLine("Destinations in memory:{0}",count);
11    }      

12 }  

   执行更新后的方法我们可以获得如下输出:

   Grand Canyon

   Hawaii

   Destinations in memory:2

使用Load方法来加载数据到内存

  用foreach来遍历DbSet的内容是其中一种方法来把数据加载到内存中,但用这种方法来加载数据效率不高,并且代码的意图也有点表达不明确,特别的是当循环代码没有明确的处理本地查询。

  幸运的是DbContext API提供了Load方法,可以用来把DbSet从数据库加载到内存中,我们继续修改下示例2-18方法:

  Example 2-19: 

  1 public static void GetLocalDestinationCountWithLoad()

2 {
3     using(var context = new BreakAwayContext())
4    {
5      context.Destination.Load();
6      var count = context.Destinations.Local.Count;
7      Console.WriteLine("Destinations in memory:{0}",count);
8    }      
9 } 

  比较两种方法,示例2-19代码明确意图明显。

  ps:Load方法实际上是IQueryable<T>的扩展方法,定义在System.Data.Entity命名空间中,如果你要使用它记得引用这个命名空间。

  因为Load是IQueryable<T>的扩展方法,所以我们同样可以对LINQ查询的结果调用Load方法,把结果加载到内存中。

  Example 2-20

   1 public static void LoadAustralianDestination()

 2 {
 3     using(var context = new BreakAwayContext())
 4    {
 5      var query = from d in context.Destinations
 6                  where d.Country=="Australian"
 7                  select d;
 8      query.Load();
 9      var count = context.Destinations.Local.Count;
10      Console.WriteLine("Australian Destinations in memory:{0}",count);
11    }      

12 }  

   这次我们只是把国家是澳大利亚的Destinations加载到内存中了,我们查看程序输出,个数明显减少了。

   ps:我们已经知道了如果对LINQ查询使用Load方法会把结果加载到内存中,但它并没有把以前的查询删除掉,怎么理解呢?比如说:你先对国家是澳大利亚的Destinations查询结果调用了Load方法,然后你又对国家是美国的Destinations的查询结果调用Load方法,两个查询结果都会存在内存中,访问Local属性都会返回它们。

 

----这篇博客准备分几部分来讲,下篇博客会接着这一篇,下一篇会讲解一些查询的高级主题:Lazy Loading、Eager Loading、Explicit Loading等。另外排版样式真是头疼,比较丑,各位将就着看看哈。

 

 

 

 

 

 

 

posted @ 2014-11-15 21:23  Hephaestus  阅读(6045)  评论(1编辑  收藏  举报