Skip to content

Files

Latest commit

d71eae6 · Jan 10, 2022

History

History
891 lines (663 loc) · 27.5 KB

04.md

File metadata and controls

891 lines (663 loc) · 27.5 KB

四、向数据库写入数据

正如您可能期望的那样,实体框架提供了少量的 API 来将数据从数据库获取到对象中,旨在覆盖稍微不同的场景。

所有数据访问层都必须支持主键加载,当然 EFCF 也是这样做的。为此,我们有寻找的方法:

代码清单 53

//retrieving a record by a single primary key consisting of an integer.
var project = ctx.Projects.Find(1);

该方法可以采用任意数量的参数;它支持具有复合主键的实体。下面的例子说明了这一点:

代码清单 54

//retrieving a record by a composite primary key consisting of two integers.
var project = ctx.SomeEntities.Find(1, 2);

如果没有找到匹配的记录,查找将返回NULL;不会抛出异常,因为这是一个完全有效的结果。

| | 提示:参数的顺序和类型必须与该实体映射中定义的主键属性的顺序相匹配。 |

自从它与。NET 3.5,语言集成查询(LINQ)已经成为查询任何类型数据的事实上的标准,所以 Entity Framework 有 LINQ 的支持也就不足为奇了。对于大多数场景,它可能是您的首选应用编程接口。它是强类型的(意味着你可以在编译时知道有些事情是不对的),重构友好,并且它的语法很容易理解。让我们看一些例子。

一个 LINQ 到实体的查询是从上下文暴露的 DbSet < T > 类型的实体集合属性构建的,它由一个 IQueryable < T > 实现组成。

| | 提示:别忘了 LINQ 是关于质疑的。您在这里找不到任何更改数据的方法。 |

LINQ 查询的执行被延迟到GetEnumerator被调用,这发生在一个foreach循环中,或者当终端操作符如到列表到数组到列表任何计数长计数单个单个默认第一个时您可以选择该查询并开始添加限制,如分页或排序。

代码清单 55

//create a base query.
var projectsQuery = from p in ctx.Projects select p;

//add sorting.
var projectsSortedByDateQuery = projectsQuery.OrderBy(x => x.Start);

//execute and get the sorted results.
var projectsSortedByDateResults = projectsSortedByDateQuery.ToList();

//add paging and ordering (required for paging).
var projectsWithPagingQuery = projectsQuery
  .OrderBy(x => x.Start)
  .Take(5)
  .Skip(0);

//execute and get the first 5 results.
var projectsWithPagingResults = projectsWithPagingQuery.ToList();

//add a restriction
var projectsStartingAWeekAgoQuery = projectsQuery
        .Where(x => x.Start.Year >= DateTime.Today.Year);

//execute and get the projects that started a week ago.
var projectsStartingAWeekAgoResults = projectsStartingAWeekAgoQuery.ToList();

你最多只能得到一个结果。

代码清单 56

//retrieving at most a single record with a simple filter. var project = ctx.Projects.SingleOrDefault(x => x.ProjectId == 1);

受几个属性的限制也同样容易:

代码清单 57

//retrieving multiple record with two filters. 
var projects = ctx.Projects
  .Where(x => x.Name.Contains("Something") && x.Start >= DateTime.Today)
  .ToList();

或者两个条件中的一个匹配:

代码清单 58

//or
var resourcesKnowingVBOrCS = ctx.Technologies
  .Where(t => t.Name == "VB.NET" || t.Name == "C#")
  .SelectMany(x => x.Resources)
  .Select(x => x.Name)
  .ToList();

计数结果:

代码清单 59

//count
var numberOfClosedProjects = ctx.Projects
  .Where(x => x.End != null && x.End < DateTime.Now)
  .Count();

检查记录是否存在:

代码清单 60

//check existence
var existsProjectBySomeCustomer = ctx.Projects
  .Any(x => x.Customer.Name == "Some Customer");

执行投影(仅获取实体的某些部分):

代码清单 61

//get only the name of the resource and the name of the associated project.
var resourcesXprojects = ctx.Projects
  .SelectMany(x => x.ProjectResources)
  .Select(x => new { Resource = x.Resource.Name, Project = x.Project.Name })
  .ToList();

进行聚合:

代码清单 62

var avgResources = ctx.Projects.Average(p => p.ProjectResources.Count());

获取不同的值:

代码清单 63

//distinct roles performed by a resource.
var roles = ctx.Resources
  .SelectMany(x => x.ProjectResources)
  .Where(x => x.Resource.Name == "Ricardo Peres")
  .Select(x => x.Role)
  .Distinct()
  .ToList();

groupon a 属性:

代码清单 64

//grouping and projecting.
var resourcesGroupedByProjectRole = ctx.Projects
  .SelectMany(x => x.ProjectResources)
  .Select(x => new { Role = x.Role, Resource = x.Resource.Name })
  .GroupBy(x => x.Role)
  .Select(x => new { Role = x.Key, Resources = x })
  .ToList();

//grouping and counting.
var projectsByCustomer = ctx.Projects
  .GroupBy(x => x.Customer)
  .Select(x => new { Customer = x.Key.Name, Count = x.Count() })
  .ToList();

//top 10 customers having more projects in descending order.
var top10CustomersWithMoreProjects = ctx.Projects
  .GroupBy(x => x.Customer.Name)
  .Select(x => new { x.Key, Count = x.Count() })
  .OrderByDescending(x => x.Count)
  .Take(10)
  .ToList();

使用子查询的结果:

代码清单 65

//subquery
var usersKnowingATechnology = (from r in ctx.Resources where r.Technologies.Any(x 
=> (from t in ctx.Technologies where t.Name == "ASP.NET" select t).Contains(x)) 
select r)
  .ToList();

部分匹配( LIKE ):

代码清单 66

  //like
  var aspTechnologies = (from t in ctx.Technologies

  where EF.Functions.Like(t.Name, "asp%")

  select t)
    .ToList();

| | 提示:注意 EF 中的静态 Like 方法。函数类。 |

最后,您可以检查一组值中的一个:

代码清单 67

//contains
var customersToFind = new string[] { "Some Customer", "Another Customer" };
var projectsOfCustomers = ctx.Projects
  .Where(x => customersToFind.Contains(x.Customer.Name))
  .ToList();

| | 注意:如果你想知道,LINQ 查询中的所有文字(字符串、数字、日期等)。)将被转换为参数,用于正确的执行计划重用。 |

| | 提示:在 EF Core 2.0 中,既不能进行数学运算,也不能进行日期和时间的运算。 |

从 EF Core 1.0 开始,您可以在查询中混合使用服务器端和客户端代码。例如,如果 LINQ 解析器发现一些它不知道如何转换成数据库调用的方法调用,它将在客户端静默执行它。根据调用的不同,它可以被证明是有用的,也可以导致性能下降。作为后者的一个例子,想象一下您正在通过客户端方法进行过滤的情况:您需要带来所有记录,对它们应用过滤,并且仅在返回结果之后。但如果谨慎使用,它确实会有用;这里有一个例子:

代码清单 68

  var projects = from p in
  ctx.Projects select new { p.Name, Age =
  CalculateAge(p.StartDate) };

另外,如果您想禁用它,有效地恢复到旧的前核心行为,您所需要做的就是配置日志记录,以便在客户端方法调用时引发异常:

代码清单 69

  services.AddDbContext<ProjectsContext>(options =>
  {
    options

  .UseSqlServer("<connection
  string>")

  .ConfigureWarnings(options =>

  options.Throw(RelationalEventId.QueryClientEvaluationWarning));
  });

SQL

尽管我们可能会尝试,但事实是,当您使用关系数据库时,不可能逃避 SQL。这可能是因为性能通常更好,或者因为使用任何其他 API 都很难甚至不可能表达某些查询,但事实就是如此。实体框架代码优先(和核心)完全支持 SQL,包括:

  • 获取实体和值。
  • 执行INSERTUPDATEDELETE
  • 调用函数和存储过程。

它确实有你可能会想到的所有缺点:

  • 它不是强类型的。
  • 没有编译时检查。
  • 如果使用特定于数据库的函数,并且以新数据库为目标,则必须重写 SQL。
  • 即使是简单的事情,例如分页或限制要返回的记录数量,您也必须知道正确的语法。

我演示的第一个案例是如何执行一个SELECT并将结果转换成一个实体。为此,我们将使用数据库设置<测试>T3 的FromSql方法。

代码清单 70

//simple select
var projectFromSQL = ctx.Projects
  .FromSql("SELECT * FROM Project WHERE Name = @p0", "Big Project")
  .SingleOrDefault();

| | 提示:注意我们如何在 SQL 之后直接传递参数;每个都必须命名为@p0@p1等等。 |

如果我们想从表值函数中检索一个实体,我们可以使用以下方法:

代码清单 71

//table-valued function
var projectFromFunction = ctx.Projects
  .FromSql("SELECT * FROM dbo.GetProjectById @p0", 1)
  .SingleOrDefault();

其中GetProjectById函数可能是这样的:

代码清单 72

  CREATE FUNCTION dbo.GetProjectById
  (     
        @ProjectID
  INT
  )
  RETURNS TABLE 
  AS
  RETURN 
  (
        SELECT
  *
        FROM
  Project
        WHERE
  ProjectId = @ProjectID
  )
  GO

| | 提示:不要忘记,如果您想要返回实体,您的 SQL 必须返回与这些实体的属性相匹配的列,正如在其映射中所指定的那样。 |

如果我们想要执行任意的 SQL 修改命令(UPDATEDELETEINSERT,我们将需要检索底层的 DbConnection ,如下例所示。

代码清单 73

  //get the ADO.NET
  connection.
  var con = ctx.Database.GetDbConnection();

  //create a command.
  var cmd = con.CreateCommand();
  cmd.CommandText = 
    "UPDATE ProjectDetail SET Budget = Budget * 1.1 WHERE ProjectId = @p0";

  //create a parameter.
  var parm = cmd.CreateParameter();
  parm.ParameterName = "p0";
  cmd.Parameters.Add(parm);

  //update records.
  var updatedRecords =
  cmd.ExecuteUpdate();

在 EF Core 2 中还有一个新的东西,它存在于 pre Core EF 中:执行数据库功能的能力。将[DbFunction]属性应用于静态方法,如下所示:

代码清单 74

  [DbFunction("ComputeHash")]
  public static int ComputeHash(this string phrase)
  {
    throw new NotImplementedException();
  }

如您所见,它的实现无关紧要,因为它永远不会被代码调用。要记住四件事:

  • 它只能接收标量作为参数。
  • 它只能返回标量类型。
  • 需要在DbContext.类中声明
  • 它需要是静态的。

如果数据库函数与方法同名,则[DbFunction]属性的名称参数不是必需的,但是如果在默认模式下找不到函数,您也可以添加模式名称。现在你可以这样使用它:

代码清单 75

  var hash = from p in
  ctx.Projects select p.Name.ComputeHash();

这将在数据库上执行数据库函数并返回其结果。您也可以在其他上下文中使用它,例如过滤。

英孚核心的一个新特性是能够将 LINQ 和 SQL 混合在一起;这可以让你两全其美:

  • 从 EF 内部执行任意复杂的 SQL 查询。
  • 把它们变成。NET 类。
  • 在顶部应用强类型的 LINQ 查询。

让我们看一个例子:

代码清单 76

  var ps = ctx.Projects
    .FromSql("SELECT p.* FROM Project p")
    .OrderBy(p => p.Start)
    .ToList();

您可以从任何有效的 SQL 中馈入FromSql方法,包括存储过程或函数。请注意,该方法是原型化的,以返回Project的实例,因此任何要执行的查询都需要根据映射返回可以映射到Projects的列。

在某些情况下,您希望同时加载主实体和关联实体的所有数据。这很可能有两个原因之一:

  • 您肯定必须访问一些导航属性,并且出于性能原因,您需要预先加载它们(例如,您需要查看所有订单详细信息)。
  • 实体(及其关联实体)的生命周期可能会比获取它的上下文更长(例如,您要将实体存储在某个缓存中),因此它将无法访问它,因此延迟加载是不可能的。

进入急装。急切加载的意思是,当发出查询时,您显式地声明实体框架将与根实体一起带来的扩展路径。然后,EF 将生成一个不同于通常使用JOIN进行所有所需关联的 SQL 表达式。

例如,下面的查询带来了一个Customer及其所有的Projects,并引入了包含的方法。

代码清单 77

  //explicitly eager
  load the Customer for each project.
  var projectsAndTheirCustomers = ctx.Projects
    .Include(x => x.Customer)
    .ToList();

对于记录,这将生成以下 SQL 语句:

代码清单 78

  SELECT
  [Extent1].[ProjectId] AS [ProjectId],
  [Extent1].[Name] AS [Name],
  [Extent1].[Start] AS [Start],
  [Extent1].[End] AS [End],
  [Extent2].[CustomerId] AS [CustomerId],
  [Extent2].[Contact_Email]
  AS [Contact_Email],
  [Extent2].[Contact_Phone]
  AS [Contact_Phone],
  [Extent2].[Name] AS [Name1]
  FROM  [dbo].[Project] AS [Extent1]
  INNER JOIN [dbo].[Customer] AS [Extent2] ON 
  [Extent1].[Customer_CustomerId] = [Extent2].[CustomerId]

Include 方法也可以取一个String作为其参数,该参数必须是导航属性的名称(引用或集合)。

代码清单 79

  //explicitly eager
  load the Customer for each project.
  var projectsAndTheirCustomers = ctx.Projects
    .Include("Customer")
    .ToList();

可以指定多个路径。

代码清单 80

  //two
  independent include paths.
  var resourcesProjectResourcesAndTechnologies = ctx.Resources
    .Include(x => x.ProjectResources)
    .Include(x => x.Technologies)
    .ToList();

在这种情况下,SQL 将如下所示。

代码清单 81

  SELECT
  [UnionAll1].[ResourceId] AS [C1],
  [UnionAll1].[ResourceId1] AS [C2],
  [UnionAll1].[ResourceId2] AS [C3],
  [UnionAll1].[Contact_Email]
  AS [C4],
  [UnionAll1].[Contact_Phone]
  AS [C5],
  [UnionAll1].[Name] AS [C6],
  [UnionAll1].[C1] AS [C7],
  [UnionAll1].[ProjectResourceId]
  AS [C8],
  [UnionAll1].[ProjectResourceId1] AS [C9],
  [UnionAll1].[Role] AS [C10],
  [UnionAll1].[Project_ProjectId]
  AS [C11],
  [UnionAll1].[Resource_ResourceId] AS [C12],
  [UnionAll1].[C2] AS [C13],
  [UnionAll1].[C3] AS [C14]
  FROM  (SELECT
          CASE
  WHEN ([Extent2].[ProjectResourceId] IS
  NULL) THEN CAST(NULL AS int)
   ELSE
  1 END AS [C1],
          [Extent1].[ResourceId] AS [ResourceId],
          [Extent1].[ResourceId] AS [ResourceId1],
          [Extent1].[ResourceId] AS [ResourceId2],
          [Extent1].[Contact_Email] AS [Contact_Email],
          [Extent1].[Contact_Phone] AS [Contact_Phone],
          [Extent1].[Name] AS [Name],
          [Extent2].[ProjectResourceId]
  AS [ProjectResourceId],
          [Extent2].[ProjectResourceId]
  AS [ProjectResourceId1],
          [Extent2].[Role] AS [Role],
          [Extent2].[Project_ProjectId]
  AS [Project_ProjectId],
          [Extent2].[Resource_ResourceId]
  AS [Resource_ResourceId],
          CAST(NULL AS int) AS [C2],
          CAST(NULL AS varchar(1)) AS [C3]
          FROM 
  [dbo].[Resource] AS [Extent1]
          LEFT
  OUTER JOIN [dbo].[ProjectResource] AS
  [Extent2] ON [Extent1].[ResourceId] = [Extent2].[Resource_ResourceId]
  UNION ALL
          SELECT
          2 AS
  [C1],
          [Extent3].[ResourceId] AS [ResourceId],
          [Extent3].[ResourceId] AS [ResourceId1],
          [Extent3].[ResourceId] AS [ResourceId2],
          [Extent3].[Contact_Email] AS [Contact_Email],
          [Extent3].[Contact_Phone] AS [Contact_Phone],
          [Extent3].[Name] AS [Name],
          CAST(NULL AS int) AS [C2],
          CAST(NULL AS int) AS [C3],
          CAST(NULL AS int) AS [C4],
          CAST(NULL AS int) AS [C5],
          CAST(NULL AS int) AS [C6],
          [Join2].[TechnologyId] AS [TechnologyId],
          [Join2].[Name] AS [Name1]
          FROM 
  [dbo].[Resource] AS [Extent3]
          INNER
  JOIN  (SELECT [Extent4].[Resource_ResourceId] AS
  [Resource_ResourceId],
  [Extent5].[TechnologyId] AS [TechnologyId], [Extent5].[Name] AS [Name]
                  FROM  [dbo].[TechnologyResource]
  AS [Extent4]
                  INNER JOIN [dbo].[Technology] AS [Extent5] ON [Extent5].[TechnologyId] = [Extent4].[Technology_TechnologyId] )
  AS [Join2] ON [Extent3].[ResourceId] = [Join2].[Resource_ResourceId]) AS [UnionAll1]
  ORDER BY [UnionAll1].[ResourceId1] ASC, [UnionAll1].[C1] ASC

下面是多级包含的最后一个例子:

代码清单 82

  //multilevel
  include paths.
  var resourcesProjectResourcesCustomers = ctx
    .Resources
    .Include(x => x.ProjectResources.Select(y => y.Project.Customer))
    .ToList();

生成的 SQL 如下所示:

代码清单 83

  SELECT 
  [Project1].[ResourceId] AS [ResourceId], 
  [Project1].[Contact_Email]
  AS [Contact_Email], 
  [Project1].[Contact_Phone]
  AS [Contact_Phone], 
  [Project1].[Name] AS [Name], 
  [Project1].[C1] AS [C1], 
  [Project1].[ProjectResourceId]
  AS [ProjectResourceId], 
  [Project1].[Role] AS [Role], 
  [Project1].[ProjectId] AS [ProjectId], 
  [Project1].[Name1] AS [Name1], 
  [Project1].[Start] AS [Start], 
  [Project1].[End] AS [End], 
  [Project1].[CustomerId] AS [CustomerId], 
  [Project1].[Contact_Email1]
  AS [Contact_Email1], 
  [Project1].[Contact_Phone1]
  AS [Contact_Phone1], 
  [Project1].[Name2] AS [Name2], 
  [Project1].[Resource_ResourceId] AS [Resource_ResourceId]
  FROM ( SELECT 
        [Extent1].[ResourceId] AS [ResourceId], 
        [Extent1].[Contact_Email] AS [Contact_Email], 
        [Extent1].[Contact_Phone] AS [Contact_Phone], 
        [Extent1].[Name] AS [Name], 
        [Join2].[ProjectResourceId]
  AS [ProjectResourceId], 
        [Join2].[Role] AS [Role], 
        [Join2].[Resource_ResourceId]
  AS [Resource_ResourceId], 
        [Join2].[ProjectId] AS [ProjectId], 
        [Join2].[Name1] AS [Name1], 
        [Join2].[Start] AS [Start], 
        [Join2].[End] AS [End], 
        [Join2].[CustomerId] AS [CustomerId], 
        [Join2].[Contact_Email] AS [Contact_Email1], 
        [Join2].[Contact_Phone] AS [Contact_Phone1], 
        [Join2].[Name2] AS [Name2], 
        CASE
  WHEN ([Join2].[ProjectResourceId] IS
  NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM 
  [dbo].[Resource] AS [Extent1]
        LEFT
  OUTER JOIN  (SELECT [Extent2].[ProjectResourceId]
  AS [ProjectResourceId], [Extent2].[Role] AS [Role], [Extent2].[Resource_ResourceId]
  AS [Resource_ResourceId], [Extent3].[ProjectId] AS [ProjectId], [Extent3].[Name] AS [Name1], [Extent3].[Start] AS [Start], [Extent3].[End] AS [End], [Extent4].[CustomerId] AS [CustomerId], [Extent4].[Contact_Email] AS [Contact_Email], [Extent4].[Contact_Phone] AS [Contact_Phone], [Extent4].[Name] AS [Name2]
              FROM  
  [dbo].[ProjectResource] AS
  [Extent2]
              INNER
  JOIN [dbo].[Project] AS [Extent3] ON [Extent2].[Project_ProjectId]
  = [Extent3].[ProjectId]
              INNER
  JOIN [dbo].[Customer] AS [Extent4] ON [Extent3].[Customer_CustomerId]
  = [Extent4].[CustomerId] ) AS [Join2] ON [Extent1].[ResourceId] = [Join2].[Resource_ResourceId]
  )  AS [Project1]
  ORDER BY [Project1].[ResourceId] ASC, [Project1].[C1] ASC

可以想象,EF 要同时获取所有需要获取的数据JOIN要经历大量的工作,因此产生了相当复杂的 SQL。

下面是使用ThenInclude的多级包含的最后一个例子:

代码清单 84

  //multilevel
  include paths
  var
  resourcesProjectResourcesCustomers = ctx
    .Resources
    .Include(x
  => x.ProjectResources)

  .ThenInclude(x => x.Role)
    .ToList();

ThenInclude只能在Include调用后使用,以强制在包含的路径上加载嵌套路径。

可以想象,EF 为了同时获取所有需要获取的数据而做了大量的工作JOIN,因此产生了相当复杂的 SQL。

在引用属性没有被急切加载的情况下,我们仍然可以强制它显式加载:

代码清单 85

  //explicitly
  load the Customer property.
  ctx.Entry(project).Reference(x => x.Customer).Load();

这同样适用于集合:

代码清单 86

  //see if the ProjectResources collection is loaded.
  var resourcesLoaded = ctx
    .Entry(project)
    .Collection(x => x.ProjectResources)
    .IsLoaded;

  if (resourcesLoaded == false)
  {
    //explicitly load the ProjectResources collection.

  ctx.Entry(project).Collection(x => x.ProjectResources).Load();
  }

另一个有趣的情况是,您希望通过过滤掉与给定条件不匹配的实体来仅加载某个集合的一部分,或者甚至计算其成员而不实际加载它们。使用 EF 可以做到这一点,为此,您可以发出如下查询:

代码清单 87

  //count an entity's collection entities without loading them.
  var countDevelopersInProject = ctx
    .Entry(project)
    .Collection(x => x.ProjectResources)
    .Query()
    .Where(x => x.Role == Role.Developer)
    .Count();

  //filter an entity’s
  collection without loading it.
  var developersInProject = ctx
    .Entry(project)
    .Collection(x => x.ProjectResources)
    .Query()
    .Where(x => x.Role == Role.Developer)
    .ToList();

您也可以强制加载并带来相关的引用或集合——注意调用Include:

代码清单 88

  //filter an entity’s
  collection without loading it.
  var developersInProject = ctx
    .Entry(project)

  .Collection(x => x.ProjectResources)
    .Query()
    .Include(x =>
  x.Resource)
    .ToList();

因此,延迟加载和急切加载的区别在于,使用延迟加载,您不需要做任何显式的事情——您只需要访问导航属性,甚至不用考虑它,而使用显式加载,您必须执行一些操作。

实体框架上下文所知的实体——无论是从中加载的还是标记为删除或插入的——都存储在所谓的本地或一级缓存中。马丁·福勒称之为身份地图,你可以在这里阅读更多关于概念。基本上,上下文跟踪所有这些实体,因此,每当执行返回它们相关记录的查询时,它不需要物化它们。可以通过ChangeTracker实例访问该缓存。两种扩展方法使它更容易:

代码清单 89

  public static class DbContextExtensions
  {
    public static IEnumerable<EntityEntry<T>> Local<T>(this DbContext context) 
      where T : class
    {
      return context.ChangeTracker.Entries<T>();
    }

    public static IEnumerable<EntityEntry<T>> Local<T>(this DbSet<T> set) 
      where T : class
    {
      if (set is InternalDbSet<T>)
      {
        var svcs = (set as InternalDbSet<T>)
          .GetInfrastructure()
          .GetService<IDbContextServices>();

        var ctx = svcs.CurrentContext.Context;

        return Local<T>(ctx);
      }

      throw new ArgumentException("Invalid set", "set");
    }
  }

  //local Projects.
  var projectsAlreadyLoaded = ctx.Projects.Local();

  //filtered local
  Projectsno need to call ToList.
  var projectsAlreadyLoadedBelongingToACustomer = projectsAlreadyLoaded
    .Where(x => x.Customer.Name == "Some Customer");

可以知道本地缓存中存在的所有实体,并查看它们的状态,如上下文所示。跟踪所有这些实体是ChangeTracker的责任。

代码清单 90

  //get the projects in local cache that have been modified.
  var modifiedProjects = ctx.ChangeTracker
    .Entries<Project>()
    .Where(x => x.State == EntityState.Modified)
    .Select(x => x.Entity);

正如您所猜测的,从本地缓存中获取实体比从数据库中加载实体要快得多。考虑到这一点,我们可以编写一个像下面这样的方法,用于透明地返回一个本地实体或者用 SQL 获取它。

代码清单 91

  //retrieve from cache or
  the database.
  public static IQueryable<T> LocalOrDatabase<T>(this DbContext context, 
        Expression<Func<T, bool>> expression) where T : class
  {
    var
  localResults = context
      .Set<T>()
      .Local()
      .Where(expression.Compile());

      if (localResults.Any() == true)
      {
        return localResults.AsQueryable();
      }

    return context.Set<T>().Where(expression);
  }

另一种有用的技术包括利用 LINQ 表达式从扩展方法构建复杂的查询。

之间的 SQL 运算符没有对应的 LINQ 表达式。我们可以在 LINQ 表达式中同时使用两个条件,一个用于范围的低端(> X),一个用于高端(< Y)。我们还可以实现一个 LINQ 扩展方法,用一个表达式为我们提供这个功能。

代码清单 92

public static class QueryableExtensions
{
  public static IQueryable<TSource> Between<TSource, TKey>(
    this IQueryable<TSource> source, 
    Expression<Func<TSource, TKey>> property, TKey low, TKey high
  ) where TKey : IComparable<TKey>
  {
    var sourceParameter = Expression.Parameter(typeof(TSource));
    var body = property.Body;
    var parameter = property.Parameters[0];
    var compareMethod = typeof(TKey).GetMethod("CompareTo", 
      new Type[] { typeof(TKey) });
    var zero = Expression.Constant(0, typeof(int));
    var upper = Expression.LessThanOrEqual(Expression.Call(body, compareMethod,
      Expression.Constant(high)), zero);
    var lower = Expression.GreaterThanOrEqual(Expression.Call(body, compareMethod, 
      Expression.Constant(low)), zero);
    var andExpression = Expression.AndAlso(upper, lower); 
    var whereCallExpression = Expression.Call
    (
      typeof(Queryable),
      "Where",
      new Type[] { source.ElementType },
      source.Expression,
      Expression.Lambda<Func<TSource, Boolean>>(andExpression, 
        new ParameterExpression[] { parameter })
    );

    return source.Provider.CreateQuery<TSource>(whereCallExpression);
  }
}

为了更好地理解这是如何实现的,理解 LINQ 表达式至关重要。网上有一些不错的链接。这项技术虽然掌握起来很复杂,但潜力巨大,引起了很多关注。

这是 IQueryable < T > 上的一个扩展方法,可以这样使用:

代码清单 93

//get projects starting between two dates.
var projectsBetweenTodayAndTheDayBefore = ctx
  .Projects
  .Between(x => x.Start, DateTime.Today.AddDays(-1), DateTime.Today)
  .ToList();

//projects with 10 to 20 resources.
var projectsWithTwoOrThreeResources = ctx
  .Projects
  .Select(x => new { x.Name, ResourceCount = x.ProjectResources.Count() })
  .Between(x => x.ResourceCount, 10, 20)
  .ToList();

LINQ 提供者将愉快地咀嚼新的表达式,并将其翻译成适当的 SQL。

DbContextDbSetT4 都实现了IInfrastructure<IServiceProvider>界面。这个方法公开了内部服务提供者,您可以从中获得对实体框架核心使用的所有服务的引用。