《c#10 in a nutshell》--- 读书随记(7)

Chaptor 8. LINQ Queries

内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的

LINQ (语言集成查询)是一组语言和运行时特性,用于编写针对本地对象集合和远程数据源的结构化类型安全查询。

LINQ 允许您查询实现 IEnumable < T > 的任何集合,无论是数组、列表还是 XML 文档对象模型(DOM) ,以及远程数据源,例如 SQLServer 数据库中的表。LINQ 提供了编译时类型检查和动态查询组合的优点。

Getting Started

string[] names = { "Tom", "Dick", "Harry" };

IEnumerable<string> filteredNames = names.Where (n => n.Length >= 4);

可以看到返回值还是一个IEnumerable,所以可以继续调用查询方法,这叫做fluent syntax,除了这种语法,还有另外一种,叫做query expression syntax

string[] names = { "Tom", "Dick", "Harry" };
IEnumerable<string> filteredNames = from n in names
                                    where n.Contains ("a")
                                    select n;

Fluent Syntax

Fluent syntax 是最灵活和最基本的。

Chaining Query Operators

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query = names
                            .Where(n => n.Contains ("a"))
                            .OrderBy (n => n.Length)
                            .Select(n => n.ToUpper());

Query Expressions

C # 为编写 LINQ 查询提供了一种语法快捷方式,称为查询表达式。与流行的观点相反,查询表达式并不是将 SQL 嵌入到 C # 中的一种方式。实际上,查询表达式的设计灵感主要来自于函数式编程语言(如 LISP 和 Haskell)的列表推导式,尽管 SQL 只是表面上的影响。

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
                            from n in names
                            where n.Contains ("a")
                            orderby n.Length
                            select n.ToUpper();

Query expressions总是以from开始,然后以select或者group结束

编译器通过将查询表达式翻译成 Fluent syntax 来处理查询表达式。它以一种相当机械的方式完成这项工作,就像它将 foreach 语句转换为对 GetEnumeratorMoveNext 的调用一样。这意味着你可以用查询语法写任何东西,也可以用 Fluent syntax 写。编译器将示例查询转换为以下内容:

IEnumerable<string> query = names
                            .Where(n => n.Contains ("a"))
                            .OrderBy (n => n.Length)
                            .Select(n => n.ToUpper());

Deferred Execution

大多数查询运算符的一个重要特性是,它们不是在构造时执行,而是在enumerated执行(换句话说,在其enumerator上调用 MoveNext 时执行)。

延迟执行还有另一个后果: 在reenumerate时,将重新计算延迟执行查询

var numbers = new List<int>() { 1, 2 };
IEnumerable<int> query = numbers.Select (n => n * 10);
foreach (int n in query) Console.Write (n + "|");       // 10|20|

numbers.Clear();
foreach (int n in query) Console.Write (n + "|");       // <nothing>

Captured Variables

如果查询的 lambda 表达式捕获外部变量,则查询将在运行查询时获得这些变量的新值:

int[] numbers = { 1, 2 };
int factor = 10;

IEnumerable<int> query = numbers.Select (n => n * factor);

factor = 20;
foreach (int n in query) Console.Write (n + "|");       // 20|40|

How Deferred Execution Works

查询运算符通过返回decorator序列提供延迟执行。

与传统的集合类(如数组或链表)不同,decorator 序列(通常)没有自己的后备结构来存储元素。相反,它包装您在运行时提供的另一个序列,并对其保持永久依赖关系。当你从一个 decorator 请求数据时,它又必须从包装好的输入序列请求数据。

调用 Where 仅仅构造了 decorator 包装序列,其中包含了对输入序列、 lambda 表达式和所提供的任何其他参数的引用。只有当 decorator 被枚举时,输入序列才会被枚举。

好消息是,我们可以很容易实现自己的查询操作,通过迭代器来实现

public static IEnumerable<TResult> MySelect<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    foreach (TSource element in source)
        yield return selector (element);
}

Chaining Decorators

IEnumerable<int> query = new int[] { 5, 12, 3 }
                                        .Where(n => n < 10)
                                        .OrderBy (n => n)
                                        .Select (n => n * 10);


How Queries Are Executed

在本章的第一节中,我们将查询描述为传送带生产线。扩展这个类比,我们可以说一个 LINQ 查询是一个延迟生产线,其中传送带滚动元素只有在需要的时候。构造一个查询可以构造一条生产线(所有东西都准备就绪) ,但是不需要任何滚动。然后,当使用者请求一个元素时(在查询上枚举) ,最右边的传送带激活; 这反过来又触发其他传送带滚动ー当需要输入序列元素时。LINQ 遵循需求驱动的拉动模型,而不是供给驱动的推动模型。在允许 LINQ 扩展到查询 SQL 数据库方面,这一点很重要

Subqueries

子查询是包含在另一个查询的 lambda 表达式中的查询。

string[] musos = { "David Gilmour", "Roger Waters", "Rick Wright", "Nick Mason" };

IEnumerable<string> query = musos.OrderBy (m => m.Split().Last());

允许子查询,因为您可以将任何有效的 C # 表达式放在 lambda 的右边。子查询只是另一个 C # 表达式。这意味着子查询的规则是 lambda 表达式规则(以及查询运算符的一般行为)的结果。

每当计算封闭的 lambda 表达式时,就执行一个子查询。这意味着子查询是根据需要执行的,由外部查询决定。你可以说执行进程从外向内。本地查询从字面上遵循这个模型; 解释查询(例如,数据库查询)从概念上遵循这个模型。

在“解释查询”中,我们描述了如何查询诸如 SQL 表之类的远程数据源。我们的示例是一个理想的数据库查询,因为它将作为一个单元进行处理,只需要一次到数据库服务器的往返过程。但是,这个查询对于本地集合是低效的,因为在每次外部循环迭代中都会重新计算子查询。我们可以通过分别运行子查询来避免这种低效率(这样它就不再是一个子查询)

int shortest = names.Min (n => n.Length);

IEnumerable<string> query = 
                            from n in names
                            where n.Length == shortest
                            select n;

子查询中的 First 或 Count 等元素或聚合运算符不会强制外部查询立即执行ーー延迟执行仍然适用于外部查询。这是因为子查询是间接调用的ーー如果是本地查询,则通过委托调用,如果是解释查询,则通过表达式树调用。

当您在 Select 表达式中包含子查询时,会出现一个有趣的情况。对于本地查询,您实际上是在投射一系列查询ーー每个查询本身都受到延迟执行的影响。效果一般是透明的,有助于进一步提高效率

Composition Strategies

介绍三种策略,用来构建更加复杂的查询

  • Progressive query construction,渐进式查询结构
  • into关键字
  • Wrapping queries,包装查询

Progressive Query Building

var filtered= names.Where(n => n.Contains ("a"));
var sorted= filtered .OrderBy (n => n);
var query= sorted.Select (n => n.ToUpper());

这种渐进式的方法的好处:

  • 容易编写
  • 可以根据判断条件,动态添加查询操作

假如有一个查询是

IEnumerable<string> query = names
                            .Select (n => n => Regex.Replace (n, "[aeiou]", ""))
                            .Where(n => n.Length > 2)
                            .OrderBy (n => n);

// Dck
// Hrry
// Mry

如果使用一个查询表达式

IEnumerable<string> query =
                        from n in names
                        where n.Length > 2
                        orderby n
                        select Regex.Replace (n, "[aeiou]", "");

// Dck
// Hrry
// Jy
// Mry
// Tm

查询的结果不一致,很明显,查询表达式的执行顺序是从左到右的,所以这时候,如果渐进式拆开两个查询,结果就是正确的了

IEnumerable<string> query =
                            from n in names
                            select Regex.Replace (n, "[aeiou]", "");

query = from n in query where n.Length > 2 orderby n select n;

// Dck
// Hrry
// Mry

The into Keyword

根据上下文,into 关键字由查询表达式以两种非常不同的方式进行解释。我们现在描述的含义是用于signaling query continuation

into关键字是让你在结束了上一个查询之后,可以继续这一个查询,属于渐进式查询的一个快捷键

IEnumerable<string> query =
                            from n in names
                            select Regex.Replace (n, "[aeiou]", "")
                            into noVowel
                                where noVowel.Length > 2 
                                orderby noVowel 
                                select noVowel;

into只能使用在select或者group后面

Scoping rules

var query =
            from n1 in names
            select n1.ToUpper()
            into n2
                where n1.Contains ("x") // Illegal: n1 is not in scope.
                select n2;

Wrapping Queries

IEnumerable<string> query =
                            from n1 in
                            (
                                from n2 in names
                                select Regex.Replace (n2, "[aeiou]", "")
                            )
                            where n1.Length > 2 
                            orderby n1 
                            select n1;

Projection Strategies

Object Initializers

到目前为止,我们的所有 select 子句都投影了标量元素类型。使用 C # 对象初始化器,您可以投影到更复杂的类型。例如,假设作为查询的第一步,我们希望从名称列表中去除元音,同时保留原始版本,以利于后续查询。我们可以写下面的类来帮助:

class TempProjectionItem
{
    public string Original;
    public string Vowelless;
}
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<TempProjectionItem> temp =
                                    from n in names
                                    select new TempProjectionItem
                                    {
                                        Original = n,
                                        Vowelless = Regex.Replace (n2, "[aeiou]", "")
                                    };

IEnumerable<string> query = 
                            from item in temp
                            where item.Vowelless.Length > 2
                            select item.Original;

Anonymous Types

可以使用匿名类型

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

var temp =
            from n in names
            select new
            {
                Original = n,
                Vowelless = Regex.Replace (n, "[aeiou]", "")
            };
            into temp
                where item.Vowelless.Length > 2
                select item.Original;

The let Keyword

Let 关键字在 range 变量旁边引入了一个新变量。

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
                        from n in names
                        let vowelless = Regex.Replace (n, "[aeiou]", "")
                        where vowelless.Length > 2
                        orderby vowelless
                        select n;

实际上编译器会将let翻译成匿名类型的例子那样的代码

您可以在 where 语句之前或之后使用任意数量的 let 语句。Let 语句可以引用前面的 let 语句中引入的变量(取决于 into 子句强加的边界)。让所有现有的变量重新项目透明。

Let 表达式不需要求值为标量类型: 例如,有时让它求值为子序列是有用的。

Interpreted Queries

LINQ 提供了两种并行体系结构: 本地对象集合的本地查询和远程数据源的解释查询。到目前为止,我们已经研究了本地查询的体系结构,它操作实现 IEnumable < T > 的集合。本地查询解析为查询枚举类中的运算符(默认情况下) ,而运算符又解析为 decorator 序列链。它们接受的委托(无论是以查询语法、 Fluent syntax 还是传统委托的形式表示)对于中间语言(IL)代码来说都是完全本地的,就像任何其他 C # 方法一样。

相比之下,解释查询是描述性的。它们操作实现 IQueryable < T > 的序列,并解析为 Queryable 类中的查询操作符,这些操作符生成在运行时解释的表达式树。这些表达式树可以被翻译成 SQL 查询,例如,允许您使用 LINQ 查询数据库。

IQueryable < T > 是 IEnumable < T > 的扩展,带有用于构造表达式树的其他方法。大多数时候,您可以忽略这些方法的细节; 它们是由运行时间接调用的。

using var dbContext = new NutshellContext();
IQueryable<string> query = 
                            from c in dbContext.Customers
                            where c.Name.Contains ("a")
                            orderby c.Name.Length
                            select c.Name.ToUpper();

foreach (string name in query) Console.WriteLine (name);

How Interpreted Queries Work

首先,编译器将查询语法转换为 Fluent syntax,这与本地查询完全一样:

IQueryable<string> query = dbContext.customers
                                .Where (n => n.Name.Contains ("a"))
                                .OrderBy (n => n.Name.Length)
                                .Select (n => n.Name.ToUpper());

接下来,编译器解析查询运算符方法。这就是本地查询和解释查询的不同之处ーー解释查询解析为 Queryable 类中的查询运算符,而不是Enumerable类中的查询运算符。

为了了解原因,我们需要查看 dbContext.Customers 变量,整个查询基于该变量构建。dbContext.Customers 属于 DbSet < T > 类型,它实现 IQueryable < T > (IEnumable < T > 的子类型)。这意味着编译器在解析 Where: 时可以选择调用 Enumable 中的扩展方法或以下扩展 Queryable 方法:

public static IQueryable<TSource> Where<TSource> (this IQueryable<TSource> source, Expression <Func<TSource,bool>> predicate)

编译器选择 Queryable。其中,因为它的签名是一个更具体的匹配。

Queryable.其中接受一个包装在 Expression < Tgenerate > 类型中的谓词。这指示编译器翻译提供的 lambda 表达式ーー换句话说,n=>n.Name.Contains("a")翻译到表达式树而不是已编译的委托。表达式树是基于 System.Linq.Expressions 类型的对象模型。可以在运行时检查的表达式(以便 EF Core 稍后可以将其转换为 SQL 语句)。因为 Queryable 返回 IQueryable < T > 的地方,OrderBy 和 Select 操作符将执行相同的过程。

Execution

解释查询遵循延迟执行模型ーー就像本地查询一样。这意味着在开始enumerating查询之前不会生成 SQL 语句。

实际上,解释查询的执行方式与本地查询不同。当您枚举解释查询时,最外面的序列运行一个程序,该程序遍历整个表达式树,并将其作为一个单元进行处理。在我们的示例中,EFCore 将表达式树转换为 SQL 语句,然后执行该语句,并将结果作为序列生成。

Combining Interpreted and Local Queries

查询可以包括解释运算符和本地运算符。典型的模式是在外部使用本地运算符,在内部使用解释组件; 换句话说,解释查询为本地查询提供信息。这种模式在查询数据库时效果很好。

public static IEnumerable<string> Pair (this IEnumerable<string> source)
{
    string firstHalf = null;
    foreach (string element in source)
        if (firstHalf == null)
            firstHalf = element;
        else
        {
            yield return firstHalf + ", " + element;
            firstHalf = null;
        }
}

using var dbContext = new NutshellContext ();
IEnumerable<string> q = dbContext.Customers
                        .Select (c => c.Name.ToUpper())
                        .OrderBy (n => n)
                        .Pair()
                        .Select ((n, i) => "Pair " + i.ToString() + " = " + n);

EF Core

EF Core Entity Classes

EF Core 允许您使用任何类来表示数据,只要它包含要查询的每个列的公共属性。

DbContext

定义实体类之后,下一步是子类 DbContext。该类的实例表示使用数据库的会话。通常,您的 DbContext 子类将为模型中的每个实体包含一个 DbSet < T > 属性

public class NutshellContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    ... properties for other tables ...
}

DbContext 对象做三件事:

  • 它充当生成可查询的 DbSet < > 对象的工厂。
  • 它跟踪您对实体所做的任何更改,以便您可以将它们写回
  • 它提供了一些 virtual 方法,您可以重写这些方法来配置连接和模型

Configuring the connection

通过重写OnConfiguring方法,可以指定数据库的连接信息

public class NutshellContext : DbContext
{
...
protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder) =>
    optionsBuilder.UseSqlServer
    (@"Server=(local);Database=Nutshell;Trusted_Connection=True");
}

Configuring the Model

默认情况下,EF Core 是基于约定的,这意味着它从你的类和属性名推断出数据库 schema。

通过重写 OnModelCreate 并调用 ModelBuilder 参数上的扩展方法,可以使用Fluent api 重写默认值。

protected override void OnModelCreating (ModelBuilder modelBuilder) =>
        modelBuilder.Entity<Customer>()
                    .ToTable ("Customer");
  • HasColumnName,指定字段有不同的列名
  • IsRequired, 表明字段不能为空
protected override void OnModelCreating (ModelBuilder modelBuilder) =>
        modelBuilder.Entity<Customer> (entity =>
        {
        entity.ToTable ("Customer");
        entity.Property (e => e.Name)
                .HasColumnName ("Full Name") // Column name is 'Full Name'
                .IsRequired();              // Column is not nullable
        });

Creating the database

最简单的创建数据库的方法是dbContext.Database.EnsureCreated();,但是最好的方法是使用 EF Core的 migrations 功能。它不仅仅是创建数据库,而且还可以自动配置它,当你在将来想要修改数据库表结构的时候,修改代码会带着数据库一起升级

dotnet ef migration add <name>
dotnet ef database update

Object Tracking

DbContext 实例跟踪它实例化的所有实体,因此当您请求表中的相同行时,它可以将相同的实体反馈给您。换句话说,一个上下文在其生命周期中将永远不会产生两个单独的实体,这两个实体引用表中的同一行(其中一行由主键标识)。这种能力被称为目标跟踪。

考虑一下当 EFCore 遇到第二个查询时会发生什么。它首先查询数据库,然后获取一行。然后它读取该行的主键,并在上下文的实体缓存中执行查找。如果看到匹配,它将返回现有对象,而不更新任何值。因此,如果另一个用户刚刚在数据库中更新了该客户的 Name,新值将被忽略。这对于避免意外的副作用(Customer 对象可能在其他地方使用)以及管理并发性都是必不可少的。如果已经更改了 Customer 对象上的属性,但尚未调用 SaveChanges,则不希望自动覆盖属性。

若要从数据库获取新信息,必须实例化新上下文或调用 Relload 方法。最佳实践是在每个工作单元中使用一个新的 DbContext 实例,因此很少需要手动重新加载实体。

Change Tracking

当您更改通过 DbContext 加载的实体中的属性值时,EF Core 会识别更改,并在调用 SaveChanges 时相应地更新数据库。为此,它创建一个通过 DbContext 子类加载的实体状态的快照,并在调用 SaveChanges 时将当前状态与原始状态进行比较,可以在 DbContext 中列举所跟踪的更改,如下所示:

foreach (var e in dbContext.ChangeTracker.Entries())
{
    Console.WriteLine ($"{e.Entity.GetType().FullName} is {e.State}");
    foreach (var m in e.Members)
        Console.WriteLine (
        $" {m.Metadata.Name}: '{m.CurrentValue}' modified: {m.IsModified}");
}

当您调用 SaveChanges 时,EF Core 使用 ChangeTracker 中的信息来构造 SQL 语句,该语句将更新数据库以匹配对象中的更改,发出插入语句以添加新行,更新语句以修改数据,并删除语句以删除从 DbContext 子类中的对象图中删除的行。任何 TransactionScope 都honored; 如果不存在任何 TransactionScope,则它将所有语句封装在新事务中。

可以通过在实体中实现 INotifyPropertyChanged 或者在实体中实现 INotifyPropertyChanging 来优化更改跟踪。前者允许 EF Core 避免与原始实体进行比较的开销; 后者允许 EF Core 避免完全存储原始值。在实现这些接口之后,在配置模型以激活优化的更改跟踪时,在 ModelBuilder 上调用 HasChangeTrackingStrategy 方法。

导航属性允许您执行以下操作:

  • 查询相关表而不必手动联接
  • 插入、删除和更新相关行,而不显式更新外键

可以根据约定配置关联关系,也可以手动配置

modelBuilder.Entity<Purchase>()
                .HasOne (e => e.Customer)
                .WithMany (e => e.Purchases)
                .HasForeignKey (e => e.CustomerID);

Adding and removing entities from navigation collections

Customer cust = dbContext.Customers.Single (c => c.ID == 1);
Purchase p1 = new Purchase { Description="Bike", Price=500 };
Purchase p2 = new Purchase { Description="Tools", Price=100 };

cust.Purchases.Add (p1);
cust.Purchases.Add (p2);
dbContext.SaveChanges();

当从集合导航属性中删除一个实体并调用 SaveChanges 时,EF Core 将清除外键字段或从数据库中删除相应的行,具体取决于如何配置或推断关系。在这个例子中,我们定义了“Purchases”。CustomerID 作为一个可空整数(这样我们就可以在没有客户或现金交易的情况下表示Purchases) ,因此从客户删除Purchases将清除其外键字段,而不是从数据库中删除。

Loading navigation properties

当查询一个实体类的时候,默认是不会查询相关的导航属性

可以使用 Include 方法提前加载

var cust = dbContext.Customers
                .Include (c => c.Purchases)
                .Where (c => c.ID == 2).First();

另一种解决方案是使用投影。当您只需要处理某些实体属性时,这种技术特别有用,因为它减少了数据传输:

var custInfo = dbContext.Customers
            .Where (c => c.ID == 2)
            .Select (c => new
            {
                Name = c.Name,
                Purchases = c.Purchases.Select (p => new { p.Description, p.Price })
            })
            .First();

这两种技术都告诉 EF Core 您需要什么样的数据,以便可以在单个数据库查询中获取这些数据。还可以根据需要手动指示 EF Core 填充导航属性:

dbContext.Entry (cust).Collection (b => b.Purchases).Load();

这叫做 explicit loading 与前面的方法不同,这将生成到数据库的额外往返过程。

Lazy loading

加载导航属性的另一种方法称为延迟加载。如果启用,EFCore 将根据需要生成导航属性,方法是为每个实体类生成一个代理类,用于拦截访问未加载的导航属性的尝试。为了实现这一点,每个导航属性都必须是 virtual 的,并且它所定义的类必须是可继承的(不是密封的)。另外,在延迟加载发生时,上下文不能被释放,这样就可以执行额外的数据库请求。

protected override void OnConfiguring (DbContextOptionsBuilderoptionsBuilder)
{
    optionsBuilder
        .UseLazyLoadingProxies()
    ...
}

延迟加载的代价是每次访问卸载的导航属性时,EF Core 必须向数据库发出额外的请求。如果您发出许多这样的请求,那么过多的往返会影响性能。

Building Query Expressions

到目前为止,在本章中,当我们需要动态组合查询时,我们已经通过有条件地链接查询运算符来完成。尽管在许多场景中这已经足够了,但有时您需要在更细粒度的级别上工作,并动态组合提供给操作符的 lambda 表达式。

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

public static IQueryable<TSource> Where<TSource> (this IQueryable<TSource> source, Expression<Func<TSource,bool>> predicate)

IEnumerable<Product> q1 = localProducts.Where(p => !p.Discontinued);

IQueryable<Product> q2 = sqlProducts.Where(p => !p.Discontinued);

Compiling expression trees

可以通过调用 Compile 将表达式树转换为委托。这在编写返回可重用表达式的方法时具有特殊的价值。

void Test()
{
var dbContext = new NutshellContext();
Product[] localProducts = dbContext.Products.ToArray();
IQueryable<Product> sqlQuery = dbContext.Products.Where (Product.IsSelling());

IEnumerable<Product> localQuery = localProducts.Where (Product.IsSelling().Compile());
}

AsQueryable

将本地序列转换为 IQueryable

posted @   huang1993  阅读(159)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示