EFCore学习1
《1》数据更新方法
//方法1批量更新数据库数据,直接使用SQL语句 ctx.Database.ExecuteSql($"UPDATE [T_Books] SET [Price] = [Price] + 2"); //方法2 EF Core仍会为每个本书发送 UPDATE 语句,并且数据库必须单独执行每个语句 //并且要先查询,后修改 var bk = ctx.Books.Where(b => b.Price > 10); foreach (var bi in bk) { bi.Price = bi.Price + 1; } //方法3 从EF Core7.0开始可以使用ExecuteUpdate 和 ExecuteDelete 方法更高效地执行相同的操作 ctx.Books.ExecuteUpdate(s => s.SetProperty(e => e.Price, e => e.Price + 1000)); await ctx.SaveChangesAsync();
//下面是批量删除 ctx.Articles.Where(b=>b.Id>4).ExecuteDelete();
《2》两种配置方式
1、Data Annotation把配置以特性(Annotation)的形式标注在实体类中。
[Table("T_Books")]
Public class Book
{
}
优点:简单;缺点:耦合度高,不灵活
2、Fluent API
builder.ToTable("T_Books");
把配置写到单独的配置类中。
缺点:复杂;优点:解耦
《3》EFCore中查看SQL语句方法
方法1:标准日志
private static ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole());
然后在OnConfig方法最后一行添加下面语句,这样就输出到控制台里面了。
optionsBuilder.UseLoggerFactory(loggerFactory);
方法2:简单日志
然后在OnConfig方法最后一行添加下面语句,这样就输出到控制台里面了。
只是用起来简单,输出的数据比较多需要自己过滤。
optionsBuilder.LogTo(msg=> { Console.WriteLine(msg); });
方法3:ToQuerystring
《4》EFCore关系数据库中,一个表中的多个外键数据,指向另外一张表出现异常如下面所示
Leave类中有两个User实体一个是Requester,另外是Approver
public User Requester { get; set; }
public User Approver { get; set; }
将这两个实体设定成User外键时会提示下面错误,代码如下
public void Configure(EntityTypeBuilder<Leave> builder) { builder.ToTable("T_Leaves"); builder.HasOne<User>(a => a.Requester).WithMany().IsRequired(); builder.HasOne<User>(a => a.Approver).WithMany(); }
下面是提示错误
将 FOREIGN KEY 约束 'FK_T_Leaves_T_User_RequesterId' 引入表 'T_Leaves' 可能会导致循环或多重级联路径。请指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。 无法创建约束或索引。
后面在第二个实体后面添加.OnDelete(DeleteBehavior.Restrict);后正常下面是更改后的代码
public void Configure(EntityTypeBuilder<Leave> builder) { builder.ToTable("T_Leaves"); builder.HasOne<User>(a => a.Requester).WithMany().IsRequired(); builder.HasOne<User>(a => a.Approver).WithMany().OnDelete(DeleteBehavior.Restrict); }
《5》自引用的组织结构树报错
.NET 8.0框架
在生成“T_OrtUnits”表时报错误,代码如下图所示
public void Configure(EntityTypeBuilder<OrgUnit> builder) { builder.ToTable("T_OrgUnits"); builder.Property(o => o.Name).IsUnicode().IsRequired().HasMaxLength(50); builder.HasOne<OrgUnit>(o => o.Parent).WithMany(o => o.Childrens); //根节点没有Parent,因此这个关系不能修饰为“不可为空” }
错误提示如下:
将 FOREIGN KEY 约束 'FK_T_OrgUnits_T_OrgUnits_ParentId' 引入表 'T_OrgUnits' 可能会导致循环或多重级联路径。请指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。
无法创建约束或索引。请参阅前面的错误。
下面更改如下正常了,或者要添加.OnDelete(DeleteBehavior.Restrict);
public void Configure(EntityTypeBuilder<OrgUnit> builder) { builder.ToTable("T_OrgUnits"); builder.Property(o => o.Name).IsUnicode().IsRequired().HasMaxLength(50); builder.HasOne<OrgUnit>(o => o.Parent).WithMany(o => o.Childrens).IsRequired(false); //根节点没有父节点,所以此关系不能修饰为”不可为空“ }
《6》IQueryable和IEnumerable区别
//服务器端评估,所有数据查询处理都在服务端执行 IQueryable<Article> arts = ctx.Articles.Where(a => a.Title.Contains("微软")); foreach (Article article in arts) { Console.WriteLine(article.Title); } //客户端评估,只执行最简单查询,其它数据处理操作在本机内存中执行 IEnumerable<Article> articles1 = ctx.Articles; IEnumerable<Article> articles = articles1.Where(a => a.Title.Contains("微软")); foreach (var article in articles) { Console.WriteLine(article.Title); }
《6》IQueryable复用
IQueryable要用终结语句才会执行数据库查询和处理操作,利用这个特性可以进行IQueryable查询条件复用
//IQueryable的复用,第一条是复用语句,第二条和第三条执行数据库查询是都会加上第一条语句条件 IQueryable<Article> articles = ctx.Articles.Where(b => b.Title.Contains("微软")); Console.WriteLine(articles.Count()); Console.WriteLine(articles.Max(b => b.Id));
上面这段代码执行了两次数据库查询操作,第一条是复用语句,生成的数据库语句如下
//第二行代码生成数据库语句 SELECT MAX(t.Id) FROM T_Articles AS t WHERE t.Title LIKE N'%微软%'), //第三行代码生成的数据库语句 SELECT MAX([t].[Id]) FROM [T_Articles] AS [t] WHERE [t].[Title] LIKE N'%微软%'
从上面生成的数据库语句中可以看出,两段SQL语句都执行第一条的复用语句
《7》IQueryable读取数据库的方式
1:DataReader:分批从数据库服务器读取数据。内存占用小,DB连接占用时间长;
2:DataTable:把所有数据都一次性从数据库服务器中加载到客户端内存中。内存占用大,节省DB连接。
IQueryable内部就是在调用DataReader。缺点:如果处理的慢,会长时间占用连接。
要想一次性加载数据到内存:用IQueryable的ToArray(),ToArrayAsync(),ToList(),ToListAsync()等方法。
下面代码是一次性加载数据到内存中,去掉ToArray()就是分批读从数据库读取数据
foreach(var a in ctx.Articles.ToArray()) { Console.WriteLine(a.Title); Thread.Sleep(10); }
何时需要一次加载
1、场景1:遍历IQueryable并且进行数据处理的过程很耗时。
2、场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回lQueryable的。必须一次性加载返回。
3、场景3:多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。把连接字符串中的MultipleActiveResultSets=true删掉,其他数据库不支持这个。
如果是其它数据库需要多个DataReader同时执行,也是要把要读取数据全部加载到内存中,不用分批读取就可以了,也是后面加载ToArray()方法
《8》EF Core执行原生SQL语句
1:执行非查询原生SQL语句
string name = ";delete from T_Articles;"; await ctx.Database.ExecuteSqlInterpolatedAsync($@"insert into T_Articles (Title,Message) select Title,{name} from T_Articles where Id >= 3");
2:执行实体相关查询语句
string titlePattern = "%微软%"; var queryable = ctx.Articles.FromSqlInterpolated($"select * from T_Articles where Title like {titlePattern} order by newid()");//查询返回的是IQueryable类型,下面要遍历才会执行SQL语句 foreach ( var article in queryable ) { Console.WriteLine( article.Id + article.Title ); } //EF Core 7.0 中引入了 FromSql。 使用更旧的版本时,请改用 FromSqlInterpolated。 下面是用FromSql查询 var blogs = ctx.Articles .FromSql($"select * from T_Articles where Price='23'") .ToList();
3:执行原生SQL语句查询
DbConnection conn = ctx.Database.GetDbConnection(); if (conn.State != System.Data.ConnectionState.Open) { await conn.OpenAsync(); } using (var cmd = conn.CreateCommand()) { cmd.CommandText = "select Price,Count(*) from T_Articles group by Price"; using (var reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { string price = reader.GetString(0); int count = reader.GetInt32(1); Console.WriteLine($"{price}:{count}"); } } }
下面是引用Dapper框架执行原生SQL语句查询,
GroupArticleByPrice是要手动添加的类
class GroupArticleByPrice { public string Price { get; set; } public int Pcount { get; set; } }
var items=ctx.Database.GetDbConnection().Query<GroupArticleByPrice>("select Price,Count(*) Pcount from T_Articles group by Price"); foreach(var item in items) { Console.WriteLine(item.Price.ToString()+ ":"+ item.Pcount); }
《9》EF Core如何检测实体类数据发生改变,使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntry
(1)实体的状态
1:已添加(Added),2:未改变(Unchanged),3:已修改(Modified),4:已删除(Deleted),5:已分离(Detached)。
首次跟踪一下实体的时候,EF Core会创建这个实体的快照。执行SaveChanges()等方法时,EF Core将会把存储的快照中的值与实体的当前值进行比较。
EntityEntry类的State属性代表实体的状态,通过DebugView.LongView的属性可以看到实体的变化信息。代码如下
var items =ctx.Articles.Take(3).ToArray(); var a1 = items[0]; var a2 = items[1]; var a3 = items[2]; var a4 = new Article { Title = "ddd", Message = "这是个测试" }; var a5 = new Article { Title = "new", Message = "流星蝴蝶剑" }; a1.Price += 1; ctx.Remove(a2); ctx.Articles.Add(a4); EntityEntry e1 = ctx.Entry(a1); EntityEntry e2 = ctx.Entry(a2); EntityEntry e3 = ctx.Entry(a3); EntityEntry e4 = ctx.Entry(a4); EntityEntry e5 = ctx.Entry(a5); Console.WriteLine(e1.State); Console.WriteLine(e2.State); Console.WriteLine(e3.State); Console.WriteLine(e4.State); Console.WriteLine(e5.State);
下面是输出的信息
(2) 如果查询出来的对象不会被修改,删除等,那么查询时可以AsNoTracking(),就能降低内存占用。
《10》全局查询筛选器
在Config文件中配置后,用查询时会自动加上查询语句
builder.HasQueryFilter(a=>a.IsDeleted==false);
如果需要屏蔽全局查询筛选器
var item = ctx.Articles.IgnoreQueryFilters().Take(3).ToArray();
《11》EF Core悲欢锁,基于MySQL数据库
Console.WriteLine("请输入名字"); string name = Console.ReadLine(); using (MyDbConfig ctx = new MyDbConfig()) using (var tx = ctx.Database.BeginTransaction()) { Console.WriteLine(DateTime.Now.ToString() + "准备select"); var h = ctx.Houses.FromSqlInterpolated($"select * from T_House where Id=1 for update").Single(); Console.WriteLine(DateTime.Now.ToString() + "已经完成select"); if (!string.IsNullOrEmpty(h.Owner)) { if (h.Owner == name) { Console.WriteLine("房子已经被你抢到了"); } else { Console.WriteLine("房子已经【" + h.Owner + "】占了"); } Console.ReadKey(); return; } h.Owner = name; Thread.Sleep(5000); Console.WriteLine("恭喜你,抢到了"); ctx.SaveChanges(); Console.WriteLine(DateTime.Now.ToString() + "保存完成"); tx.Commit(); //解锁 Console.ReadKey();
下面是运行结果
《12》MySQL乐观并发
namespace 乐观并发 { class HouseConfig : IEntityTypeConfiguration<House> { public void Configure(EntityTypeBuilder<House> builder) { builder.ToTable("T_House"); builder.Property(b=>b.Name).IsRequired(); builder.Property(b=>b.Owner).IsConcurrencyToken(); } } }
static void Main(string[] args) { Console.WriteLine("请输入名字"); string name = Console.ReadLine(); using (MyDbConfig ctx = new MyDbConfig()) { var h = ctx.Houses.Single(h => h.Id == 1); if (!string.IsNullOrEmpty(h.Owner)) { if (h.Owner == name) { Console.WriteLine("房子已经被你抢到了"); } else { Console.WriteLine("房子已经【" + h.Owner + "】占了"); } Console.ReadKey(); return; } h.Owner = name; Thread.Sleep(5000); try { ctx.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("并发访问冲突"); var entry1 =ex.Entries.First(); string newValue = entry1.GetDatabaseValues().GetValue<string>("Owner"); Console.WriteLine("被++++++" + newValue + "抢走了"); } Console.ReadKey(); }
《12》SQLServer乐观并发
namespace 乐观并发 { class House { public string Name { get; set; } public long Id { get; set; } public string Owner { get; set; } public byte[] RowVersion { get; set; } } }
在把RowVersion配置成.IsRowVersion属性,基它和上面MySQL乐观并发一直
namespace 乐观并发 { class HouseConfig : IEntityTypeConfiguration<House> { public void Configure(EntityTypeBuilder<House> builder) { builder.ToTable("T_House"); builder.Property(b=>b.Name).IsRequired(); builder.Property(b => b.RowVersion).IsRowVersion(); } } }