当前位置: 首页 > 工具软件 > efcore > 使用案例 >

LINQ和EF Core基础

庄文栋
2023-12-01

LINQ和EFCore基础

LINQ基础

语言集成查询(Language Integrated Query)是一组语言扩展,用于处理数据序列,然后对它们进行过滤、排序,并将它们投影到不同的输出。

LINQ查询语法是定义在Enumable里的,这就意味着LINQ不仅可以对内置序列进行操作,比如List,Dictionary,Stack等,也可以对Sqlite,MySql等数据库内容进行操作。仔细观察LINQ的方法,所有实现了IEnumerable的类型都可以被LINQ操作。

LINQ分为多个部分:

  • 扩展方法(必需):Where,OrderBy,Select等,这些提供了基本操作;
  • LINQ提供程序(必需):LINQ to Entities,LINQ to XML等,这些程序将LINQ指令转换为其特定操作;
  • Lambda表达式(可选):简化LINQ操作数;
  • LINQ查询理解语法(可选):where,in,from,orderby,descending,select。这些可以简化LINQ的查询。

LINQ处理数组

现在假设有一序列为

string[] names = {"Peter","Alice","Bob","John","Jim","Sam"};

Where过滤

在使用LINQ查询语法之前,要在代码顶部添加一行:

using System.Linq;

来引入LINQ命名空间。

首先观察Where方法:

Where<TSource>(this IEnumerable<TSource>, Func<TSource,Boolean>);

可以看到其参数中含有一个Func委托:

Func<T,bool>

这就要求必须编写一个方法,这个方法的入口参数为T,返回参数为bool。

那么假如想过滤出一些名字,这些名字的要求是字符长度大于等于4,那么就可以这样写:

首先写一个符合Func参数的方法:

bool GetNames(string name)
{
    return name.Length > 3;
}

然后写Where查询操作:

var query = names.Where(new Func<string,bool>(GetNames));

然后就能得到一个数组,里面包含符合要求的name了。

C#的编译器可以自动实例化委托,所以也可以把查询操作这样写:

var query = names.Where(GetNames);

在C#3.0引入了Lambda表达式之后,可以更进一步简化查询代码:

var query = names.Where(n => n.Length > 3);

这样就不需要再写一个GetNames方法了,简化了代码,提高了效率。

OrderBy排序

可以使用OrderBy和ThenBy来进行序列的排序。

假设要将names序列按照字符长度排序,那么可以这样写:

var query = names.OrderBy(n=>n.Length);

如果在此之上还想按照字典序排列,那么可以这样写:

var query = names.OrderBy(n=>n.Lngth).ThenBy(n=>n);

最后的输出结果就是这样的:

"Bob","Jim","Sam","John","Alice","Peter"

OfType过滤

假设有这么一个序列,存储了一些类型,这些类型遵从一定的继承层次,当想从中过滤出一些特定的类型来使用的话,OfType就很有用了。

现在有一异常序列:

var exceptions = new Exception[]
{
    new ArgumentException(),
    new SystemException(),
    new IndexOutOfRangeException(),
    new InvalidOperationException(),
    new NullRefrenceException(),
    new InvalidCastException(),
    new OverFlowException(),
    new DevideByZeroException(),
    new ApplicationException(),
};

如果想要从中筛选出和代数计算有关的异常,那么就可以这样写:

var query = exceptions.OfType<ArithmeticException>();

LINQ处理集合

在集合的处理中,除了上面的方法之外,还可以使用下面的方法。

创建三个数组:

var names1 = new string[]{"John","Machel","Bob","Dick"};
var names2 = new string[]{"Jack","Alice","Dinnis","Jack","Linus"};
var names3 = new string[]{"Jack","James","Jack","Stephen","Conor"};

如果想要获取关于names2的集合的话,一般情况下是要用循环去解决。现在用LINQ的**Distinct()**方法就能解决这个问题:

var query = names2.Distinct();

求集合的并集可以用下面的方法:

var query = names2.Union(names3);

求交集可以这样写:

var query = names2.Intersect(names3);

求差集:

var query = names2.Except(names3);

连接两个数组:

var query = names2.Concat(names3);

Entity Framework Core

实体框架核心是微软开发的开源对象关系映射框架,可以用来读写数据库。

EFCore不仅支持传统关系数据库管理系统RDBMS,也支持现代数据库,比如MongoDB,CosmosDB等NoSQL,有时甚至可以支持第三方程序,这都得益于其开源性。EFCore和.Net Core一样,都可以跨平台使用。

设置EFcore

在使用EF Core之前,需要先设置EF Core的使用环境。

首先要根据使用的目的数据库下载不同的NuGet包,这里使用SQLite为例子。

然后安装dotnet-ef工具,使用指令:

dotnet tool install --global dotnet-ef

最后在csproj文件中添加对应NuGet包的依赖,完成导入。

定义EFCore模型

EF Core使用约定,注解和Fluent API组合,在运行时构建实体模型。实体类表示表的结构,类的实例表示表中的一行。

约定

一般来说,编写EF的代码要遵守下面的约定:

  • 表名和DbContext类中DbSet的属性名匹配;

  • 列名和类中的属性名匹配;

  • string类型和nvarchar匹配

  • 名为ID的属性,可以将其重命名为类名+ID,然后假定这个是主键。如果这个属性是整数或者Guid类型,那就可以假定为IDENTITY类型。

当然,可以不局限于这些约定,创造自己的约定也是可以的。

注解特性

只有约定还不足以完成对映射的搭建,借助C#的特性可以进一步帮助构建模型:

比如在数据库中,产品名称要求限制40字符并且不能为空,那么就可以这样定义:

[Required]
[StringLength(40)]
public string ProductName {get;set;}

如果没有明显映射的时候,可以用特性手动添加:

[Column(TypeName = "money")]
public decimal? UnitPrice {get;set;}

如果有项的长度超过nvarchar的800字符长度的时候,就需要手动映射到ntext。

Fluent API

Fluent API不仅可以用来替代特性,也可以进行特性的补充。

例如上面的ProductName,如果不使用特性而使用Fluent API的话,可以在DbContext类的OnModelCreating方法中写成下面的格式:

modelBuilder.Entity<Product>()
    .Property(p => p.ProductName)
    .IsRequired()
    .HasMaxLength(40);

注意: Fluent API 配置具有最高优先级,并将替代约定和数据注释。

new一个数据库

如果手头上没有数据库,就需要创建一个新数据库。通常情况下,可以使用对应的SQL工具来创建一个空的数据库,以SQLite为例,首先需要安装好使用SQLite的相关工具,然后在命令行中执行下面的操作:

sqlite3
.open DBname.db
.quit

或者是这样写:

sqlite3 DBname.db
.quit

也可以通过写C#代码的方式创建数据库。

首先是添加工具:

dotnet add package Microsoft.EntityFrameworkCore.Design

假设要创建这样一个数据库:数据库名字是ProductDB,有一个Product表,表中有ProductID和ProductName两个行,那么就要在.cs文件中这样写:

class Product {
    
    [Required]
    public int ProductID {get;set;}
    public string ProductName{get;set;}
}

class ProductContext : DbContext {
    public DbSet<Product> Products {get;set;}
    string DBPath = "./Product.db";
    
    protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite($"Data Source={DbPath}");
    
}

注意到,在方法OnConfiguring中,使用了Lambda表达式。方法的原始写法是这样的:

 protected override void OnConfiguring(DbContextOptionsBuilder options){
     	options.UseSqlite($"Data Source={DbPath}");
 }

然后在命令行中输入:

dotnet ef migrations add ProductDB
dotnet ef database update

这样,一个新数据库就迁移完成了,也能立即使用LINQ对数据库进行一系列的操作。

实际上,EF Core的迁移不仅可以用来创建数据库,它最主要的用途是更新数据库架构以符合当前的模型,并且不会消除原来的数据。

用一个现有的数据库

如果手头上有一个现成的数据库,比如NorthWind——微软提供的一个示例数据库。下面的所有操作都会针对NorthWind这个数据库。

现在让来构建EF Core模型。

在大致观察过WorthWind数据库之后,选择Category和Product表。

建立实体

在EF Core模型中,需要用到一些数据实体,这些实体都是以类的形式表示的,其中类表示表,类属性表示行。

建立关于这两个表的类:

class Category {
    public int CategoryID {get;set;}
    
    public string CategoryName {get;set;}
    
    [Column(TypeName = "ntext")]
    public string Description {get;set;}
    
    //导航属性,用于关联不同的行
    public virtual ICollection<Product> Products {get;set;}
    
    public Category(){
        this.Products = new HashSet<Product>();
    }
}

class Product {
    public int ProductID {get;set;}
    
    [Required]
    [StringLength(40)]
    public string ProductName {get;set;}
    
    [Column("UnitPrice",TypeName = "money")]
    public decimal? Cost {get;set;}
    
    [Column("UnitsInStock")]
    public short? Stock {get;set;}
    
    public bool Discontinued {get;set;}
    
    //2个被关联的属性
    public int CategoryID {get;set;}
    public virtual Category Category {get;set;}
}

注意到这两个类中都有一项被virtual修饰的关联属性,这可以让EF Core继承和覆盖这些属性来提供额外的特性,比如延迟加载。

建立DbContext类

DbContext类在C#中用于表示数据库,这个类知道怎么样和数据库通信,并且将C#代码转化为SQL语句,以便查询和操作数据。

在DbContext类里,必须有一些DbSet<T>属性,这些属性表示数据库中的表。为了表示每个表有哪些类,DbSet使用泛型来指明表类,这些类表示表中的一行,类的属性表示表中的类。例如:

//             ↓表示表的行 
public DbSet<Product> Products {get;set;}
//        ↑表示一个表    ↑C#中代表的表名

如果不想让表公共可写,那么可以设置成只读:

public DbSet<Product> Products {
    get{
        return Set<Product>();
    }
}

DbContext类里应该还包括OnConfiguring方法来链接数据库。OnModelCreating方法可以用来编写Fluent API语句来替代特性修饰实体类。

最终得到DbContext类的大概内容如下:

class Northwind : DbContext {
    private string DBPath = "./Northwind.db";
    
    public DbSet<Category> Categories {get;set;}
    public DbSet<Product> Products {get;set;}
    
    protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite($"Data Source={DBPath}");
	
    //覆盖并实现OnModelCreating方法
    protected override void OnModelCreating(ModelBuilder model)
    {
        model.Entity<Category>()
            .Property(c => c.CategoryName)
            .IsRequired()
            .HasMaxLength(15);
        model.Entity<Product>()
            .Property(p => p.Cost)
            .HasConversion<double>();
    }
}

注意:在EF Core 3.0及以上版本,decimal不再支持排序和其他操作。因此可以告诉SQLite将decimal转换成double值,但是在运行时并不会执行转换。

自动生成模型

上面的所有操作都是由人工手动进行的,但是这些操作也可以让程序自动生成。

首先应安装好EF Core的设计包:

dotnet add package Microsoft.EntityFrameworkCore.Design

选定一个存放模型的文件夹,例如AutoModel,并且输入下面的指令:

dotnet ef DbContext scaffold "Filename=Northwind.db" Microsoft.EntityFrameworkCore.Sqlite --table Categories --table Products --output-dir AutoModel --namespace AutoModel --data-annotations --context Northwind

看起来很长一串指令,将逻辑理清后是这样的:

  • 使用dotnet-ef工具:dotnet ef
  • 需要执行的dotnet-ef工具指令:DbContext scaffold
  • 链接的数据库:“Filename=Northwind.db”
  • 使用的数据库提供者:Microsoft.EntityFrameworkCore.Sqlite
  • 需要用到的表:–table Categories --table Products
  • 输出文件夹:–output-dir AutoModel
  • 类的名称空间:–namespace AutoModel
  • 使用Fluent API和数据注解:–data-annotations
  • XXContext中的XX:–context Northwind

打开自动生成的文件,会发现和手动创建的有一些不同,比如:

  • Index特性:用来指明应该含有索引的属性
  • CategoryID是被Key特性修饰,说明它是主键
  • Category类中的Products被InverseProperty特性修饰,这个特性用来定义Product类中Category的外键属性
  • Northwind被声明为partial,这允许了在未来的类扩展,并且不会被抹除
  • Northwind类中含有两个构造方法,这对于想自定义链接字符串的程序很有用
  • OnConfiguring方法中,如果构造方法的options没有指定,那么将使用链接字符串在当前文件夹查找数据库。但是应该指定options,硬编码数据库连接字符串是不安全的。
  • 含有一个OnModelCreatingPartial的partial方法,这可以让程序员额外添加新的模型,并且不会被抹除

对EF Core模型进行查询

前面说过,LINQ是可以对数据库进行操作的,所以可以编写一些简单的LINQ语句来查询Northwind数据库。

由于DbContext是通过转化SQL语句和db文件进行通信,这和对文件的读写是一样的,所以要用try块或者using包裹。

获取一些数据

假如要查询每一个Category里Product的数量,那我们可以这样写:

using (var db = new Northwind())
{
    IQueryable<Category> cats = db.Categories.Include(c => c.Products);
    
   foreach(var c in cats)
   {
       Console.WriteLine($"{c.CategoryName} has {c.Products.Count} products.");
   }
}

在这里,Include(c => c.Products)语句将Category和Product关联了起来,前面在Category类中写的virtual的Products属性得到了应用。

不仅可以提取所有的Category,还可以进行筛选提取。比如提取库存大于100的Category:

using (var db = new Northwind())
{
    IQueryable<Category> cats = db.Categories.Include(c => c.Products.Where(p.Stock > 100));
    
   foreach(var c in cats)
   {
       Console.WriteLine($"{c.CategoryName} has {c.Products.Count} prodcts.");
   }
}

只需要在c.Products后面添加Where筛选语句就可以了。

查看SQL语句

可以在程序中查看DbContext对数据库进行的SQL操作。假如对上面的Include操作进行查看,只需要添加这样一行:

Console.WriteLine(cats.ToQueryString());

筛选和排列

假设要筛选价格超过100的Product,并且按照价格从低到高排序,可以这样写:

using var db = new Northwind();
var products = db.Products.Where(p => p.Cost > 100).OrderBy(p => p.Cost);
foreach(var p in products)
{
    Console.WriteLine(p.ProductName);
}

模式匹配和Like

EF Core也支持模式匹配Like。假设想查询含有某个字符串中字符的Product,那么可以这样写:

using var db = new Northwind();
var query = db.Products.Where(p => EF.Functions.Like(p.ProductName,"%che%"));
foreach(var p in query)
{
    Console.WriteLine(p.ProductName);
}

全局过滤器

在示例数据库,Products表中,有些Product已经停产,为了确保不会被检索到,那么就可以确定一个全局过滤器,在Northwind类的OnModelCreating方法中添加这样一句:

model.Entity<Product>()
    .HasQueryFilter(p => !p.Discontinued);

此后不论是程序员是否忘记过滤掉停产产品,程序也能自动过滤掉。

记录EF Core

如果想要监视EF Core和数据库之间的交互,可以用日志记录功能。

注册日志提供程序

首先是编写一个Provider用来提供一个Logger,这个类必须实现ILoggerProvider,并且有一个方法会返回一个Logger实例。由于不使用任何非托管资源,因此Dispose方法不需要做任何事情,但是必须存在。

class DBLoggerProvider : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName)
    {
        return new DBLogger();
    }
    
    public void Dispose(){}
}

实现日志程序

日志类必须实现ILogger接口,并且当日志级别是None、Trace和Infomation时,禁用这个Logger。其余的级别均要启用。

注意:这里的ILogger接口源自Microsoft.Extensions.Logging。

class DBLogger : ILogger
{
    //如果有非托管资源要使用,这里要返回一个实现IDisposable的类
    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }
    
    public bool IsEnabled(LogLevel level)
    {
        switch(level)
        {
            case LogLevel.Trace:
            case LogLevel.Information:
            case LogLevel.None:
                return false;
            default:
                return true;
        }
    }
    
    public void Log<TState>(LogLevel level, EventId id, TState state, Exception e,
                           Func<TState, Exception, string> formatter)
    {
        Write($"Level: {level}, Event ID: {id}");
        if(state != null)
        {
            Write($", State: {state}");
        }
        if(e != null)
        {
            Write($", Exception: {e.Message}");
        }
        WriteLine();
    }
}

然后在主程序的using块中注册一个日志记录器:

using var db = new Northwind();
var loggerFactory = db.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new DBLoggerProvider());

这样,一个日志记录器就注册完成并且可以立即使用了。

在EF Core的日志记录器中,如果想要知道每一条LINQ查询语句是怎样转化为SQL语句的话,那么就可以抓取特定Event ID的事件,这个ID是20100。对Log方法的修改如下:

public void Log<TState>(LogLevel level, EventId id, TState state, Exception e,
                           Func<TState, Exception, string> formatter)
    {
        if(id == 20100){
            Write($"Level: {level}, Event ID: {id}");
        	if(state != null)
        	{
            	Write($", State: {state}");
        	}
        	if(e != null)
        	{
            	Write($", Exception: {e.Message}");
        	}
        	WriteLine();
        }
    }
}

查询Tag

在复杂的场景进行日志记录的时候,往往会有大量的日志,如果想要查找某些特定操作的日志是比较困难的。EF Core2.2引入了查询标记,允许向日志添加SQL注释:

using var db = new Northwind();
var query = db.Products.TagWith("Cost larger than 100").Where(p => p.Cost > 100);

EF Core的加载模式

EF Core有三种加载模式:立即加载,延迟加载和显示加载。

立即加载

在前面的获取数据中,去掉Include方法,然后运行程序,发现Stock那里均是0,这是因为Category的每个实例的Product都是Null,原始查询只在Category表里进行。

延迟加载

延迟加载能很好地解决上面立即加载产生的问题。

首先添加Microsoft.EntityFrameworkCore.Proxies包,然后在Northwind类中的OnConfiguring方法里,进行下面的修改:

Onconfiguring(DbContextOptionsBuilder options) =>
    options.UseLazyLoadingProxies().UseSqlite($"{DBPath}");

然后再进行立即加载所描述的操作,就会发现打印出来的结果就正常了。

但是,延迟加载所带来的缺点就是,每次读取Product属性的时候,都要检查是否被加载了,如果没有加载,就要去加载它们,导致返回结果时,要进行大量的数据库的交互,造成性能损失。这可以通过前面的日志记录器中查询到。

显式加载

显式加载和延迟加载类似,但是可以控制特定的数据和时间进行加载。

使用DbContext中的Entry方法就可以实现显式加载:

using var db = new Northwind();
var category = db.Categories.Single(c => c.CategoryID == 2);
var products = db.Entry(category).Collection(c => c.Products).Load();

在这里,获取了一个CategoryID为2的category,然后用Entry方法进行显式加载。很明显有一个Load()方法,这就体现了显式。

EF Core操作数据

使用EF Core对数据进行增删改是一件比较简单的事情,DbContext能够自动维护更改跟踪。当准备将更改发送到数据库的时候,调用SaveChanges()方法。这个方法返回成功更改的实体数量。

插入

使用Add方法添加一个实体:

using var db = new Northwind();
db.Products.Add(new Product(){CategoryID = 1,ProductName = "apple",Cost = 10});
db.SaveChanges();

更新

获取特定实体并且更改数据:

using var db = new Northwind();
var product = db.Products.Single(p => p.ProductID == 1);
product.Cost += 10;
db.SaveChanges();

删除

使用Remove删除单个实体,RemoveRange来删除多个实体:

using var db = new Northwind();
db.Products.Remove(db.Products.Single(p => p.ProductID == 2));
db.SaveChanges();

数据库的池化

为了提高EF Core查询模型的性能,并且在Web服务中尽可能汇集数据库来提高效率,那么就可以使用数据库池来进行。在Startup类中的ConfigureServices方法里添加:

service.AddDbContextPool<Northwind>(options => options.UseSqlite("Data Source = path"))

事务

每次调用SaveChanges时,都会启动一个隐藏的事务,以便在出现问题时执行回滚操作。如果一个事务中多个操作都成功了,那么就会提交事务和所有更改。

事务通过应用锁来防止在提交更改的时候读写,从而维护了数据库的稳定性。

事务有四个基本特性:

  • 原子性:即不可分割性。要么都提交,要么都不提交。
  • 一致性:事务必须使数据库从一个一致性状态变到另一个一致性状态。
  • 隔离性:在事务的处理期间,会对其他进程隐藏修改。
  • 持久性:事务在提交之后,对系统的影响是永久的。

定义显式事务

在程序中,我们可以使用DbContext的Database属性建立一个显式的事务:

using var db = new Northwind();
using var t = db.Database.BeginTransaction();
var product = db.Products.Single(p => p.ProductID == 2);
product.Cost += 10;
db.SaveChanges();
t.Commit();

LINQ进阶

这一部分讨论LINQ比较进阶一些的内容。

语法糖

C#3.0引入了查询理解语法,可以让有SQL经验的程序员更容易编写LINQ语句。使用前面提到的数组names:

var query = from name in names
    where name.Length > 3
    select name;

这和前面的结果是一样的。

创建自己的LINQ扩展方法

可以通过扩展方法的形式创建一个自定义的LINQ语句。自定义方法可以放在单独的类库里,也可以附加在Linq命名空间里。假设要附加一个中位数方法:

public static class NewLinqExtensions
{

    public static int Midian(this IEnumerable<int> sequence)
    {
        var ordered = sequence.OrderBy(item => item);
        var midianNum = ordered.Count() / 2;
        return ordered.ElementAt(midianNum);
    }

    public static int Midian<T>(this IEnumerable<T> sequence, Func<T, int> selector)
    {
        return sequence.Select(selector).Midian();
    }
}

LINQ To XML

LINQ可以对XML进行查询和操作。

写入XML

假设要将Northwind数据库中的Category表做成xml文件,那么就要理清xml树的结构:

<Categories>
    <category>
    	<ID />
        <Name></Name>
        <description></description>
    </category>
</Categories>

大致是这样的。

接下来就可以写代码来进行转换:

using var db = new Northwind();
//首先进行转换,将数据库内容转为数组
var cats = db.Categories.ToArray();
//开始创建xml树
var xmlTree = new XElement("Categories",  //xml树根
                          from c in cats  //用LINQ理解语法遍历cats创建树
                          select new XElement("category",
                                             new XAttribute("ID", c.CategoryID),
                                             new XElement("Name", c.CategoryName),
                                             new XElement("description", c.Description)));

如果对理解查询语法不熟悉的话,可以这样写:

var xmlTree = new XElement("Categories",
                xmlContents.Select(c => new XElement("category",
                    new XAttribute("ID", c.CategoryID),
                    new XElement("Name", c.CategoryName),
                    new XElement("description", c.Description))));

最后得出的结构是一样的。

读取XML

使用LINQ可以轻松查询或处理XML文件。

假设要读取上面xml中的ID为2的category,并将Name修改为HiXML,那么就可以这样写:

//首先将上面的xml存到一个文件中,读取文件
var xml = XDocument.Load("e:/Northwind.xml");
//获取所有的category
var categories = xml.Descendants("Categories").Descendants("category");
//找到ID为2的category
var c = categories.Single(c => Convert.ToInt32(c.Attribute("ID").Value) == 2);
//修改
c.SetElementValue("Name","HiXML");
//保存
xml.Save("e:/Northwind.xml");

最后可以在文件中看到Name被修改了。

 类似资料: