安徽建设工程造价信息网站,网站没有在工信部备案,wordpress找不到根目录,网站建设图库目录
IQueryable使用
原生SQL使用
实体状态跟踪
全局查询筛选器
并发控制使用 IQueryable使用 在EFCore中IQueryable是一个接口用于表示可查询的集合#xff0c;它继承自IEnumerable但具有一些关键的区别#xff0c;使得它在处理数据库查询时非常有用#xff0c;普通集…目录
IQueryable使用
原生SQL使用
实体状态跟踪
全局查询筛选器
并发控制使用 IQueryable使用 在EFCore中IQueryable是一个接口用于表示可查询的集合它继承自IEnumerable但具有一些关键的区别使得它在处理数据库查询时非常有用普通集合的版本(IEnumerable)是在内存中过滤(客户端评估)而IQueryable版本则是把查询操作翻译成SQL语句(服务器端评估)
接下来我们开始讲解其简单的应用如下所示是两种使用的代码基本上都一样唯一区别在于两种在执行查询时的行为有所不同如下所示 IQueryable查询会被延迟执行并且它能将LINQ查询转换为SQL以便在数据库中执行适合处理大规模数据。 IEnumerable查询会立即执行适合用于内存中的数据集合但无法进行数据库优化因此性能较差。 class Program
{static void Main(string[] args){using (MyDbContext ctx new MyDbContext()){IEnumerableClass classes1 ctx.Classes.Include(t t.Students);IQueryableClass classes2 ctx.Classes.Include(t t.Students);foreach (var c in classes1){Console.WriteLine($Class Name: {c.Name});foreach (var s in c.Students){Console.WriteLine($\tStudent Name: {s.Name}, Age: {s.Age});}}foreach (var c in classes2){Console.WriteLine($Class Name: {c.Name});foreach (var s in c.Students){Console.WriteLine($\tStudent Name: {s.Name}, Age: {s.Age});}}}}
}
两者的区别如下所示
特性IQueryableIEnumerable执行时机延迟执行查询会转化为 SQL 并在数据库中执行即时执行查询在内存中执行数据来源通常用于数据库查询支持远程数据源通常用于内存中的集合性能优化利用数据库的索引和优化进行查询数据已经在内存中无法优化常见用途用于数据库查询如 Entity Framework用于内存中的集合如 List、Array 等 延迟执行IQueryable支持延迟加载执行查询的集合时只有在查询被执行时才会真正访问数据库或数据源查询会在执行时转化为相应的SQL语句或者其他数据源的查询语言只有在需要数据时查询才会被执行如下所示 // 假设 context 是一个 DbContext 对象
IQueryableUser query context.Users.Where(u u.Age 18);
// 这个查询还没有执行只有调用 ToList() 或其他方法时查询才会发送到数据库执行
var result query.ToList(); // 执行查询返回结果
IQueryable只是代表一个“可以放到数据库服务器去执行的查询”它没有立刻执行只是可以被执行而已对于IQueryable接口调用非终结方法的时候不会执行查询而调用终结方法的时候则会立刻执行查询。
一个方法的返回值类型如果是IQueryable类型那么这个方法一般就是非终结方法否则就是终结方法
// 终结方法
ToArrar()、ToList()、Min()、Max()、Count()等// 非终结方法
GroupBy()、OrderBy()、Include()、Skip()、Take()等
IQueryable代表一个对数据库中数据进行查询的一个逻辑这个查询是一个延迟查询我们可以调用非终结方法向IQueryable中添加查询逻辑当执行终结方法的时候才真正生成SQL语句来执行查询可以实现以前要靠SQL拼接实现的动态查询逻辑。 分页查询IQueryable是用于支持延迟执行的LINQ查询的一种接口在分页查询的实现中我们通常结合Skip()和Take()方法来控制结果集的起始位置和返回的数量分页查询通过限制数据的加载量来提高查询效率尤其在处理大数据集时如下所示 // Skip() 方法: 用于跳过前N条记录通常用来跳过前几页的数据。
// Take() 方法: 用于返回指定数量的记录。
public IQueryableT GetPagedResultsT(IQueryableT query, int pageNumber, int pageSize)
{return query.Skip((pageNumber - 1) * pageSize) // 跳过前几页的记录.Take(pageSize); // 获取当前页的记录
}
这里我们通过分页去查询我们数据库当中的学生表以每页2条数据为准如下所示 获取数据 从数据库获取数据主要分为以下两种 1DataReader分批从数据库服务器读取数据内存占用小、DB连接占用时间长。 2DataTable把所有数据都一次性从数据库服务器都加载到客户端内存中内存占用大节省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参数如下所示 namespace Program
{class Program{static async Task Main(string[] args){string description test;using (MyDbContext ctx new MyDbContext()){await ctx.Database.ExecuteSqlInterpolatedAsync($INSERT INTO T_Class (Name, Description) SELECT Name, {description} FROM T_Students WHERE Id 2);}}}
}
得到的结果如下所示 字符串内插如果赋值给string变量就是字符串拼接字符串内插如果赋值FormattableString变量编译器就会构造FormattableString对象该对象会进行参数化SQL处理一定程度上防止了SQL注入攻击如下所示 实体查询如果要执行的原生SQL是一个查询语句并且查询的结果也能对应一个实体就可以调用对应实体的DbSet的FromSqlInterpolated()方法来执行一个查询的SQL语句同样使用字符串内插来传递参数如下所示 namespace Program
{class Program{static async Task Main(string[] args){string description %test%;using (MyDbContext ctx new MyDbContext()){var queryable ctx.Classes.FromSqlInterpolated($select * from T_Class where Description like {description});foreach (var item in queryable){Console.WriteLine(item.Id item.Name item.Description);}}}}
}
查询的结果如下所示 FromSqlInterpolated()方法的返回值是IQueryable类型的因此我们可以在执行IQueryable之前对IQueryable进行进一步的处理把只能用原生SQL语句写的逻辑用FromSqlInterpolated()去执行然后把分页、分组、二次过滤、排序、Include等其他逻辑尽可能仍然使用EF Core的标准去操作实现。
局限性SQL查询必须返回实体类型对应数据库表的所有列结果集中的列必须与属性映射到的列名称匹配只能单表查询而不能使用join语句进行关联查询但是可在查询后面使用Include来进行关联数据的获取。 任意SQL查询这里使用DbConnection来获取数据库的连接对象而不借用EF Core生成的SQL进行查询如下所示 namespace Program
{class Program{static async Task Main(string[] args){using (MyDbContext ctx new MyDbContext()){DbConnection conn ctx.Database.GetDbConnection(); // 获取数据库连接if (conn.State ! System.Data.ConnectionState.Open){await conn.OpenAsync(); // 打开数据库连接}using (DbCommand cmd conn.CreateCommand()){cmd.CommandText select * from T_Class where Description like %test%;using (DbDataReader reader await cmd.ExecuteReaderAsync()){while (await reader.ReadAsync()){Console.WriteLine(reader[Id] reader[Name] reader[Description]);}}}}}}
}
最终得到的结果如下所示 总结一般Ling操作就够了尽量不用写原生SQL非查询SQL用ExecuteSqllnterpolated()针对实体的SQL查询用FromSqllnterpolated()复杂SQL查询用ado.net的方式或者Dapper等 实体状态跟踪
实体状态跟踪是指框架如何追踪实体对象在其生命周期中的状态变化这些状态帮助EFCore确定如何与数据库进行交互以便在保存更改时正确生成SQL查询。
EFCore使用一个叫做Change Tracker的机制来跟踪实体的状态确保数据库中的数据与应用程序中的实体对象保持一致其实体对象主要有以下五种状态 已添加(Added)实体是新创建的尚未保存到数据库中EFCore将会把这些实体作为新的记录插入到数据库中如下 var newStudent new Student { Name John };
dbContext.Students.Add(newStudent); // 设置状态为 Added 未改变(Unchanged)实体的状态没有发生变化EFCore不会对其生成任何SQL语句实体的属性值与数据库中的数据一致如下 var student dbContext.Students.Find(1); // 假设没有更改属性
// 没有显式的修改实体状态保持 Unchanged 已修改(Modified)实体已存在并且它的属性值发生了变化EFCore会在保存更改时生成一个 update sql语句将这些更改同步到数据库中如下 var student dbContext.Students.Find(1);
student.Name Jane; // 设置状态为 Modified
dbContext.Students.Update(student); 已删除(Deleted)实体被标记为删除并且当保存更改时EFCore会生成deleted sql语句需要显式调用删除操作来设置实体为删除状态如下 var student dbContext.Students.Find(1);
dbContext.Students.Remove(student); // 设置状态为 Deleted 已分离(Detached)实体不再被EFCore上下文跟踪通常是因为实体从上下文中移除或在数据库之外创建如果试图对这些实体做更改EFCore不会追踪它们因此不会执行任何操作如下 var student dbContext.Students.Find(1);
dbContext.Entry(student).State EntityState.Detached; // 设置状态为 Detached 如果想查看实体状态这里我们可以使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntryEntityEntry类的State属性代表实体的状态通过DebugView.LongView属性可以看到实体的变化信息如下所示
var student dbContext.Students.Find(1);
var entry dbContext.Entry(student);
Console.WriteLine(entry.State); // 输出实体的当前状态例如 Added, Modified, Unchanged, 等
DbContext会根据跟踪的实体的状态在SaveChanges()的时候根据实体状态的不同生成update、delete、insert等sql语句来把内存中实体的变化更新到数据库中。
默认情况下EFCore会追踪所有实体的状态如果不想追踪某些实体的状态可以使用AsNoTracking方法禁用状态跟踪这通常用于查询操作以提高性能
var students dbContext.Students.AsNoTracking().ToList();
如果我们确认我们的操作只会查询不会被修改、删除等那么这里我们就可以使用AsNoTracking()方法来提升性能降低内存占用如下所示 全局查询筛选器
EFCore中的全局查询筛选器是一种用于在整个应用程序中自动应用的查询条件它允许在查询时自动对数据进行过滤确保数据的一致性和安全性而无需在每个查询中显式添加筛选条件常用的场景如下所示 软删除通过全局查询筛选器确保删除的记录在查询中不被返回而无需显式地为每个查询添加where子句。 假设我们有一个Product实体其中包含一个IsDeleted属性用来标记某个产品是否被删除我们可以在OnModelCreating方法中为Product实体配置全局查询筛选器确保查询时自动排除已删除的产品如下所示
public class ApplicationDbContext : DbContext
{public DbSetProduct Products { get; set; }protected override void OnModelCreating(ModelBuilder modelBuilder){// 为 Product 实体添加全局查询筛选器modelBuilder.EntityProduct().HasQueryFilter(p !p.IsDeleted);}
} 如果想查询已经删除的数据并且我们也已经配置了全局忽略删掉数据的过滤器这里我们可以在要查询删除数据的地方添加上忽略全局过滤器的函数如下所示 多租户为每个租户添加自动的筛选条件确保每个租户只访问自己的数据。 这里可以使用多个条件创建复杂的筛选器例如如果有一个TenantId字段来支持多租户功能可以根据租户ID创建全局筛选器这种方式确保了在每次查询Product实体时都会自动根据当前租户的ID过滤数据如下所示
modelBuilder.EntityProduct().HasQueryFilter(p p.TenantId currentTenantId);
并发控制使用
并发控制用于确保多个用户或多个进程对数据库进行并发访问时不会产生数据冲突或不一致的问题避免多个用户同时操作资源造成的并发冲突问题例如统计点击量。最好的解决方案就是非数据库解决方案。如果从数据库层面来处理的话EFCore支持如下两种主要的并发控制机制 悲观并发控制假设并发冲突的可能性较大因此会通过锁定数据来防止其他用户修改正在处理的数据。在悲观并发控制中EFCore支持使用数据库的锁机制如行级锁来实现并发控制。 悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定确保同时只有一个使用者操作被锁定的资源EF Core没有封装悲观并发控制的使用需要开发人员编写原生SQL语句来使用悲观并发控制不同数据库的语法不一样。
这里我们通过一个占据房子书写房名的案例进行讲解这里通过MySQL方案来实现如下
class House {public long Id {get; set;}public string Name {get; set;}public string Owner {get; set;}
}
// MySQL方案
select * from T_Houses Where Id 1 for update
如果有其他的查询操作也使用for update来查询id1的这条数据的话那么查询就会被挂起直到针对这条数据的更新操作完成从而释放这个行锁代码才会继续执行如下所示
namespace Program
{class Program{static async Task Main(string[] args){Console.WriteLine(请输出您的名字);string name Console.ReadLine();using (MyDbContext ctx new MyDbContext()){var houses ctx.Houses.Single(h h.Id 1);if (!string.IsNullOrEmpty(houses.Owner)){if (houses.Owner name){Console.WriteLine(恭喜你你已经买过了);} else{Console.WriteLine($房子已经被{houses.Owner}买走了);}return;}houses.Owner name;Thread.Sleep(10000);Console.WriteLine(恭喜你你已经买到了房子);ctx.SaveChanges();Console.ReadKey();}}}
}
上面代码如果我们不进行并发控制的话下面如果我们同时执行两个人抢房子两者都会出现买到了房子但是实际上房子最后还是被Hack买到了如下 接下来我们在程序中添加事务操作如下所示
namespace Program
{class Program{static async Task Main(string[] args){Console.WriteLine(请输出您的名字);string name Console.ReadLine();using (MyDbContext ctx new MyDbContext())using (var tx ctx.Database.BeginTransaction()) // 开启事务{Console.WriteLine(DateTime.Now正在为您查询房源信息);var houses ctx.Houses.FromSqlInterpolated($select * from T_House where Id 1 for update).Single();Console.WriteLine(DateTime.Now房源信息完毕);if (!string.IsNullOrEmpty(houses.Owner)){if (houses.Owner name){Console.WriteLine(恭喜你你已经买过了);} else{Console.WriteLine($房子已经被{houses.Owner}买走了);}Console.ReadKey();return;}houses.Owner name;Thread.Sleep(10000);Console.WriteLine(恭喜你你已经买到了房子);ctx.SaveChanges();Console.WriteLine(正在为您保存房源信息);tx.Commit(); // 提交事务Console.ReadKey();}}}
}
得到的结果如下所示可以看到我们的并发操作以及处理好了 总结 悲观并发控制的使用比较简单锁是独占排他的如果系统并发量很大的话会严重影响性能如果使用不当的话甚至会导致死锁这点尤为重要所以要根据实际情况进行选择使用。 乐观并发控制假设并发冲突较少因此允许多个操作同时对数据进行修改在提交更改时EFCore会检查数据是否被其他操作修改过如果数据已被修改当前操作会被拒绝并抛出 DbUpdateConcurrencyException异常。 举例当update的时候如果数据库中的Owner值已经被其他操作者更新为其他值了那么where语句的值就会被设为false因此这个update语句影响的行数就是0EFCore就知道发生并发冲突了因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常如下所示
1标记并发字段在实体类或配置类中标记一个字段作为并发标记通常是一个时间戳字段或者是一个列EFCore使用该字段来检测数据是否在并发操作期间被修改过如下所示
namespace test
{internal class HouseConfig : IEntityTypeConfigurationHouse{public void Configure(EntityTypeBuilderHouse builder){builder.ToTable(T_House);builder.Property(x x.Name).IsRequired();builder.Property(x x.Owner).IsConcurrencyToken(); // 并发标记}}
}
2处理并发冲突当发生并发冲突时EFCore会抛出DbUpdateConcurrencyException异常可以捕获此异常并根据需求进行处理比如
namespace Program
{class Program{static async Task Main(string[] args){Console.WriteLine(请输出您的名字);string name Console.ReadLine();using (MyDbContext ctx new MyDbContext()){Console.WriteLine(DateTime.Now 正在为您查询房源信息);var houses ctx.Houses.Single(h h.Id 1);Console.WriteLine(DateTime.Now 房源信息完毕);if (!string.IsNullOrEmpty(houses.Owner)){if (houses.Owner name){Console.WriteLine(恭喜你你已经买过了);}else{Console.WriteLine($房子已经被{houses.Owner}买走了);}Console.ReadKey();return;}houses.Owner name;Console.WriteLine(恭喜你你已经买到了房子);try{ctx.SaveChanges();}catch (DbUpdateConcurrencyException ex){Console.WriteLine(并发访问冲突);var entry ex.Entries.Single();string newValue entry.GetDatabaseValues().GetValuestring(Owner);Console.WriteLine($房子已经被{newValue}买走了);}Console.ReadKey();}}}
}
最终呈现的效果如下所示