我们将在本文深入掌握 EF Core 的高级查询功能,通过详细解析 LINQ 查询、原生 SQL 查询,以及投影与选择三大核心主题,提升数据库操作的效率和灵活性。文章将首先回顾 LINQ 的基础和复杂查询构建方法,然后探讨如何在 EF Core 中安全有效地使用原生 SQL,最后介绍投影与选择技术,展示如何通过投影优化查询性能。通过这篇文章,我们将学会在实际项目中应用高级查询技巧,从而更好地管理和操作数据。

一、 LINQ 查询

1.1. 延迟加载与立即加载

在 EF Core 中,延迟加载(Lazy Loading)和立即加载(Eager Loading)是处理相关数据的两种主要方式,它们决定了如何加载与主实体相关的导航属性。

  1. 延迟加载(Lazy Loading)
    延迟加载是指在第一次访问导航属性时,EF Core 会自动从数据库加载相关数据。说明只有在代码显式访问这些属性时,才会触发数据库查询。这种方式可以减少初始查询的复杂性和数据量,但可能导致 N+1 查询问题。

    • 使用延迟加载的步骤

      • 启用延迟加载
        延迟加载需要在 EF Core 中显式启用,我们需要安装 Microsoft.EntityFrameworkCore.Proxies 包并通过配置上下文来实现,代码如下:

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLazyLoadingProxies();
        }
        
      • 配置虚拟导航属性
        要使延迟加载生效,必须配置虚拟导航属性,并且标记为 virtual,代码如下:

        public class Blog
        {
            public int BlogId { get; set; }
            public string Url { get; set; }
        
            public virtual ICollection<Post> Posts { get; set; }
        }
        
    • 延迟加载的优缺点

      • 优点
        • 初始查询简单,减少了不必要的数据加载。
        • 延迟加载的实现自动化,代码更加简洁。
      • 缺点
        • 可能导致多次数据库查询(N+1 问题),影响性能。
        • 在某些情况下,可能出现数据访问的延迟。
  2. 立即加载(Eager Loading)
    立即加载是在查询主实体时,通过 Include 方法预先加载所有相关数据。这样可以在一次查询中加载所有需要的数据,避免后续的数据库往返查询。

    • 使用立即加载的方法
      我们可以通过使用 EF Core 提供的 IncludeThenInclude 方法来实现立即加载,代码如下:

      var blogs = context.Blogs
                         .Include(b => b.Posts)
                         .ToList();
      

      如果有多个层次的导航属性,可以使用 ThenInclude 方法进一步加载相关数据:

      var blogs = context.Blogs
                         .Include(b => b.Posts)
                         .ThenInclude(p => p.Comments)
                         .ToList();
      
    • 立即加载的优缺点

      • 优点

        • 避免了 N+1 查询问题,减少数据库往返。
        • 在单次查询中获取所有相关数据,效率更高。
      • 缺点

        • 初始查询可能变得复杂且数据量大,影响性能。
        • 无法动态选择是否加载某些数据,缺乏灵活性。
  3. 如何选择加载方式

    • 延迟加载 适合在某些场景下,只有在访问相关数据时才需要加载,比如当导航属性访问频率较低时。
    • 立即加载 适合在需要一次性加载所有相关数据,避免多次查询时使用,尤其是在处理复杂关系时。
1.2. 复杂查询的构建
  1. 连接查询(Join)
    在 EF Core 中,连接查询(Join) 用于将多个表或实体的相关数据根据某些条件组合在一起。连接查询对于从不同的表中提取相关数据非常有用,特别是在处理多对多或一对多关系时。EF Core 支持使用 LINQ 的 Join 方法来执行连接查询。

    • Join 方法的基本语法
      连接查询使用 LINQ 的 Join 方法来关联两个或多个数据源,一般我们会使用数据源的键进行匹配,代码如下:

      var result = from t1 in context.Table1
                   join t2 in context.Table2
                   on t1.Key equals t2.Key
                   select new { t1.Property1, t2.Property2 };
      
    • 示例:内连接(Inner Join)
      内连接是最常见的连接类型,它只返回两个数据源中都有匹配记录的行。下面的代码中设定有两个实体 CustomerOrder,我们希望检索所有下了单的客户及其订单信息:

      var query = from c in context.Customers
                  join o in context.Orders
                  on c.CustomerId equals o.CustomerId
                  select new
                  {
                      CustomerName = c.Name,
                      OrderDate = o.OrderDate,
                      OrderTotal = o.Total
                  };
      

      内联除了使用 equals 来实现,还可以使用 Join 方法来实现,代码如下:

      var query = context.Customers.Join(context.Orders,
                                         c => c.CustomerId,
                                         o => o.CustomerId,
                                         (c, o) => new
                                         {
                                             CustomerName = c.Name,
                                             OrderDate = o.OrderDate,
                                             OrderTotal = o.Total
                                         });
      
    • 示例:左连接(Left Join)
      左连接返回左表中的所有记录,即使右表中没有匹配的记录。LINQ 中没有直接提供左连接的方法,但我们可以通过使用 GroupJoinDefaultIfEmpty 来实现。

      var query = from c in context.Customers
                  join o in context.Orders
                  on c.CustomerId equals o.CustomerId into orderGroup
                  from o in orderGroup.DefaultIfEmpty()
                  select new
                  {
                      CustomerName = c.Name,
                      OrderDate = o?.OrderDate, // 使用 ?. 来处理可能为空的情况
                      OrderTotal = o?.Total
                  };
      
    • 多个连接(Multiple Joins)
      有时我们需要连接多个表,那么可以通过链式连接语句实现多个表的连接,代码如下:

      var query = from o in context.Orders
                  join c in context.Customers on o.CustomerId equals c.CustomerId
                  join p in context.Products on o.ProductId equals p.ProductId
                  select new
                  {
                      CustomerName = c.Name,
                      ProductName = p.Name,
                      OrderDate = o.OrderDate,
                      OrderTotal = o.Total
                  };
      
    • 跨表聚合
      在连接的基础上,我们还可以执行聚合操作,比如计算每个客户的订单总额:

      var query = from c in context.Customers
                  join o in context.Orders on c.CustomerId equals o.CustomerId
                  group o by c.Name into customerGroup
                  select new
                  {
                      CustomerName = customerGroup.Key,
                      TotalOrders = customerGroup.Sum(o => o.Total)
                  };
      
  2. 分组查询(GroupBy)
    在 EF Core 中,分组查询是用于将数据按特定字段进行分组的强大工具。GroupBy 是 LINQ 中的一种常见操作符,通过将数据集按照指定的键分组,可以对每个组中的数据进行进一步的操作,比如求和、计数、平均等。

    • GroupBy 的基本语法
      在 LINQ 查询中,可以使用 GroupBy 操作符对数据进行分组。它通常与其他聚合操作符(如 SumCountAverage 等)结合使用,以对分组后的数据进行进一步处理。语法如下:

      var query = from item in context.Items
                  group item by item.Category into grouped
                  select new
                  {
                      Category = grouped.Key,
                      ItemCount = grouped.Count(),
                      TotalPrice = grouped.Sum(i => i.Price)
                  };
      

      同样,我们除了使用 group 关键字外,还可以使用 GroupBy 方法来实现,代码如下:

      var query = context.Items
                         .GroupBy(i => i.Category)
                         .Select(g => new
                         {
                             Category = g.Key,
                             ItemCount = g.Count(),
                             TotalPrice = g.Sum(i => i.Price)
                         });
      
    • GroupBy 和复杂的分组键
      有时分组可能需要使用多个字段作为分组键。例如可以按客户和月份同时进行分组:

      var query = from o in context.Orders
                  group o by new { o.CustomerId, Month = o.OrderDate.Month } into g
                  select new
                  {
                      g.Key.CustomerId,
                      g.Key.Month,
                      TotalOrderValue = g.Sum(o => o.TotalAmount)
                  };
      
  3. 聚合函数(Sum、Count、Average 等)
    在 EF Core 中,聚合函数用于对一组数据进行计算,并返回一个单一的值。这些函数通常在结合 GroupBy 进行分组查询时使用,但它们也可以在整个数据集上独立使用。常见的聚合函数包括 SumCountAverageMinMax。这些函数为统计、汇总、平均、最小值、最大值等操作提供了便捷的工具。下面我们一起来看看如何使用常见的聚合函数。

    • Sum - 求和
      Sum 函数用于计算指定列的总和。例如,如果我们有一个 Orders 表,需要计算所有订单的总金额:

      var totalSales = context.Orders.Sum(o => o.TotalAmount);
      

      我们也可以在分组查询中使用 Sum 来计算每个组的总和:

      var query = from o in context.Orders
                  group o by o.CustomerId into g
                  select new
                  {
                      CustomerId = g.Key,
                      TotalSales = g.Sum(o => o.TotalAmount)
                  };
      
    • Count - 计数
      Count 函数用于计算记录的数量。它可以用来统计表中总记录数或满足特定条件的记录数。例如,统计订单表中订单的总数:

      var totalOrders = context.Orders.Count();
      

      也可以统计每个客户的订单数:

      var query = from o in context.Orders
                  group o by o.CustomerId into g
                  select new
                  {
                      CustomerId = g.Key,
                      OrderCount = g.Count()
                  };
      
    • Average - 平均值
      Average 函数用于计算某列数值的平均值。例如,计算所有订单的平均金额:

      var averageOrderValue = context.Orders.Average(o => o.TotalAmount);
      

      也可以计算每个客户的平均订单金额:

      var query = from o in context.Orders
                  group o by o.CustomerId into g
                  select new
                  {
                      CustomerId = g.Key,
                      AverageOrderValue = g.Average(o => o.TotalAmount)
                  };
      
    • Min - 最小值
      Min 函数用于获取某列的最小值。例如,找到订单表中订单金额的最小值:

      var minOrderValue = context.Orders.Min(o => o.TotalAmount);
      

      同样,也可以找出每个客户的最低订单金额:

      var query = from o in context.Orders
                  group o by o.CustomerId into g
                  select new
                  {
                      CustomerId = g.Key,
                      MinOrderValue = g.Min(o => o.TotalAmount)
                  };
      
    • Max - 最大值
      Max 函数用于获取某列的最大值。例如,找到订单表中订单金额的最大值:

      var maxOrderValue = context.Orders.Max(o => o.TotalAmount);
      

      或者找到每个客户的最高订单金额:

      var query = from o in context.Orders
                  group o by o.CustomerId into g
                  select new
                  {
                      CustomerId = g.Key,
                      MaxOrderValue = g.Max(o => o.TotalAmount)
                  };
      
1.3. 动态查询
  1. 如何根据条件动态构建 LINQ 查询
    在实际开发中,常常需要根据用户输入或其他动态条件构建 LINQ 查询。在 EF Core 中,可以通过多种方式动态构建 LINQ 查询,以便灵活处理不同的查询条件。以下是一些常见的方式和技巧。
    • 使用 IQueryable 构建动态查询
      IQueryable 接口允许我们延迟执行查询,并在执行前动态添加条件。这样可以逐步构建查询,并根据条件进行过滤、排序等操作。
      假设我们有一个 Products 表,需要根据用户输入的多个条件(如类别、价格范围、名称等)动态构建查询,实现代码如下:

      public IQueryable<Product> GetProducts(string category, decimal? minPrice, decimal? maxPrice, string name)
      {
          var query = context.Products.AsQueryable();
          if (!string.IsNullOrEmpty(category))
          {
              query = query.Where(p => p.Category == category);
          }
          if (minPrice.HasValue)
          {
              query = query.Where(p => p.Price >= minPrice.Value);
          }
          if (maxPrice.HasValue)
          {
              query = query.Where(p => p.Price <= maxPrice.Value);
          }
          if (!string.IsNullOrEmpty(name))
          {
              query = query.Where(p => p.Name.Contains(name));
          }
          return query;
      }
      

      上面的代码中,我们根据每个输入参数的值动态构建查询,并将这些条件逐一添加到查询中。最终返回的 query 是一个 IQueryable<Product> 对象,只有在调用 ToList()FirstOrDefault() 等方法时,才会执行查询。

    • 使用 Expression 表达式树
      对于更复杂的动态查询,特别是当条件组合非常灵活时,可以使用 Expression 表达式树来构建查询。表达式树提供了构建 LINQ 查询的更强大的方式,允许我们在运行时动态生成查询逻辑。
      假设我们要动态生成一个查询,根据多个条件组合进行过滤:

      public IQueryable<Product> GetProducts(Expression<Func<Product, bool>> filter)
      {
          return context.Products.Where(filter);
      }
      // 动态生成表达式
      public Expression<Func<Product, bool>> BuildProductFilter(string category, decimal? minPrice, decimal? maxPrice)
      {
          Expression<Func<Product, bool>> filter = p => true;
          if (!string.IsNullOrEmpty(category))
          {
              filter = filter.And(p => p.Category == category);
          }
          if (minPrice.HasValue)
          {
              filter = filter.And(p => p.Price >= minPrice.Value);
          }
          if (maxPrice.HasValue)
          {
              filter = filter.And(p => p.Price <= maxPrice.Value);
          }
          return filter;
      }
      

      需要注意,And 是自定义的扩展方法,用于组合两个表达式,代码如下:

      public static class ExpressionExtensions
      {
          public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
          {
              var parameter = Expression.Parameter(typeof(T));
              var combined = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter).Visit(expr1.Body);
              combined = Expression.AndAlso(combined, new ReplaceExpressionVisitor(expr2.Parameters[0], parameter).Visit(expr2.Body));
              return Expression.Lambda<Func<T, bool>>(combined, parameter);
          }
      
          private class ReplaceExpressionVisitor : ExpressionVisitor
          {
              private readonly Expression _oldValue;
              private readonly Expression _newValue;
      
              public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
              {
                  _oldValue = oldValue;
                  _newValue = newValue;
              }
      
              public override Expression Visit(Expression node)
              {
                  if (node == _oldValue)
                      return _newValue;
                  return base.Visit(node);
              }
          }
      }
      
    • 使用 Dynamic LINQ 扩展库
      有时条件较为复杂,可以考虑使用 Dynamic LINQ 扩展库(System.Linq.Dynamic.Core),它允许使用字符串来动态构建查询条件:

      var query = context.Products
                        .Where("Category == @0 AND Price >= @1 AND Price <= @2", category, minPrice, maxPrice);
      

二、 原生 SQL 查询

2.1. 原生 SQL 查询的概述

在 EF Core 中,除了使用 LINQ 查询之外,还可以直接执行原生 SQL 查询。FromSqlRawFromSqlInterpolated 是 EF Core 提供的两种方法,用于在 LINQ 查询中直接执行 SQL 语句。这两者主要用于需要手动编写复杂 SQL 查询或调用存储过程的场景。

  1. FromSqlRaw
    FromSqlRaw 方法用于执行原生 SQL 查询,允许我们以字符串的形式传递 SQL 语句。这个方法不会对传入的 SQL 语句进行参数化,因此我们需要手动处理参数,以防止 SQL 注入。
    假设我们有一个 Products 表,想要通过 SQL 查询获取所有产品:

    var products = context.Products
                          .FromSqlRaw("SELECT * FROM Products")
                          .ToList();
    

    如果我们的查询需要参数,可以使用 SqlParameter 来传递参数。比如,查询价格大于指定值的所有产品:

    var minPrice = 100m;
    var products = context.Products
                          .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
                          .ToList();
    

    在上面的代码中,{0} 是一个占位符,minPrice 的值将被安全地替换进去。

  2. FromSqlInterpolated
    FromSqlInterpolated 方法类似于 FromSqlRaw,但它支持字符串插值(interpolation),使得传递参数更加方便、安全。这个方法自动处理参数化,防止 SQL 注入。使用 FromSqlInterpolated 可以直接在字符串中插入变量:

    var minPrice = 100m;
    var products = context.Products
                          .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
                          .ToList();
    

    代码中的 $"{minPrice}" 是字符串插值,EF Core 会自动将其转换为参数化查询。

  3. 比较 FromSqlRawFromSqlInterpolated

    • 参数化处理FromSqlRaw 需要手动处理参数,使用 {0}, {1} 等占位符;而 FromSqlInterpolated 则直接通过字符串插值进行参数化处理。
    • SQL 注入防护FromSqlInterpolated 自动进行参数化,防止 SQL 注入,因此在多数情况下,它更安全。使用 FromSqlRaw 时,开发者需要格外小心,避免直接拼接 SQL 字符串。
  4. 处理复杂查询和存储过程
    有时,我们可能需要执行更复杂的查询或调用存储过程,这两种方法都可以处理。假设我们有一个存储过程 GetProductsByCategory,它接受一个类别名称作为参数并返回该类别的所有产品:

    var category = "Electronics";
    var products = context.Products
                          .FromSqlInterpolated($"EXEC GetProductsByCategory {category}")
                          .ToList();
    

    使用 FromSqlRaw 也可以达到同样效果:

    var category = "Electronics";
    var products = context.Products
                          .FromSqlRaw("EXEC GetProductsByCategory {0}", category)
                          .ToList();
    

Tip:无论使用 FromSqlRaw 还是 FromSqlInterpolated,EF Core 都会将 SQL 查询的结果映射到相应的实体类中。这意味着查询返回的列名必须与实体类中的属性名匹配,或者我们需要使用映射配置。

2.2. 混合使用 LINQ 和 SQL

在 EF Core 中,虽然大多数查询可以通过 LINQ 来表达,但有时我们可能需要在 LINQ 查询中直接插入原生 SQL 片段来处理一些复杂或特定的查询需求。EF Core 提供了一些方法来在 LINQ 查询中使用 SQL 片段。

  1. 使用 EF.Functions 与 SQL 片段
    EF.Functions 是 EF Core 提供的一个对象,允许我们在 LINQ 查询中使用数据库特定的 SQL 函数。我们可以在查询中使用一些常见的 SQL 函数(例如 LIKECONTAINSDATEPART),也可以使用自定义的 SQL 片段。
    假设我们有一个产品表,想要查找名称包含某个特定模式的所有产品,类似于 SQL 中的 LIKE

    var products = context.Products
                          .Where(p => EF.Functions.Like(p.Name, "%electronics%"))
                          .ToList();
    

    在上面的代码中 EF.Functions.Like 直接在 LINQ 查询中生成 LIKE SQL 片段。

  2. 使用 FromSqlRawFromSqlInterpolated 在查询中混合使用 SQL
    FromSqlRawFromSqlInterpolated 通常用于完全基于 SQL 的查询,但我们也可以将它们与 LINQ 查询结合起来。假我们想通过 SQL 片段从数据库中获取数据,然后在其上进一步应用 LINQ 查询:

    var sql = "SELECT * FROM Products WHERE Price > {0}";
    var minPrice = 100m;
    
    var products = context.Products
                          .FromSqlRaw(sql, minPrice)
                          .Where(p => p.Category == "Electronics")
                          .ToList();
    

    在这个例子中,FromSqlRaw 先执行了 SQL 查询,然后通过 LINQ 进一步过滤结果。

  3. 使用 EF.Functions.ExecuteSqlInterpolatedExecuteSqlRaw 执行 SQL 片段
    EF Core 提供了执行 SQL 命令的功能,我们可以在这些命令中包含 SQL 片段。假设我们需要在查询中更新某些记录,使用 SQL 片段和 LINQ 可以实现这一点:

    var category = "Electronics";
    var newPrice = 200m;
    
    context.Database.ExecuteSqlInterpolated(
        $"UPDATE Products SET Price = {newPrice} WHERE Category = {category}");
    
  4. 使用 ToQueryString 查看生成的 SQL 查询
    在调试时,我们可能需要查看 EF Core 如何将 LINQ 转换为 SQL 语句。ToQueryString 可以显示生成的 SQL 查询,代码如下:

    var query = context.Products
                      .Where(p => EF.Functions.Like(p.Name, "%electronics%"));
    
    var sql = query.ToQueryString();
    Console.WriteLine(sql);
    

    这个方法会输出 LINQ 查询对应的 SQL 语句,帮助我们更好地理解和优化查询。

2.3. 存储过程与视图
  1. 在 EF Core 中调用存储过程
    在 EF Core 中调用存储过程是一种执行复杂查询或数据操作的常见方法,特别是在需要进行多表联接、大量数据处理或需要数据库级优化的情况下。EF Core 提供了几种调用存储过程的方式,具体取决于是否需要返回结果集或执行非查询操作。

    • 返回结果集的存储过程
      当存储过程返回结果集时,通常需要将结果映射到实体或非实体类。可以使用 FromSqlRawFromSqlInterpolated 方法来调用存储过程。假设我们有一个 GetProductsByCategory 的存储过程,它根据类别返回产品列表。代码如下:
      • 存储过程定义示例:

        CREATE PROCEDURE GetProductsByCategory
            @Category NVARCHAR(50)
        AS
        BEGIN
            SELECT * FROM Products WHERE Category = @Category
        END
        
      • 在 EF Core 中调用:

        var category = "Electronics";
        var products = context.Products
                              .FromSqlInterpolated($"EXEC GetProductsByCategory {category}")
                              .ToList();
        

        在代码中,存储过程返回的数据会被映射到 Product 实体,并且可以像普通的 LINQ 查询结果一样操作。
        如果存储过程返回的数据不完全匹配 EF Core 的实体类,我们可以将结果映射到一个自定义的 DTO(数据传输对象)类。

      • 定义 DTO:

        public class ProductDto
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public decimal Price { get; set; }
        }
        
      • 调用存储过程并映射到 DTO:

        var category = "Electronics";
        var products = context.Set<ProductDto>()
                              .FromSqlInterpolated($"EXEC GetProductsByCategory {category}")
                              .ToList();
        

        这里使用 context.Set<ProductDto>() 创建一个查询上下文,EF Core 会将存储过程的结果映射到 ProductDto 类。

  2. 执行非查询存储过程
    有时我们可能需要调用一个不返回结果集的存储过程,例如执行插入、更新或删除操作。这种情况下,使用 ExecuteSqlRawExecuteSqlInterpolated 来执行存储过程。假设我们有一个存储过程 UpdateProductPrice,它更新指定产品的价格。

    • 存储过程定义:

      CREATE PROCEDURE UpdateProductPrice
          @ProductId INT,
          @NewPrice DECIMAL(18, 2)
      AS
      BEGIN
          UPDATE Products SET Price = @NewPrice WHERE Id = @ProductId
      END
      

      在 EF Core 中调用:

      var productId = 1;
      var newPrice = 19.99m;
      
      context.Database.ExecuteSqlInterpolated($"EXEC UpdateProductPrice {productId}, {newPrice}");
      

      在这个例子中,ExecuteSqlInterpolated 用于执行存储过程,该存储过程不会返回任何结果集。

  3. 处理输出参数的存储过程
    EF Core 也支持处理带有输出参数的存储过程。我们可以使用 SqlParameter 来定义和读取输出参数。假设我们有一个存储过程 GetProductStock,它返回某产品的库存数量。
    存储过程定义:

    CREATE PROCEDURE GetProductStock
        @ProductId INT,
        @StockCount INT OUTPUT
    AS
    BEGIN
        SELECT @StockCount = Stock FROM Products WHERE Id = @ProductId
    END
    

    在 EF Core 中调用并处理输出参数:

    var productId = 1;
    var stockCountParam = new SqlParameter
    {
        ParameterName = "@StockCount",
        SqlDbType = SqlDbType.Int,
        Direction = ParameterDirection.Output
    };
    
    context.Database.ExecuteSqlRaw(
        "EXEC GetProductStock @ProductId, @StockCount OUTPUT",
        new SqlParameter("@ProductId", productId),
        stockCountParam
    );
    
    int stockCount = (int)stockCountParam.Value;
    Console.WriteLine($"Product Stock: {stockCount}");
    

    在这个例子中,stockCountParam 被定义为输出参数,通过存储过程调用后,读取其 Value 属性即可获取结果。

  4. 使用 SQL Server 的 TVP(表值参数)
    对于 SQL Server,存储过程可以使用表值参数(TVP)来批量传递数据。EF Core 支持传递 TVP 作为参数。假设我们有一个存储过程 InsertProducts,接受一个 TVP 来批量插入产品。
    存储过程和 TVP 定义:

    CREATE TYPE ProductTableType AS TABLE
    (
        Name NVARCHAR(50),
        Price DECIMAL(18, 2)
    )
    
    CREATE PROCEDURE InsertProducts
        @ProductList ProductTableType READONLY
    AS
    BEGIN
        INSERT INTO Products (Name, Price)
        SELECT Name, Price FROM @ProductList
    END
    

    在 EF Core 中调用:

    var products = new List<SqlDataRecord>
    {
        new SqlDataRecord(new SqlMetaData("Name", SqlDbType.NVarChar, 50), 
                          new SqlMetaData("Price", SqlDbType.Decimal, 18, 2))
        {
            SetString(0, "Product1"),
            SetDecimal(1, 9.99m)
        },
        // 添加更多产品
    };
    
    var productListParam = new SqlParameter
    {
        ParameterName = "@ProductList",
        SqlDbType = SqlDbType.Structured,
        TypeName = "ProductTableType",
        Value = products
    };
    
    context.Database.ExecuteSqlRaw("EXEC InsertProducts @ProductList", productListParam);
    

    在这个例子中,EF Core 使用 SqlDataRecord 构建数据集,通过 SqlParameter 将 TVP 传递给存储过程。

  5. 使用数据库视图进行查询
    在 EF Core 中,数据库视图(Views)是查询复杂数据或预先计算和存储结果的强大工具。与表类似,视图在数据库中存储为虚拟表,可以包含来自一个或多个表的列,并且可以包括聚合、联接和其他复杂的查询逻辑。EF Core 提供了对数据库视图的良好支持,使得我们可以像查询普通实体一样查询视图。
    在数据库中,我们首先需要创建一个视图。以下是一个简单的视图示例,它联接了 ProductsCategories 表,并返回每个产品及其类别的名称。

    CREATE VIEW ProductCategoryView AS
    SELECT p.Id AS ProductId, p.Name AS ProductName, c.Name AS CategoryName, p.Price
    FROM Products p
    JOIN Categories c ON p.CategoryId = c.Id
    

    这个视图 ProductCategoryView 将包含产品 ID、产品名称、类别名称和价格。
    要在 EF Core 中使用视图,我们可以将视图映射到一个类(通常是只读的,因为视图不支持直接插入、更新或删除操作)。

    public class ProductCategoryView
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public string CategoryName { get; set; }
        public decimal Price { get; set; }
    }
    

    这个类与数据库视图的列一一对应。EF Core 将使用此类来映射视图的查询结果。
    在我们的 DbContext 类中,使用 OnModelCreating 方法来配置视图的映射。视图不需要主键,但是为了在 EF Core 中正常工作,需要通过 HasNoKey 来声明这一点。

    public class ApplicationDbContext : DbContext
    {
        public DbSet<ProductCategoryView> ProductCategoryViews { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ProductCategoryView>()
                        .HasNoKey()
                        .ToView("ProductCategoryView"); // 指定视图的名称
    
            base.OnModelCreating(modelBuilder);
        }
    }
    

    在这个配置中,ToView("ProductCategoryView") 告诉 EF Core,这个实体类对应的是一个数据库视图,而不是一个表。
    配置完成后,我们可以像查询普通实体一样查询视图的数据。由于视图通常是只读的,所以只能执行 Select 操作。

    var productCategories = context.ProductCategoryViews
    							.Where(p => p.Price > 100)
    							.ToList();
    

    这个查询将返回所有价格高于 100 的产品及其类别名称。
    虽然视图本身已经封装了复杂的查询逻辑,但我们仍然可以在 EF Core 中对视图进行进一步的 LINQ 操作。例如,我们可以对视图进行分组、排序或应用其他过滤条件。

    var groupedByCategory = context.ProductCategoryViews
    							.GroupBy(p => p.CategoryName)
    							.Select(g => new
    							{
    								Category = g.Key,
    								Products = g.ToList()
    							})
    							.ToList();
    

    在这个例子中,视图的结果按类别分组,并返回每个类别下的所有产品。

    使用视图的一大好处是,视图的查询逻辑通常由数据库优化器处理,并且视图可以预先编译和缓存。这使得视图在执行复杂的联接和聚合查询时通常比直接在应用程序中编写复杂 LINQ 查询更快。

    • 只读限制: 在大多数情况下,视图是只读的。EF Core 不支持通过视图直接进行插入、更新或删除操作。如果需要修改数据,必须针对基础表进行操作。
    • 维护复杂性: 视图的逻辑变化需要在数据库中手动更新,这增加了维护的复杂性,尤其是在开发和数据库管理之间的协作时。

三、 投影与选择

3.1. 投影的概念
  1. 什么是投影,为什么使用投影
    投影是指在查询中选择特定的字段或计算结果,而不是返回完整的对象或记录。投影允许我们从数据库中检索一部分数据,而不是整个实体,从而提高查询效率并减少传输的数据量。在编程中,投影通常与 SELECT 语句相关联,可以理解为选择我们感兴趣的列或根据这些列生成新的数据结构。

  2. 为什么使用投影

    • 性能优化:

      • 减少传输的数据量: 通过仅选择必要的字段,投影减少了查询中从数据库传输到应用程序的数据量。这对于处理大数据集时特别有用,因为可以避免不必要的网络传输和内存消耗。
      • 提高查询速度: 选择较少的列通常意味着数据库可以更快地处理和返回结果,尤其是在涉及复杂联接或子查询时。
    • 避免不必要的数据加载:

      • 加载我们需要的内容: 在很多情况下,我们并不需要实体的所有字段。使用投影,我们可以只加载那些在当前操作中真正需要的数据,避免加载和存储无用的数据。
      • 减少内存消耗: 在大数据集或嵌套关系复杂的情况下,加载完整的实体会占用大量内存,而投影可以有效地减少内存占用。
    • 简化数据模型:

      • 创建更简单的数据结构: 投影允许我们从实体或表中提取部分数据并将其投影到一个更简单的模型中,如匿名对象或数据传输对象(DTO)。这对于简化复杂的数据结构特别有用。
      • 聚合和计算: 我们可以在投影中应用计算或聚合函数,从而直接在查询中生成所需的结果,而不是在检索后再进行处理。
  3. 常用的投影操作(SelectSelectMany
    在 EF Core 中,投影操作是查询的一部分,用于选择并构造结果集中的数据。这些操作主要包括 SelectSelectMany,它们在 LINQ 查询中非常常用。下面介绍这两个投影操作的用法及其应用场景。

    • Select
      Select 是最常用的投影操作符,用于将每个源元素投影到新的形式。它可以选择源元素中的一个或多个字段,也可以对这些字段进行计算或转换,最后返回一个新的对象或匿名类型。
      简单投影
    var productNames = context.Products
    						.Select(p => p.Name)
    						.ToList();
    

    这个查询只返回产品的名称,忽略了其他属性。
    投影到匿名类型

    var productSummaries = context.Products
    							.Select(p => new 
    							{ 
    								p.Name, 
    								p.Price 
    							})
    							.ToList();
    

    这个查询将每个产品的 NamePrice 投影到一个匿名类型中。
    使用计算的投影

    var orderSummaries = context.Orders
    							.Select(o => new 
    							{ 
    								o.OrderId, 
    								TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice) 
    							})
    							.ToList();
    

    这个查询为每个订单计算总金额,并将其投影到一个匿名类型中。

    • SelectMany
      SelectMany 是一个更复杂的投影操作符,用于将多个集合的元素展平并返回一个单一的集合。它通常用于处理嵌套集合或复杂的数据结构,特别是在查询中需要展开集合或处理一对多关系时。
      展开嵌套集合
      假设我们有一个 Customer 实体,其中包含一个 Orders 集合,SelectMany 可以将这些订单展平为一个集合。
    var allOrders = context.Customers
    					.SelectMany(c => c.Orders)
    					.ToList();
    

    这个查询返回所有客户的订单,作为一个平铺的集合。
    组合多个集合
    我们可以使用 SelectMany 将多个集合组合在一起。例如,获取每个客户的订单并将客户信息与订单信息合并。

    var customerOrders = context.Customers
    							.SelectMany(c => c.Orders, (customer, order) => new 
    							{ 
    								customer.Name, 
    								order.OrderId, 
    								order.OrderDate 
    							})
    							.ToList();
    

    这里,SelectMany 不仅展平了订单集合,还将每个订单与其所属客户的信息合并到一个匿名类型中。

    • SelectSelectMany 的区别
      • Select:用于对每个元素进行投影,可以选择和变换数据,但保留集合的层次结构。如果我们对一个集合的集合进行 Select 操作,我们仍然会得到一个嵌套的集合。
      • SelectMany:用于将嵌套的集合展平,将多个集合的元素合并为一个单一的集合,打破了集合的层次结构。
3.2. 匿名类型与 DTO
  1. 使用匿名类型进行投影
    使用匿名类型进行投影是 LINQ 查询中的一种常见技巧,特别是在只需要部分字段或希望创建一个新的、轻量级的数据结构时。匿名类型允许我们在不显式定义类的情况下,动态创建对象,并在查询中投影所需的数据。

    • 什么是匿名类型
      匿名类型是一种没有显式命名的类型,由编译器自动生成。它的属性在定义时被指定,但类型名称不公开。匿名类型通常用于临时数据结构,特别适合在查询中投影多个字段,而不需要为这些字段创建单独的类。

    • 使用匿名类型进行投影的好处

      • 简洁性: 匿名类型减少了代码的样板,使得查询更加简洁明了。
      • 临时数据结构: 当我们只需要在查询结果中使用一些特定字段,且这些字段不会在其他地方复用时,匿名类型非常方便。
      • 编译时安全性: 尽管匿名类型没有显式名称,但它们的属性是强类型的,编译器会确保类型安全。
    • 使用匿名类型进行投
      简单字段投影
      假设我们有一个 Product 实体类,我们只需要产品的名称和价格:

      var productSummaries = context.Products
      							.Select(p => new 
      							{ 
      								p.Name, 
      								p.Price 
      							})
      							.ToList();
      

      这个查询将 NamePrice 投影到一个匿名类型中,返回的结果是一个包含这两个属性的对象集合。
      结合多个实体的字段
      我们可以使用匿名类型将多个实体的字段组合在一起。例如,获取每个订单的订单 ID 和客户的名字:

      var orderDetails = context.Orders
      						.Select(o => new 
      						{ 
      							o.OrderId, 
      							CustomerName = o.Customer.Name, 
      							o.OrderDate 
      						})
      						.ToList();
      

      在这个查询中,OrderIdOrderDate 来自 Orders 实体,而 CustomerName 来自关联的 Customer 实体。结果是一个包含这些字段的匿名类型对象集合。
      嵌套匿名类型
      我们也可以在匿名类型中嵌套另一个匿名类型。例如,如果我们希望包含产品信息作为嵌套对象:

      var orderSummaries = context.Orders
      							.Select(o => new 
      							{ 
      								o.OrderId, 
      								o.OrderDate,
      								Products = o.OrderItems.Select(oi => new 
      								{ 
      									oi.Product.Name, 
      									oi.Quantity, 
      									oi.UnitPrice 
      								})
      							})
      							.ToList();
      

      这里,Products 是一个嵌套的匿名类型集合,每个订单中包含一个由 NameQuantityUnitPrice 组成的产品信息对象列表。

    • 注意事项

      • 匿名类型的只读性: 匿名类型的属性是只读的,一旦创建无法修改。如果我们需要修改数据,请考虑使用 DTO 或实体类。
      • 作用域: 由于匿名类型没有显式的类定义,它们的作用域通常只限于方法内部。如果我们需要跨方法传递数据,最好定义一个明确的类型(如 DTO)。
      • 不支持序列化: 匿名类型无法序列化,因此不适用于需要序列化的场景,如 Web API 的返回值。
  2. 使用 Data Transfer Objects (DTO) 进行复杂数据传输
    使用数据传输对象(Data Transfer Objects,简称 DTO)进行复杂数据传输是设计和开发中常见的模式,特别是在需要从数据库中提取和处理数据并将其传递到前端或服务端的场景中。DTO 是一种轻量级的类,用于将数据从一层传递到另一层,通常用于简化数据结构,减少不必要的负载,以及提高数据传输的安全性。

    • 什么是 DTO
      DTO 是一个仅包含数据的类,通常没有业务逻辑。它的主要作用是将数据从一个地方传输到另一个地方,确保数据的结构明确且只包含需要的字段。DTO 可以包含多个实体或字段的组合,甚至包括经过计算的属性。

    • 为什么使用 DTO

      • 分离关注点: DTO 帮助分离数据模型和业务逻辑。通过 DTO,我们可以将数据库实体模型与 API 或服务层中的数据模型解耦,避免直接暴露数据库结构。
      • 优化数据传输: 通过只传输必要的数据,DTO 可以减少带宽和处理时间,特别是在需要传输大量数据或复杂结构的情况下。
      • 提高安全性: 通过 DTO,我们可以控制传递给客户端的数据,避免敏感信息的泄漏,并确保客户端只接收到它们需要的数据。
      • 易于扩展和维护: DTO 可以为不同的用例定制,允许我们灵活地添加、修改或删除数据字段,而不会影响数据库或其他业务逻辑。
    • 使用 DTO 进行复杂数据传输的示例
      假设我们有一个订单系统,需要传输客户信息和其订单的详细信息。我们可以定义一个 CustomerOrderDto 来封装这些信息。

      public class CustomerOrderDto
      {
      	public int CustomerId { get; set; }
      	public string CustomerName { get; set; }
      	public DateTime OrderDate { get; set; }
      	public List<OrderItemDto> OrderItems { get; set; }
      }
      
      public class OrderItemDto
      {
      	public int ProductId { get; set; }
      	public string ProductName { get; set; }
      	public int Quantity { get; set; }
      	public decimal UnitPrice { get; set; }
      }
      

      这里,CustomerOrderDto 包含了客户信息以及客户的订单项列表,每个订单项由 OrderItemDto 表示。

      接下来,我们可以使用 LINQ 查询将数据库实体投影到 DTO:

      var customerOrders = context.Orders
      							.Where(o => o.CustomerId == customerId)
      							.Select(o => new CustomerOrderDto
      							{
      								CustomerId = o.Customer.Id,
      								CustomerName = o.Customer.Name,
      								OrderDate = o.OrderDate,
      								OrderItems = o.OrderItems.Select(oi => new OrderItemDto
      								{
      									ProductId = oi.Product.Id,
      									ProductName = oi.Product.Name,
      									Quantity = oi.Quantity,
      									UnitPrice = oi.UnitPrice
      								}).ToList()
      							})
      							.ToList();
      

      这个查询从 Orders 表中获取数据,将其投影到 CustomerOrderDto 中,并且包含了嵌套的 OrderItemDto 列表。每个订单的详细信息都被封装在 DTO 中,使得它们可以被安全地传递到前端或其他服务。

      在复杂的业务场景中,DTO 可以组合来自多个实体的字段,或包括计算得出的字段。例如,如果我们需要传递订单的总金额,可以将其包含在 DTO 中:

      public class CustomerOrderWithTotalDto : CustomerOrderDto
      {
      	public decimal TotalAmount { get; set; }
      }
      
      var customerOrdersWithTotal = context.Orders
      									.Where(o => o.CustomerId == customerId)
      									.Select(o => new CustomerOrderWithTotalDto
      									{
      										CustomerId = o.Customer.Id,
      										CustomerName = o.Customer.Name,
      										OrderDate = o.OrderDate,
      										TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),
      										OrderItems = o.OrderItems.Select(oi => new OrderItemDto
      										{
      											ProductId = oi.Product.Id,
      											ProductName = oi.Product.Name,
      											Quantity = oi.Quantity,
      											UnitPrice = oi.UnitPrice
      										}).ToList()
      									})
      									.ToList();
      

      在这个示例中,CustomerOrderWithTotalDto 继承自 CustomerOrderDto,并添加了一个 TotalAmount 字段,用于存储订单的总金额。这样,我们可以在查询时直接计算并传输这些数据。

      • 注意事项
        • 保持 DTO 简单: DTO 主要是为了传输数据,不应包含复杂的业务逻辑。保持它们的结构简单,有助于维护代码的清晰和易于理解。
        • 避免 DTO 与实体耦合: 尽量避免 DTO 与数据库实体过度耦合,否则会使我们的数据传输逻辑紧密依赖于数据库结构的变化。
        • 考虑序列化: 如果 DTO 需要在网络上传输,如通过 Web API 传递到前端,确保它们是可序列化的,通常这意味着它们必须是纯粹的数据容器。
3.3. 子查询与嵌套查询**
  1. LINQ 中的子查询实现
    在 LINQ 中,子查询是一种在查询表达式中嵌套另一查询的方式,用于在主查询中执行额外的查询操作。子查询可以用于过滤、投影、计算或生成动态集合。LINQ 中的子查询可以通过各种方式实现,例如在 SelectWhereAnyAllContains 等操作符中嵌套其他查询。

    • 子查询的常见用法

      • Where 子句中使用子查询
        假设我们有一个 Order 实体和一个 Customer 实体,我们希望查询那些有特定产品订单的客户:

        var customersWithSpecificProductOrders = context.Customers
        	.Where(c => context.Orders
        		.Any(o => o.CustomerId == c.Id && o.OrderItems
        			.Any(oi => oi.ProductId == specificProductId)))
        	.ToList();
        

        这个查询在 Where 子句中使用了子查询,首先查找包含特定产品的订单,然后根据这些订单来筛选客户。

      • Select 子句中使用子查询
        我们可以在 Select 子句中使用子查询来计算相关数据。例如,获取每个客户及其订单的数量:

        var customerOrderCounts = context.Customers
        	.Select(c => new 
        	{
        		CustomerName = c.Name,
        		OrderCount = context.Orders.Count(o => o.CustomerId == c.Id)
        	})
        	.ToList();
        

        这里,子查询用于计算每个客户的订单数,并将其作为结果的一部分返回。

      • 使用子查询进行投影
        假设我们需要获取每个订单的详情,并在每个订单中包含其所属客户的名称:

        var orderDetails = context.Orders
        	.Select(o => new 
        	{
        		o.OrderId,
        		o.OrderDate,
        		CustomerName = context.Customers
        			.Where(c => c.Id == o.CustomerId)
        			.Select(c => c.Name)
        			.FirstOrDefault()
        	})
        	.ToList();
        

        在这个例子中,子查询在 Select 子句中用于检索每个订单相关的客户名称。

      • 使用 Join 和子查询结合
        有时,我们可能需要将子查询与 Join 结合使用。例如,查找每个客户的订单总金额:

        var customerOrderTotals = context.Customers
        	.Join(context.Orders,
        		c => c.Id,
        		o => o.CustomerId,
        		(c, o) => new 
        		{
        			CustomerName = c.Name,
        			TotalAmount = context.Orders
        				.Where(order => order.CustomerId == c.Id)
        				.Sum(order => order.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice))
        		})
        	.ToList();
        

        在这个查询中,Join 用于将客户与订单连接,而子查询用于计算每个客户的订单总金额。

    • 注意事项

      • 性能考虑: 子查询可能会导致性能下降,特别是在大型数据集或复杂查询中。如果可能,应考虑通过优化索引或重构查询来提高效率。
      • 嵌套查询深度: 虽然子查询很强大,但嵌套查询过多会使代码复杂且难以维护。保持查询简单明了,有助于代码的可读性和性能。
      • 转换为 SQL 的注意点: 在使用 LINQ 子查询时,注意其转换为 SQL 语句的方式。某些 LINQ 子查询可能会生成复杂的 SQL 语句,导致性能问题。应检查生成的 SQL,并根据需要进行优化。
  2. 层次投影的实现技巧
    在 LINQ 查询中,层次投影(Hierarchical Projection)是一种将数据结构映射到具有层次结构的对象模型中的技术。通常用于处理和投影具有父子关系或嵌套结构的数据。层次投影非常适用于需要从数据库中提取树形结构、嵌套列表或具有复杂关系的实体数据的场景。

    • 层次投影的常见应用场景

      • 父子关系:例如,订单与订单项、类别与子类别、员工与其下属等。
      • 树形结构:例如,组织结构图、目录树、评论系统等。
      • 嵌套列表:例如,博客文章及其嵌套评论、论坛帖子及其回复等。
    • 层次投影的实现技巧

      • 父子关系的层次投影
        假设我们有一个 Category 实体,每个类别都有一个可能为空的 ParentCategoryId 来表示父子关系。我们想投影出每个类别及其子类别的层次结构。

        var categories = context.Categories
        	.Where(c => c.ParentCategoryId == null)
        	.Select(c => new CategoryDto
        	{
        		CategoryId = c.Id,
        		CategoryName = c.Name,
        		SubCategories = context.Categories
        			.Where(sc => sc.ParentCategoryId == c.Id)
        			.Select(sc => new SubCategoryDto
        			{
        				SubCategoryId = sc.Id,
        				SubCategoryName = sc.Name
        			})
        			.ToList()
        	})
        	.ToList();
        

        在这个查询中,CategoryDto 是用于表示父类别的 DTO,而 SubCategoryDto 表示子类别。通过在查询中嵌套 Select 子查询,我们可以将父类别和子类别的数据结构映射到层次结构的对象模型中。

      • 树形结构的层次投影
        假设我们有一个 Employee 实体,每个员工可能有一个 ManagerId 来表示层次关系。我们想构建一个包含员工及其直接下属的树形结构。

        var employees = context.Employees
        	.Where(e => e.ManagerId == null)
        	.Select(e => new EmployeeDto
        	{
        		EmployeeId = e.Id,
        		EmployeeName = e.Name,
        		Subordinates = GetSubordinates(e.Id)
        	})
        	.ToList();
        
        private List<EmployeeDto> GetSubordinates(int managerId)
        {
        	return context.Employees
        		.Where(e => e.ManagerId == managerId)
        		.Select(e => new EmployeeDto
        		{
        			EmployeeId = e.Id,
        			EmployeeName = e.Name,
        			Subordinates = GetSubordinates(e.Id)
        		})
        		.ToList();
        }
        

        这里,通过递归调用 GetSubordinates 方法,我们可以构建员工及其下属的树形结构。每个 EmployeeDto 对象不仅包含员工的基本信息,还包含一个嵌套的 Subordinates 列表,表示该员工的直接下属。

      • 嵌套评论的层次投影
        假设我们有一个 Comment 实体,每个评论可能有一个 ParentCommentId,表示嵌套的评论结构。我们想提取并构建一个帖子及其所有评论的层次结构。

        var postWithComments = context.Posts
        	.Where(p => p.Id == postId)
        	.Select(p => new PostDto
        	{
        		PostId = p.Id,
        		Title = p.Title,
        		Comments = GetComments(p.Id, null)
        	})
        	.FirstOrDefault();
        
        private List<CommentDto> GetComments(int postId, int? parentCommentId)
        {
        	return context.Comments
        		.Where(c => c.PostId == postId && c.ParentCommentId == parentCommentId)
        		.Select(c => new CommentDto
        		{
        			CommentId = c.Id,
        			Content = c.Content,
        			SubComments = GetComments(postId, c.Id)
        		})
        		.ToList();
        }
        

        在这个示例中,GetComments 方法递归获取所有子评论,并将其作为 SubComments 属性嵌套在 CommentDto 中,从而实现嵌套评论的层次投影。

    • 注意事项

      • 性能考虑: 层次投影,尤其是递归查询,可能会导致性能问题。对于深度嵌套的层次结构,递归查询可能产生大量数据库访问,应考虑使用 AsNoTracking 来避免不必要的跟踪开销,或通过预加载数据进行优化。
      • 避免循环引用: 在构建层次结构时,确保我们的模型中没有循环引用,否则可能会导致无限递归或内存溢出。
      • DTO 设计: 在层次投影中,DTO 的设计非常重要。确保 DTO 的结构足够灵活,以支持嵌套和层次关系,同时又不至于过于复杂难以维护。
3.4. EF Core 投影优化
  1. 如何避免查询过多数据
    在使用 LINQ 查询时,避免查询过多的数据是优化性能和提高应用程序效率的重要步骤。以下是一些技巧和策略,可以帮助我们在使用 LINQ 查询时减少不必要的数据传输和处理:

    • 使用 Select 只查询所需的列
      在 LINQ 查询中,如果只需要部分字段,使用 Select 操作符只提取这些字段,而不是整个实体。这可以显著减少传输的数据量。
      var customerNames = context.Customers
      	.Select(c => new { c.Id, c.Name })
      	.ToList();
      
      在这个示例中,只有 IdName 字段被查询,而不是整个 Customer 实体的所有字段。
  2. 使用分页(SkipTake
    当查询可能返回大量记录时,使用分页技术可以限制一次查询的数据量。 Skip 用于跳过一定数量的记录,而 Take 用于限制返回记录的数量。

    var pagedResults = context.Customers
    	.OrderBy(c => c.Name)
    	.Skip(20)
    	.Take(10)
    	.ToList();
    

    这将返回从第21条到第30条的10个客户记录。

  3. 使用 Where 进行筛选
    在查询中使用 Where 子句来过滤数据,只返回满足条件的记录。这是减少数据量的最基本策略。

    var activeCustomers = context.Customers
    	.Where(c => c.IsActive)
    	.ToList();
    

    这个查询只返回 IsActivetrue 的客户。

  4. 使用 AsNoTracking
    当我们不需要修改查询结果时,使用 AsNoTracking 可以避免上下文对实体进行跟踪,从而减少内存使用和跟踪开销。

    var customers = context.Customers
    	.AsNoTracking()
    	.ToList();
    

    使用 AsNoTracking 后,查询结果不会被上下文跟踪,这对只读操作特别有用。

  5. 延迟加载和显式加载
    对于导航属性,延迟加载可以避免立即加载所有相关数据。显式加载可以控制何时加载相关数据,而不是在主查询中一次性加载所有相关实体。
    延迟加载

    var customer = context.Customers.FirstOrDefault(c => c.Id == customerId);
    // Orders 不会被立即加载,只有在访问 customer.Orders 时才会加载
    var orders = customer.Orders.ToList();
    

    显式加载

    var customer = context.Customers
    	.FirstOrDefault(c => c.Id == customerId);
    
    context.Entry(customer)
    	.Collection(c => c.Orders)
    	.Load();
    

    显式加载允许我们明确指定何时加载相关的 Orders

  6. 使用 AnyCount 替代 ToList
    如果我们只需要检查是否存在数据或获取数据的数量,使用 AnyCount 而不是 ToList,避免将所有记录加载到内存中。

    bool hasOrders = context.Orders.Any(o => o.CustomerId == customerId);
    
    int orderCount = context.Orders.Count(o => o.CustomerId == customerId);
    

    这里,AnyCountToList 更高效,因为它们只查询所需的少量数据。

  7. 适时使用投影(Projection)
    投影允许我们将查询结果映射到特定的 DTO 或匿名类型,减少不必要的数据传输,尤其是在处理嵌套数据结构时。

    var customerDtos = context.Customers
    	.Select(c => new CustomerDto
    	{
    		Id = c.Id,
    		Name = c.Name,
    		OrderCount = c.Orders.Count()
    	})
    	.ToList();
    

    在这个示例中,使用投影创建了一个包含所需字段的 CustomerDto,而不是加载完整的 Customer 实体。

  8. 使用数据库级别的优化
    确保数据库层面有适当的索引,并且查询能有效利用这些索引。查询性能在很大程度上依赖于数据库的配置和优化。

  9. 使用原生 SQL 查询(FromSqlRaw
    对于复杂查询,特别是涉及多个表的联接、子查询等,使用原生 SQL 查询(FromSqlRaw)可能比 LINQ 更高效。

    var customers = context.Customers
    	.FromSqlRaw("SELECT Id, Name FROM Customers WHERE IsActive = 1")
    	.ToList();
    

    通过原生 SQL,我们可以完全控制查询的优化方式。

  10. 使用缓存机制
    对于经常查询的数据,可以考虑使用内存缓存或分布式缓存来减少对数据库的重复查询,尤其是在高频率查询的场景中。

    • 在投影中使用自动加载功能
      在 EF Core 中,自动加载(Auto-Loading)指的是在查询实体时,EF Core 自动加载该实体的相关导航属性。虽然自动加载可以减少显式查询的复杂性,但在使用投影时,应谨慎使用以避免不必要的数据加载和性能问题。
      自动加载的工作机制
      当启用了自动加载功能时,EF Core 在访问实体的导航属性时,会自动从数据库中加载相关数据。这通常是通过延迟加载(Lazy Loading)实现的。当访问导航属性时,EF Core 会发出一个额外的 SQL 查询来加载相关的数据。
      在投影中使用自动加载
      当我们在 LINQ 查询中进行投影时,如果投影包含导航属性,并且自动加载已启用,EF Core 可能会自动加载这些导航属性的数据。以下是一些常见场景及如何有效使用自动加载的指南。

    • 延迟加载与自动加载
      延迟加载(Lazy Loading)是 EF Core 实现自动加载的常用方式。在投影中,如果访问了导航属性,而该属性尚未加载,延迟加载会触发加载该属性的数据。

      public class Customer
      {
      	public int Id { get; set; }
      	public string Name { get; set; }
      	public virtual ICollection<Order> Orders { get; set; }
      }
      
      var customer = context.Customers
      	.Select(c => new 
      	{
      		c.Name,
      		OrdersCount = c.Orders.Count  // 延迟加载 Orders 导致数据库查询
      	})
      	.FirstOrDefault();
      

      在这个示例中,当访问 Orders.Count 时,如果 Orders 尚未加载,延迟加载会触发 SQL 查询来加载订单数据。为了避免这种潜在的额外查询,可以显式加载或直接在查询中处理。

    • 投影中避免不必要的自动加载
      如果不希望在投影中自动加载导航属性,可以通过显式加载所需数据或使用 .AsNoTracking() 来避免上下文跟踪并加载导航属性。

      var customers = context.Customers
      	.AsNoTracking()
      	.Select(c => new 
      	{
      		c.Name,
      		OrdersCount = c.Orders.Count  // 不会触发延迟加载,因为 AsNoTracking 禁用了自动加载
      	})
      	.ToList();
      

      AsNoTracking() 禁用上下文跟踪,确保导航属性不会自动加载,但同时仍然允许我们在投影中访问已加载的数据。

    • 显式加载与投影结合
      为避免在投影中触发多个数据库查询,可以通过显式加载相关数据,然后进行投影。这在处理复杂导航属性时尤其有用。

      var customers = context.Customers
      	.Include(c => c.Orders)
      	.Select(c => new 
      	{
      		c.Name,
      		OrdersCount = c.Orders.Count  // 不会触发额外的查询,因为 Orders 已被 Include 加载
      	})
      	.ToList();
      

      在这个示例中,Include 方法显式加载了 Orders,因此在投影中访问 Orders.Count 不会再触发额外的数据库查询。

    • 使用显式投影避免加载多余的数据
      通过明确选择要加载的数据,可以避免自动加载导航属性,从而提高查询性能。

      var customers = context.Customers
      	.Select(c => new 
      	{
      		c.Name,
      		OrdersCount = c.Orders.Count(o => o.Status == "Completed") // 只计算特定状态的订单
      	})
      	.ToList();
      

      在此示例中,投影只计算已完成订单的数量,避免了加载不必要的订单数据。

    • 使用 Load 方法在需要时显式加载
      对于特定导航属性,在需要时手动加载可以避免不必要的自动加载。

      var customer = context.Customers
      	.FirstOrDefault(c => c.Id == customerId);
      
      context.Entry(customer)
      	.Collection(c => c.Orders)
      	.Load(); // 显式加载 Orders 导航属性
      
      var customerData = new 
      {
      	customer.Name,
      	OrdersCount = customer.Orders.Count
      };
      

      在这个示例中,显式加载确保了 Orders 导航属性被加载,并且只有在需要时才发出 SQL 查询。

四、 总结

本文探讨了如何在 EF Core 中使用高级查询技术,包括 LINQ 查询、原生 SQL 查询以及数据投影和选择。通过这些方法,开发者可以实现更高效的数据访问、灵活的数据转换,以及复杂的查询需求,为应用程序提供更强大的数据处理能力。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐