添加
与数据库进行交互需要用到我们的数据库上下文,我的是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语句
上面我们在控制台中输出了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等执行对数据库的操作的方法
总结一下哪些常用的方法可以执行对数据库的操作:
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()等,还有SaveChangesAsync()。
删除
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()来模拟前端传回的数据,AsNoTracking顾名思义,就是不进行追踪,查完就跟context没有任何关系了。league就相当于前端的JSON传进来的。修改了league.Name之后,如何再次让它被Context追踪?使用Update()方法将数据传进去就可以了。它可以将league的状态设置成modified状态。
Update()同样也有UpdateRange()方法,context上也有这个UpDate()方法,也就是说可以直接context.Update(league)
。
执行完发现我们虽然只修改了Name属性,但Country属性也重设了一遍,这是因为重新进行追踪后,league的所有属性都处于modified状态,相当于都修改过。
也可以只修改Name这一个属性,开销似乎会小很多,可以查阅官方文档。
不追踪-AsNoTracking()
不加AsNoTracking(),context查询的数据都将被它追踪,而且是不断变化地追踪。既然有变化追踪,就会不断消耗CPU和内存,开销有点大。
如果查询的数据量比较大,而且不需要进行变化追踪,可以加上AsNoTracking()。执行完AsNoTracking(),依然处于没有执行对数据库查询地动作的状态,直到遇到ToList()等枚举查询结果的方法才执行。
AsNoTracking()可以跟我们前面用的时候一样单独设置,也可以进行全局的设置。去数据库上下文中的构造函数中添加:
public DemoDbContext()
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
这样就算是在这个上下文的全局进行设置了,context将不会再追踪,也不用单独再在查询中写AsNoTracking()。