boredbird 2020-06-26
与数据库进行交互需要用到我们的数据库上下文,我的是DemoDbContext。Context用完后需要对它进行清理资源,也就是调用它的Dispose方法,因为它实现了IDisposable接口。也可以直接使用using关键字,当方法走完时,Context就会被Dispose掉。
添加一条League:
class Program { static void Main(string[] args) { using var context = new DemoDbContext(); var serieA = new League { Country = "Italy", Name = "Serie A" }; context.Leagues.Add(serieA); // 此时与数据库没有任何交互 var count = context.SaveChanges(); // 此时发生与数据库交互 Console.WriteLine(count); } }
输出结果为1。数据也添加成功了:
在调用context的SaveChanges方法时,Context会检查所有它的对象的最终的状态,有的对象可能新增了,有的对象可能修改了等。SaveChanges相当于在同一个事务里,针对它的变化,执行相应的SQL语句。如果执行失败,会整体性回滚,执行成功会返回受影响的行数,这里就是1。
添加多笔数据,有两种方式,一种是把多笔数据对应的多个对象作为AddRange方法的参数;另一种是new一个集合,把多笔数据放在集合里作为AddRange方法的参数:
context.Leagues.AddRange(serieB, serieC); // 方法1 context.Leagues.AddRange(new List<League> { serieB, serieC }); // 方法2
添加多笔不同类型的数据:
using var context = new DemoDbContext(); var seriaA = context.Leagues.Single(x => x.Name == "Serie A"); var serieB = new League { Country = "Italy", Name = "Serie B" }; var serieC = new League { Country = "Italy", Name = "Serie C" }; var milan = new Club { Name = "AC Milan", City = "Milan", DateOfEstablished = new DateTime(1899, 12, 16), League = seriaA }; context.AddRange(serieB, serieC, milan); var count = context.SaveChanges(); Console.WriteLine(count);
其中SeriaA是从数据库中查询到的Name为"Serie A"的实体。serieB, serieC与milan是不同类型,milan的League属性是查询出来的SeriaA。在添加多笔不同类型的数据到数据库时,直接使用context.AddRange,省去了中间的League等DbSet。直接使用context也可以使用Add方法添加单笔数据。运行了一下是可以直接执行成功的。
可以看到先进行了一次查询,又进行了三次INSERT,是符合预期的。
上面我们在控制台中输出了SQL语句,如何做到的呢?我们在SaveChanges时,想要在控制台日志中输出执行的SQL语句。回到数据库Context类中,加一个静态只读的日志工厂:
public static readonly ILoggerFactory ConsoleLoggerFactory = LoggerFactory.Create(builder => { builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information) .AddConsole(); });
其中AddConsole会有红色下划线,我们需要在数据库上下文所在的项目中安装Microsoft.Extensions.Logging.Console这个Nuget包。
再看上面这个ILoggerFactory类的属性,.Net Core的日志系统会输出很多日志,我们加了一个过滤器过滤一下。范畴(category)为DbLoggerCategory.Database.Command.Name,等级(level)为Information。
然后修改一下重写的OnConfiguring,在UseSqlServer前面加上UseLoggerFactory:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseLoggerFactory(ConsoleLoggerFactory) .UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=YangDemo;Integrated Security=True"); }
再次运行程序就会在控制台输出日志:
查询Leagues表中的所有数据:
using var context = new DemoDbContext(); var leagues = context.Leagues.Where(l => l.Country == "Italy").AsEnumerable(); // var leagues = context.Leagues.Where(l => l.Country == "Italy").ToList(); // AsEnumerable与ToList结果相同,但有不同之处 foreach (League league in leagues) { Console.WriteLine(league.Name); }
其中AsEnumerable和ToList的不同之处参见:https://www.cnblogs.com/Kit-L/p/13191498.html#asenumerable方法
如果不写ToList或者AsEnumerable等枚举查询结果集合的语句的话,只相当于组建了SQL查询语句,并不执行。下列语句是用查询表达式形式来进行查询,但一般没人这样用。
var leagues2 = (from league in context.Leagues where league.Country == "Italy" select league).ToList();
也可以先不ToList,而是直接用for循环来遍历查询,但这种方式最好是for循环中的操作很简单很快,否则会产生数据冲突等问题。一般也不应该这样做,最好是先ToList再从结果中读取数据。
可以看到我们查询中的条件"Italy"是写死的,我们可以用一个参数来替换,这样生成的SQL语句中的"Italy"也会变成参数:
var italy = "Italy"; var leagues = context.Leagues.Where(l => l.Country == italy).AsEnumerable();
看一下控制台输出的SQL语句:
可以看到变成了参数的形式,本来是字符串的形式。但可以看到itely的参数值是一个问号,但我们传进去的是"Italy"。这是因为默认情况下,EF Core不会把参数输出到日志中。可以回数据库上下文类中进行修改使其显示:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseLoggerFactory(ConsoleLoggerFactory) .EnableSensitiveDataLogging() // .UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=YangDemo;Integrated Security=True"); }
如何实现SQL语句的Country LIKE %e%
这种模糊查询操作?可以使用字符串的Contains方法:
var leagues = context.Leagues .Where(l => l.Country.Contains("e")) .AsEnumerable();
还有另一种写法:
var leagues2 = context.Leagues .Where(x => EF.Functions.Like(x.Country, "%e%")) .ToList();
总结一下哪些常用的方法可以执行对数据库的操作:
ToList():返回集合。
First()、FirstOrDefault():返回单笔数据,返回满足条件的第一笔数据。
First()必须有数据,没有就报错,FirstOrDefault()可以有数据也可以没有数据。最终如果要输出的话,可以用?.的方式,如league?.Name
。First()等括号中可以直接写条件,常常可以省略Where()这一块。
Single():符合查询条件的只能是一个数据。
SingleOrDefault():符合查询条件的只能是一个数据或者没有数据。
Last()、LastOrDefault():最后一笔数据,其他特征同上。想使用它们,必须进行排序,比如使用OrderBy()或OrderByDescending()。
Count()、LongCount():查询出来的Count的结果。统计个数。
Min()、Max():最小最大值。
Average():平均值。
Sum():求和。
Find():它不是LINQ方法,是DbSet的方法,但也会执行查询动作。
还有这些方法的异步版本,比如ToListAsync()、FirstAsync()等。
EF Core只能删除被Context追踪的数据,而数据只有先查询出来,才能被追踪。也就是说不查询出来,是无法删除的。
我们知道Clubs表中有一笔数据"AC Milan",我们先查出来再删掉它:
var milan = context.Clubs.Single(x => x.Name == "AC Milan"); // 追踪数据 // 调用删除方法的多种形式 context.Clubs.Remove(milan); context.Remove(milan); context.Clubs.RemoveRange(milan, milan); context.RemoveRange(milan, milan); var count = context.SaveChanges(); Console.WriteLine(count);
如果想使用SQL语句或存储过程对数据库进行Delete动作的话,EF Core也是支持的。
与删除一样,首先数据要被context追踪,才可以修改。:
using var context = new DemoDbContext(); var league = context.Leagues.First(); league.Name += "~~"; // 此时context就知道这个属性已经被修改了,league对象的状态就是Modified,再SaveChange就可以把所有修改操作到数据库 var count = context.SaveChanges(); Console.WriteLine(count);
修改多条数据:
例如:
var leagues = context.Leagues.Skip(1).Take(3).ToList(); foreach (var league in leagues) { league.Name += "~~"; } var count = context.SaveChanges();
而在实际应用场景中,修改的数据往往都是前后端分离的系统架构里,从前端传过来的JSON,反序列化之后变成一个C#类。此时这些数据都不是context查询出来的,自然也就没追踪,也就没法进行修改。
using var context = new DemoDbContext(); var league = context.Leagues.AsNoTracking().First(); // AsNoTracking() league.Name += "++"; context.Leagues.Update(league); // 进行追踪 var count = context.SaveChanges(); Console.WriteLine(count);
使用AsNoTracking()来解决,顾名思义,就是不进行追踪,查完就跟context没有任何关系了。league就相当于前端的JSON传进来的。修改了league.Name之后,如何再次让它被Context追踪?使用Update()方法将数据传进去就可以了。它可以将league的状态设置成modified状态。
Update()同样也有UpdateRange()方法,context上也有这个UpDate()方法,也就是说可以直接context.Update(league)
。
执行完发现我们虽然只修改了Name属性,但Country属性也重设了一遍,这是因为重新进行追踪后,league的所有属性都处于modified状态,相当于都修改过。
也可以只修改Name这一个属性,开销似乎会小很多,可以查阅官方文档。
不加AsNoTracking(),context查询的数据都将被它追踪,而且是不断变化地追踪。既然有变化追踪,就会不断消耗CPU和内存,开销有点大。
如果查询的数据量比较大,而且不需要进行变化追踪,可以加上AsNoTracking()。执行完AsNoTracking(),依然处于没有执行对数据库查询地动作的状态,知道遇到ToList()等才执行。
AsNoTracking()可以跟我们前面用的时候一样单独设置,也可以进行全局的设置。去数据库上下文中建立构造函数:
public DemoDbContext() { ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; }
这样就算是在全局进行设置了,context将不会再追踪,也不用单独再在查询中写AsNoTracking()。