从CRUD到高级功能:EF Core在.NET Core中全面应用(三)

打印 上一主题 下一主题

主题 909|帖子 909|积分 2727

目次
IQueryable使用
原生SQL使用
实体状态跟踪
全局查询筛选器
并发控制使用

IQueryable使用

        在EFCore中IQueryable是一个接口用于表示可查询的集合,它继承自IEnumerable但具有一些关键的区别,使得它在处理数据库查询时非常有用,平凡集合的版本(IEnumerable)是在内存中过滤(客户端评估),而IQueryable版本则是把查询操作翻译成SQL语句(服务器端评估):
接下来我们开始讲解其简单的应用,如下所示是两种使用的代码,根本上都一样,唯一区别在于两种在执行查询时的行为有所差异,如下所示:
   IQueryable:查询会被延迟执行并且它能将LINQ查询转换为SQL以便在数据库中执行,适合处理大规模数据。
IEnumerable:查询会立刻执行适适用于内存中的数据集合但无法进行数据库优化,因此性能较差。
  1. class Program
  2. {
  3.     static void Main(string[] args)
  4.     {
  5.         using (MyDbContext ctx = new MyDbContext())
  6.         {
  7.             IEnumerable<Class> classes1 = ctx.Classes.Include(t => t.Students);
  8.             IQueryable<Class> classes2 = ctx.Classes.Include(t => t.Students);
  9.             foreach (var c in classes1)
  10.             {
  11.                 Console.WriteLine($"Class Name: {c.Name}");
  12.                 foreach (var s in c.Students)
  13.                 {
  14.                     Console.WriteLine($"\tStudent Name: {s.Name}, Age: {s.Age}");
  15.                 }
  16.             }
  17.             foreach (var c in classes2)
  18.             {
  19.                 Console.WriteLine($"Class Name: {c.Name}");
  20.                 foreach (var s in c.Students)
  21.                 {
  22.                     Console.WriteLine($"\tStudent Name: {s.Name}, Age: {s.Age}");
  23.                 }
  24.             }
  25.         }
  26.     }
  27. }
复制代码
两者的区别如下所示:
特性IQueryableIEnumerable执行机遇延迟执行,查询会转化为 SQL 并在数据库中执行即时执行,查询在内存中执行数据泉源通常用于数据库查询,支持远程数据源通常用于内存中的集合性能优化利用数据库的索引和优化进行查询数据已经在内存中,无法优化常见用途用于数据库查询(如 Entity Framework)用于内存中的集合,如 List、Array 等   延迟执行:IQueryable支持延迟加载,执行查询的集适时只有在查询被执行时才会真正访问数据库或数据源,查询会在执行时转化为相应的SQL语句或者其他数据源的查询语言,只有在必要数据时查询才会被执行,如下所示:
  1. // 假设 context 是一个 DbContext 对象
  2. IQueryable<User> query = context.Users.Where(u => u.Age > 18);
  3. // 这个查询还没有执行,只有调用 ToList() 或其他方法时,查询才会发送到数据库执行
  4. var result = query.ToList(); // 执行查询,返回结果
复制代码
IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立刻执行只是可以被执行而已,对于IQueryable接口调用非闭幕方法的时间不会执行查询,而调用闭幕方法的时间则会立刻执行查询。
一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非闭幕方法,否则就是闭幕方法:
  1. // 终结方法:
  2. ToArrar()、ToList()、Min()、Max()、Count()等
  3. // 非终结方法:
  4. GroupBy()、OrderBy()、Include()、Skip()、Take()等
复制代码
IQueryable代表一个对数据库中数据进行查询的一个逻辑,这个查询是一个延迟查询,我们可以调用非闭幕方法向IQueryable中添加查询逻辑,当执行闭幕方法的时间才真正天生SQL语句来执行查询,可以实现从前要靠SQL拼接实现的动态查询逻辑。
   分页查询:IQueryable是用于支持延迟执行的LINQ查询的一种接口,在分页查询的实现中我们通常结合Skip()和Take()方法来控制结果集的起始位置和返回的数量,分页查询通过限制数据的加载量来提高查询效率,尤其在处理大数据集时,如下所示:
  1. // Skip() 方法: 用于跳过前N条记录,通常用来跳过前几页的数据。
  2. // Take() 方法: 用于返回指定数量的记录。
  3. public IQueryable<T> GetPagedResults<T>(IQueryable<T> query, int pageNumber, int pageSize)
  4. {
  5.     return query.Skip((pageNumber - 1) * pageSize)  // 跳过前几页的记录
  6.                 .Take(pageSize);                  // 获取当前页的记录
  7. }
复制代码
这里我们通过分页去查询我们数据库当中的门生表,以每页2条数据为准,如下所示:

   获取数据: 从数据库获取数据主要分为以下两种:
  1)DataReader:分批从数据库服务器读取数据,内存占用小、DB连接占用时间长。
  2)DataTable:把所有数据都一次性从数据库服务器都加载到客户端内存中,内存占用大节省DB连接。
  我们可以通过insert into select多插入一些数据,然后加上Delay/Sleep的遍历IQueryable,在遍历执行的过程中停止MySQL服务器,可以验证IQueryable内部就是在调用DataReader,其优点是节省客户端内存,缺点是如果处理的慢就会长时间占用连接。
如果想IQueryable一次性加载数据到内存中,可以用IQueryable的ToArrary()、ToArrayAsync()、ToList()、ToListAsync()等方法,等ToArray()执行完毕再断服务器试一下。
原生SQL使用

尽管EF Core已经非常强大,但是仍然存在着无法被写成尺度EF Core调用方法的SQL语句,少数情况下仍然必要写原生的SQL,这里有三种情况:非查询语句、实体查询、任意SQL查询,如下:
   非查询语句:这里我们可以通过ExecuteSqlInterpolatedAsync进行字符串插值拼接,其会自动处理SQL参数,如下所示:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             string description = "test";
  8.             using (MyDbContext ctx = new MyDbContext())
  9.             {
  10.                 await ctx.Database.ExecuteSqlInterpolatedAsync(
  11.                     $"INSERT INTO T_Class (Name, Description) SELECT Name, {description} FROM T_Students WHERE Id > 2"
  12.                 );
  13.             }
  14.         }
  15.     }
  16. }
复制代码
得到的结果如下所示:

字符串内插如果赋值给string变量,就是字符串拼接,字符串内插如果赋值FormattableString变量,编译器就会构造FormattableString对象,该对象会进行参数化SQL处理,一定程度上防止了SQL注入攻击,如下所示:

   实体查询:如果要执行的原生SQL是一个查询语句并且查询的结果也能对应一个实体,就可以调用对应实体的DbSet的FromSqlInterpolated()方法来执行一个查询的SQL语句,同样使用字符串内插来传递参数,如下所示:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             string description = "%test%";
  8.             using (MyDbContext ctx = new MyDbContext())
  9.             {
  10.                 var queryable = ctx.Classes.FromSqlInterpolated(
  11.                     @$"select * from T_Class where Description like {description}"
  12.                 );
  13.                 foreach (var item in queryable)
  14.                 {
  15.                     Console.WriteLine(item.Id+" "+item.Name+" "+item.Description);
  16.                 }
  17.             }
  18.         }
  19.     }
  20. }
复制代码
查询的结果如下所示:

        FromSqlInterpolated()方法的返回值是IQueryable类型的,因此我们可以在执行IQueryable之前对IQueryable进行进一步的处理,把只能用原生SQL语句写的逻辑用FromSqlInterpolated()去执行,然后把分页、分组、二次过滤、排序、Include等其他逻辑尽可能仍然使用EF Core的尺度去操作实现。
范围性:SQL查询必须返回实体类型对应数据库表的所有列,结果集中的列必须与属性映射到的列名称匹配,只能单表查询而不能使用join语句进行关联查询,但是可在查询后面使用Include来进行关联数据的获取。
   任意SQL查询:这里使用DbConnection来获取数据库的连接对象,而不借用EF Core天生的SQL进行查询,如下所示:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             using (MyDbContext ctx = new MyDbContext())
  8.             {
  9.                 DbConnection conn = ctx.Database.GetDbConnection(); // 获取数据库连接
  10.                 if (conn.State != System.Data.ConnectionState.Open)
  11.                 {
  12.                     await conn.OpenAsync(); // 打开数据库连接
  13.                 }
  14.                 using (DbCommand cmd = conn.CreateCommand())
  15.                 {
  16.                     cmd.CommandText = "select * from T_Class where Description like '%test%'";
  17.                     using (DbDataReader reader = await cmd.ExecuteReaderAsync())
  18.                     {
  19.                         while (await reader.ReadAsync())
  20.                         {
  21.                             Console.WriteLine(reader["Id"] + " " + reader["Name"] + " " + reader["Description"]);
  22.                         }
  23.                     }
  24.                 }
  25.             }
  26.         }
  27.     }
  28. }
复制代码
最终得到的结果如下所示:

   总结:一般Ling操作就够了只管不用写原生SQL,非查询SQL用ExecuteSqllnterpolated(),针对实体的SQL查询用FromSqllnterpolated(),复杂SQL查询用ado.net的方式或者Dapper等
  实体状态跟踪

实体状态跟踪:是指框架如何追踪实体对象在其生命周期中的状态变革,这些状态资助EFCore确定如何与数据库进行交互,以便在保存更改时精确天生SQL查询。
EFCore使用一个叫做Change Tracker的机制来跟踪实体的状态,确保数据库中的数据与应用程序中的实体对象保持同等,其实体对象主要有以下五种状态:
   已添加(Added):实体是新创建的尚未保存到数据库中,EFCore将会把这些实体作为新的记录插入到数据库中,如下:
  1. var newStudent = new Student { Name = "John" };
  2. dbContext.Students.Add(newStudent);  // 设置状态为 Added
复制代码
未改变(Unchanged):实体的状态没有发生变革,EFCore不会对其天生任何SQL语句,实体的属性值与数据库中的数据同等,如下:
  1. var student = dbContext.Students.Find(1);  // 假设没有更改属性
  2. // 没有显式的修改,实体状态保持 Unchanged
复制代码
已修改(Modified):实体已存在并且它的属性值发生了变革,EFCore会在保存更改时天生一个 update sql语句将这些更改同步到数据库中,如下:
  1. var student = dbContext.Students.Find(1);
  2. student.Name = "Jane";  // 设置状态为 Modified
  3. dbContext.Students.Update(student);
复制代码
已删除(Deleted):实体被标志为删除并且当保存更改时,EFCore会天生deleted sql语句,必要显式调用删除操作来设置实体为删除状态,如下:
  1. var student = dbContext.Students.Find(1);
  2. dbContext.Students.Remove(student);  // 设置状态为 Deleted
复制代码
已分离(Detached):实体不再被EFCore上下文跟踪,通常是由于实体从上下文中移除或在数据库之外创建,如果试图对这些实体做更改EFCore不会追踪它们,因此不会执行任何操作,如下:
  1. var student = dbContext.Students.Find(1);
  2. dbContext.Entry(student).State = EntityState.Detached;  // 设置状态为 Detached
复制代码
如果想查看实体状态,这里我们可以使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntry,EntityEntry类的State属性代表实体的状态,通过DebugView.LongView属性可以看到实体的变革信息,如下所示:
  1. var student = dbContext.Students.Find(1);
  2. var entry = dbContext.Entry(student);
  3. Console.WriteLine(entry.State);  // 输出实体的当前状态(例如 Added, Modified, Unchanged, 等)
复制代码
DbContext会根据跟踪的实体的状态,在SaveChanges()的时间,根据实体状态的差异天生update、delete、insert等sql语句来把内存中实体的变革更新到数据库中。
默认情况下EFCore会追踪所有实体的状态,如果不想追踪某些实体的状态可以使用AsNoTracking方法禁用状态跟踪,这通常用于查询操作以提高性能:
  1. var students = dbContext.Students.AsNoTracking().ToList();
复制代码
如果我们确认我们的操作只会查询不会被修改、删除等,那么这里我们就可以使用AsNoTracking()方法来提升性能,降低内存占用,如下所示:

全局查询筛选器

EFCore中的全局查询筛选器是一种用于在整个应用程序中自动应用的查询条件,它答应在查询时自动对数据进行过滤,确保数据的同等性和安全性而无需在每个查询中显式添加筛选条件,常用的场景如下所示:
   软删除:通过全局查询筛选器确保删除的记录在查询中不被返回,而无需显式地为每个查询添加where子句。
  假设我们有一个Product实体,其中包罗一个IsDeleted属性用来标志某个产品是否被删除,我们可以在OnModelCreating方法中为Product实体设置全局查询筛选器确保查询时自动排除已删除的产品,如下所示:
  1. public class ApplicationDbContext : DbContext
  2. {
  3.     public DbSet<Product> Products { get; set; }
  4.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  5.     {
  6.         // 为 Product 实体添加全局查询筛选器
  7.         modelBuilder.Entity<Product>()
  8.             .HasQueryFilter(p => !p.IsDeleted);
  9.     }
  10. }
复制代码

如果想查询已经删除的数据,并且我们也已经设置了全局忽略删掉数据的过滤器,这里我们可以在要查询删除数据的地方添加上忽略全局过滤器的函数,如下所示:

   多租户:为每个租户添加自动的筛选条件,确保每个租户只访问本身的数据。
  这里可以使用多个条件创建复杂的筛选器,比方如果有一个TenantId字段来支持多租户功能,可以根据租户ID创建全局筛选器,这种方式确保了在每次查询Product实体时都会自动根据当前租户的ID过滤数据,如下所示:
  1. modelBuilder.Entity<Product>()
  2.     .HasQueryFilter(p => p.TenantId == currentTenantId);
复制代码
并发控制使用

并发控制用于确保多个用户或多个历程对数据库进行并发访问时,不会产生数据辩论或不同等的问题,避免多个用户同时操作资源造成的并发辩论问题,比方:统计点击量。最好的解决方案就是:非数据库解决方案。如果从数据库层面来处理的话,EFCore支持如下两种主要的并发控制机制:
   悲观并发控制:假设并发辩论的可能性较大,因此会通过锁定数据来防止其他用户修改正在处理的数据。在悲观并发控制中EFCore支持使用数据库的锁机制(如行级锁)来实现并发控制。
  悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源,EF Core没有封装悲观并发控制的使用,必要开辟人员编写原生SQL语句来使用悲观并发控制,差异数据库的语法不一样。
这里我们通过一个占据房子书写房名的案例进行讲解,这里通过MySQL方案来实现,如下:
  1. class House {
  2.     public long Id {get; set;}
  3.     public string Name {get; set;}
  4.     public string Owner {get; set;}
  5. }
  6. // MySQL方案
  7. select * from T_Houses Where Id = 1 for update
复制代码
如果有其他的查询操作也使用for update来查询id=1的这条数据的话,那么查询就会被挂起直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继承执行,如下所示:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             Console.WriteLine("请输出您的名字");
  8.             string name = Console.ReadLine();
  9.             using (MyDbContext ctx = new MyDbContext())
  10.             {
  11.                 var houses = ctx.Houses.Single(h => h.Id == 1);
  12.                 if (!string.IsNullOrEmpty(houses.Owner))
  13.                 {
  14.                     if (houses.Owner == name)
  15.                     {
  16.                         Console.WriteLine("恭喜你,你已经买过了");
  17.                     } else
  18.                     {
  19.                         Console.WriteLine($"房子已经被{houses.Owner}买走了");
  20.                     }
  21.                     return;
  22.                 }
  23.                 houses.Owner = name;
  24.                 Thread.Sleep(10000);
  25.                 Console.WriteLine("恭喜你,你已经买到了房子");
  26.                 ctx.SaveChanges();
  27.                 Console.ReadKey();
  28.             }
  29.         }
  30.     }
  31. }
复制代码
上面代码,如果我们不进行并发控制的话,下面如果我们同时执行两个人抢房子,两者都会出现买到了房子,但是现实上房子末了照旧被Hack买到了,如下

接下来我们在程序中添加事件操作,如下所示:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             Console.WriteLine("请输出您的名字");
  8.             string name = Console.ReadLine();
  9.             using (MyDbContext ctx = new MyDbContext())
  10.             using (var tx = ctx.Database.BeginTransaction()) // 开启事务
  11.             {
  12.                 Console.WriteLine(DateTime.Now+"正在为您查询房源信息");
  13.                 var houses = ctx.Houses.FromSqlInterpolated($"select * from T_House where Id = 1 for update").Single();
  14.                 Console.WriteLine(DateTime.Now+"房源信息完毕");
  15.                 if (!string.IsNullOrEmpty(houses.Owner))
  16.                 {
  17.                     if (houses.Owner == name)
  18.                     {
  19.                         Console.WriteLine("恭喜你,你已经买过了");
  20.                     } else
  21.                     {
  22.                         Console.WriteLine($"房子已经被{houses.Owner}买走了");
  23.                     }
  24.                     Console.ReadKey();
  25.                     return;
  26.                 }
  27.                 houses.Owner = name;
  28.                 Thread.Sleep(10000);
  29.                 Console.WriteLine("恭喜你,你已经买到了房子");
  30.                 ctx.SaveChanges();
  31.                 Console.WriteLine("正在为您保存房源信息");
  32.                 tx.Commit(); // 提交事务
  33.                 Console.ReadKey();
  34.             }
  35.         }
  36.     }
  37. }
复制代码
得到的结果如下所示,可以看到我们的并发操作以及处理好了:

总结: 悲观并发控制的使用比力简单,锁是独占排他的,如果系统并发量很大的话会严峻影响性能,如果使用不当的话乃至会导致死锁,这点尤为重要,以是要根据现实情况进行选择使用。
   乐观并发控制:假设并发辩论较少,因此答应多个操作同时对数据进行修改,在提交更改时EFCore会检查数据是否被其他操作修改过,如果数据已被修改当前操作会被拒绝并抛出 DbUpdateConcurrencyException非常。
  举例:当update的时间如果数据库中的Owner值已经被其他操作者更新为其他值了,那么where语句的值就会被设为false,因此这个update语句影响的行数就是0,EFCore就知道发生并发辩论了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException非常,如下所示:
1)标志并发字段:在实体类或设置类中标志一个字段作为并发标志,通常是一个时间戳字段或者是一个列,EFCore使用该字段来检测数据是否在并发操作期间被修改过,如下所示:
  1. namespace test
  2. {
  3.     internal class HouseConfig : IEntityTypeConfiguration<House>
  4.     {
  5.         public void Configure(EntityTypeBuilder<House> builder)
  6.         {
  7.             builder.ToTable("T_House");
  8.             builder.Property(x => x.Name).IsRequired();
  9.             builder.Property(x => x.Owner).IsConcurrencyToken(); // 并发标记
  10.         }
  11.     }
  12. }
复制代码
2)处理并发辩论:当发生并发辩论时,EFCore会抛出DbUpdateConcurrencyException非常,可以捕获此非常并根据需求进行处理,好比:
  1. namespace Program
  2. {
  3.     class Program
  4.     {
  5.         static async Task Main(string[] args)
  6.         {
  7.             Console.WriteLine("请输出您的名字");
  8.             string name = Console.ReadLine();
  9.             using (MyDbContext ctx = new MyDbContext())
  10.             {
  11.                 Console.WriteLine(DateTime.Now + "正在为您查询房源信息");
  12.                 var houses = ctx.Houses.Single(h => h.Id == 1);
  13.                 Console.WriteLine(DateTime.Now + "房源信息完毕");
  14.                 if (!string.IsNullOrEmpty(houses.Owner))
  15.                 {
  16.                     if (houses.Owner == name)
  17.                     {
  18.                         Console.WriteLine("恭喜你,你已经买过了");
  19.                     }
  20.                     else
  21.                     {
  22.                         Console.WriteLine($"房子已经被{houses.Owner}买走了");
  23.                     }
  24.                     Console.ReadKey();
  25.                     return;
  26.                 }
  27.                 houses.Owner = name;
  28.                 Console.WriteLine("恭喜你,你已经买到了房子");
  29.                 try
  30.                 {
  31.                     ctx.SaveChanges();
  32.                 }
  33.                 catch (DbUpdateConcurrencyException ex)
  34.                 {
  35.                     Console.WriteLine("并发访问冲突");
  36.                     var entry = ex.Entries.Single();
  37.                     string newValue = entry.GetDatabaseValues().GetValue<string>("Owner");
  38.                     Console.WriteLine($"房子已经被{newValue}买走了");
  39.                 }
  40.                 Console.ReadKey();
  41.             }
  42.         }
  43.     }
  44. }
复制代码
最终呈现的效果如下所示:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

反转基因福娃

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表