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被修改了。