EF Core 学习笔记
以下 Model First 学习笔记。
学习笔记来源于B站Up主软件工艺师分享的学习视频:https://www.bilibili.com/video/BV1xa4y1v7rR
准备工作
-
建好项目
-
在项目中安装 Microsoft.EntityFrameworkCore.SqlServer 的 Nuget 包;
-
使用数据库
public class EFCoreDemoContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB; Initial Catalog=Demo"); //使用 SQL SERVER 数据库 } public DbSet<League> Leagues { get; set; } public DbSet<Club> Clubs { get; set; } public DbSet<Player> Players { get; set; } public DbSet<Resume> Resumes { get; set; } public DbSet<Game> Games { get; set; } } /// <summary> /// 输出数据库操作日志 /// </summary> public static readonly ILoggerFactory ConsoleLoggerFactory = LoggerFactory.Create(builder => { //对日志进行过滤 builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name //只输出数据库操作命令即可 && level == LogLevel.Information) //日志级别 .AddConsole(); //需要安装 Microsoft.Extensions.Logging.Console });
如何使用 EF Core 生成数据库
流程:
- 在项目中使用 C# 代码定义/修改 Model
- 创建 Migration 文件。Migration 文件对源代码版本控制非常友好。
- 应用 Migration 到数据库/生成脚本
使用 Migration 命令需要安装两个 NuGet 库:
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.EntityFrameworkCore.Design
其中 Design 是 Tools 的依赖项。两个库需要安装在 DataContext 所在的项目中。
在对应项目的控制台中执行命令:
add-migration xxx,生成对应数据库映射。
执行完后会生成两个文件:
- 项目名称+Snapshot:相当于一个”快照“,用于追踪所有 Model 的状态,不可手动更改。
- 时间戳+xxx:数据库迁移文件。包含以下方法:
- Up():执行数据库操作
- Down():执行回滚操作
script-migration:生成数据库迁移脚本。通常在生产环境中使用。
update-database(-verbose):生成数据库迁移脚本。通常开发环境中使用。
设置字段属性
可以在 C#代码中给 Model 中的字段设置 Attribute 来关联设置数据库中对应字段的。使用前需要在项目中安装 NuGet 包:System.ComponentModel.Annotations。
代码如下:
public class League
{
public int Id { get; set; }
[Required] //必填项,不可为 null
[MaxLength(100)] //最大长度为 100
public string Name { get; set; }
[Required, MaxLength(50)]
public string Country { get; set; }
}
public class Club
{
//......
[Column(TypeName = "date")] //设置数据库中的数据类型为 日期 类型
public DateTime DateOfEstablishment { get; set; }
}
更改后执行 add-migration xxx 和 update-database(-verbose)两条命令,即可完成数据库结构的更改。
实体间的对应关系
1:n 关系
有两种方式,两种方式在数据库中,均会在 n 的表中生产对应 1 的外键。
- 在 1 的类中引入 n 的对象的集合
- 在 n 的类中引入 1 的单个对象
如下代码,1:n 关系有 Club:Players 和 League:Clubs。
public class Club
{
public Club()
{
Players = new List<Player>();
}
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
[Column(TypeName = "date")] //设置数据库中的数据类型为 日期 类型
public DateTime DateOfEstablishment { get; set; }
public string History { get; set; }
public League League { get; set; }
public List<Player> Players { get; set; }
}
m:n 关系
需要建立中间表(类),中间表(类)中存储 n 和 m 类中的对象(外键),同时分别在 n 和 m 对应的类中,建立中间类的对象集合。如下代码,Game 和 Player 是 n:m 的关系,GamePlayer 是中间表。
public class GamePlayer
{
public int PlayerId { get; set; }
public int GameId { get; set; }
public Game Game { get; set; }
public Player Player { get; set; }
}
public class Game
{
public Game()
{
GamePlayers = new List<GamePlayer>();
}
public int Id { get; set; }
public string Round { get; set; }
public DateTimeOffset? StartTime { get; set; }
public List<GamePlayer> GamePlayers { get; set; }
}
public class Player
{
public Player()
{
GamePlayers = new List<GamePlayer>();
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public List<GamePlayer> GamePlayers { get; set; }
}
GamePlayer 的主键是联合主键,需要在 DbContext 中重写 OnModelCreating 方法设置,如下:
public class EFCoreDemoContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//设置联合主键
modelBuilder.Entity<GamePlayer>().HasKey(x => new { x.PlayerId, x.GameId });
}
public DbSet<League> Leagues { get; set; }
public DbSet<Club> Clubs { get; set; }
public DbSet<Player> Players { get; set; }
public DbSet<Resume> Resumes { get; set; }
public DbSet<Game> Games { get; set; }
}
1:1 关系
需要在两个类中分别建立对方的对象实体(外键关系),然后在 DbContext 中重写 OnModelCreating 方法设置,如下:
public class Resume
{
public int Id { get; set; }
public string Description { get; set; }
public int PlayerId { get; set; }
public Player Player { get; set; }
}
public class Player
{
public Player()
{
GamePlayers = new List<GamePlayer>();
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public List<GamePlayer> GamePlayers { get; set; }
public int ResumeId { get; set; }
public Resume Resume { get; set; }
}
public class EFCoreDemoContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//设置联合主键
modelBuilder.Entity<GamePlayer>().HasKey(x => new { x.PlayerId, x.GameId });
//设置一对一关系
modelBuilder.Entity<Resume>().HasOne(x => x.Player).WithOne(x => x.Resume).HasForeignKey<Resume>(x => x.PlayerId);
}
public DbSet<League> Leagues { get; set; }
public DbSet<Club> Clubs { get; set; }
public DbSet<Player> Players { get; set; }
public DbSet<Resume> Resumes { get; set; }
public DbSet<Game> Games { get; set; }
}
数据库操作
添加、查询
单条数据添加
using var context = new EFCoreDemoContext(); //此处 using 为 C# 8 的语法,当context使用完后,会直接释放。
var serieA = new League
{
Country = "Italy",
Name = "Serie A"
};
context.Leagues.Add(serieA);
var count = context.SaveChanges();
Console.WriteLine(count);
context.SaveChanges(); 会自动生成数据库语句,完成数据库操作
批量数据库操作
using var context = new EFCoreDemoContext();
var serieB = new League
{
Country = "Italy",
Name = "Serie B"
};
var serieC = new League
{
Country = "Italy",
Name = "Serie C"
};
//添加多条数据
context.Leagues.AddRange(serieB, serieC);
context.Leagues.AddRange(new List<League> { serieB, serieC });
var count = context.SaveChanges();
Console.WriteLine(count);
插入两个不同表的数据
using var context = new EFCoreDemoContext();
var serieA = 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 Lian",
City = "Milan",
DateOfEstablishment = new DateTime(1899, 2, 1),
League = serieA
};
context.AddRange(serieB, serieC, milan);
查询
使用LINQ查询:此处,如果没有后面的 ToList() 方法,则只是构建一个查询语句,而没有完成查询操作。需要在查询语句后面加上形如 ToList() 的方法,才可以进行查询。
using var context = new EFCoreDemoContext();
var leagues = context.Leagues
.Where(x => x.Country == "Italy")
.ToList();
var leagues2 = (from lg in context.Leagues
where lg.Country == "Italy"
select lg).ToList();
使用 foreach 循环时,会自动打开数据库连接,且连接会一直打开,直到循环结束。此时若循环体操作复杂,则会引起数据问题。所以尽量先使用 ToList() 等方法完成查询,然后对 ToList() 的结果进行循环操作。
foreach (var league in context.Leagues)
{
Console.WriteLine(league.Name);
}
能完成查询的方法
查询语句后,添加如下形式的方法,才可完成查询:
ToList():返回集合;
First():返回符合条件的第一条数据,若无返回值则会报错;
FirstOrDefault():返回符合条件的第一条数据,可以没有返回结果
Single():符合查询条件的只能是一个数据
SingleOrDefault():符合查询条件的只能是一个或没有数据
Last():返回最后一条数据,若无返回值则会报错;
LastOrDefault():返回最后一条数据,可以没有返回结果;
统计方法:Count()、LongCount()、Max()、Min()、Sum()、Average()
Find()
同时还有如上方法的异步版本,如 ToListAsync()
var first = context.Leagues
.FirstOrDefault(x =>
EF.Functions.Like(x.Country, "%e%"));
Console.WriteLine(first?.Name);
查询使用参数的区别
如下两条查询语句,当查询语句中使用的是写死的字符串 ”Italy“,则生成的 SQL 语句中也是写死的固定值;若使用变量 italy,则生成的 SQL 语句中使用参数传递的形式传入 italy 的值。
var italy = "Italy";
var leagues = context.Leagues
.Where(x => x.Country == "Italy")
.ToList();
leagues = context.Leagues
.Where(x => x.Country == italy)
.ToList();
模糊查询的两种方法
var leagues3 = context.Leagues
.Where(x => x.Country.Contains("e"))
.ToList();
var leagues4 = context.Leagues
.Where(x =>
EF.Functions.Like(x.Country, "%e%")
)
.ToList();
生成 SQL 语句的顺序
如下语句,查询结果一致, EF 中只会生成一次 SQL 语句。因为在第一次的时候,context 中已经将结果存储在内存中,后面的 Find 方法可以直接从内存中取得对应的数据。
var findfirst = context.Leagues
.SingleOrDefault(x => x.Id == 2);
var findfirst2 = context.Leagues.Find(2);
当将二者换了顺序之后,则会生成两条 SQL 语句。因为 Find 方法之前没有完成查询,它想完成查询只可以通过生成 SQL 语句来查询。
var findfirst2 = context.Leagues.Find(2);
var findfirst = context.Leagues
.SingleOrDefault(x => x.Id == 2);
var last = context.Leagues
.LastOrDefault(x =>
EF.Functions.Like(x.Country, "%e%"));
Console.WriteLine(last?.Name);
以上代码会产生异常,因为要使用 LastOrDefault 方法,必须对查询结果进行排序。如下:
var last = context.Leagues
.OrderBy(x => x.Id)
//.OrderByDescending(x => x.Id)
.LastOrDefault(x =>
EF.Functions.Like(x.Country, "%e%"));
Console.WriteLine(last?.Name);
修改、删除
删除数据
EF Core 只能删除被 context 追踪的数据。而只有被查询出来的数据才可以被追踪。
//删除
var milian = context.Clubs.Single(x => x.Name == "AC Milian");//先查询处需要处理的数据
//调用删除方法:四种
context.Clubs.Remove(milian);
//context.Remove(milian);
//context.Clubs.RemoveRange(milian, milian);
//context.RemoveRange(milian, milian);
var countRemove = context.SaveChanges();
Console.WriteLine(countRemove);
修改数据
只能修改被 context 追踪的数据
-
修改单条数据
var leagueUpdate = context.Leagues.First(); leagueUpdate.Name += "~~"; var countUpdate = context.SaveChanges(); Console.WriteLine(countUpdate);
-
批量修改数据
var leaguesUpdate = context.Leagues.Skip(1).Take(3).ToList(); //skip 和 Take 结合使用通常是做翻页使用的,skip 表示跳过前面一条数据,take 表示取三条数据 foreach (var league in leaguesUpdate) { league.Name += "~~"; } var countUpdates = context.SaveChanges(); Console.WriteLine(countUpdates);
更改未追踪数据
上述修改方式并不符合大部分使用场景。常用场景是前端返回 json 数据,然后反序列化形成 C# 类,这些数据就都不是使用 Context 查询出来的
var leagueNoTracking = context.Leagues.AsNoTracking().First(); //AsNoTracking()表示取消追踪,用于模拟前端传过来的json数据
//AsNoTracking 可以在这单独设置,也可以全局设置
leagueNoTracking.Name += "~~";
context.Leagues.Update(leagueNoTracking); //将 leagueNoTracking 添加到 Context的追踪范围内,然后把状态设置为修改状态,此时该数据除主键外,所有的属性都是修改状态,提交后都会修改
var countUpdateNoTracking = context.SaveChanges();
Console.WriteLine(countUpdateNoTracking);
全局设置 NoTracking
EFCoreDemoContext.cs
public EFCoreDemoContext()
{
//全局设置数据不追踪
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
添加关系型数据
-
有以下两种形式
- 添加单个外键属性
- 添加集合属性
//添加关系型数据 var serieAA = context.Leagues.Single(x => x.Name == "Serie A"); var juventus = new Club { League = serieAA, //第一种形式,相当于添加外键 Name = "Juventus", City = "Torino", DateOfEstablishment = new DateTime(1897, 11, 1), Players = new List<Player> //第二种形式 { new Player { Name = "C. Ronaldo", DateOfBirth = new DateTime(1985,2,5) } } }; context.Clubs.Add(juventus); int countAdd = context.SaveChanges(); Console.WriteLine(countAdd);
-
如下代码先将 Club juventus 查询出来,context 对其进行了追踪,故可以直接在其中添加 Player,而不需要添加外键
var juventusOne = context.Clubs.Single(x => x.Name == "Juventus"); juventusOne.Players.Add(new Player { Name = "Gonzalo Higuan", DateOfBirth = new DateTime(1987, 12, 10), }); int countjuventusOne = context.SaveChanges(); Console.WriteLine(countjuventusOne);
-
利用新的 context 进行更新数据。此时数据及其更新状态已经在 juventusOne 有记录,使用新的 Context 可以进行正常更新。但是,使用此 update 方法,除了会正常增加数据,同时也会对 Clubs 中的 Juventus 数据进行更新。
var juventusOne = context.Clubs.Single(x => x.Name == "Juventus"); juventusOne.Players.Add(new Player { Name = "Gonzalo Higuan", DateOfBirth = new DateTime(1987, 12, 10), }); //利用新的 context 进行更新数据 { using var newContext = new EFCoreDemoContext(); newContext.Clubs.Update(juventusOne); int countjuventusOne = newContext.SaveChanges(); Console.WriteLine(countjuventusOne); }
-
变化追踪:为了避免 Clubs 中的 Juventus 数据被更改,可以使用 Attach 方法。Attach 只会对产生变化的数据进行更改。如下代码中,Attach 发现 juventusOne 对象数据有主键,且并没有更改,所以不会对其进行更新;但是发现 juventusOne.Players 有一个 Player 没有主键,故会对其完成新增操作。
var juventusOne = context.Clubs.Single(x => x.Name == "Juventus"); juventusOne.Players.Add(new Player { Name = "Gonzalo Higuan", DateOfBirth = new DateTime(1987, 12, 10), }); //利用新的 context 进行更新数据 { using var newContext = new EFCoreDemoContext(); newContext.Clubs.Attach(juventusOne); int countjuventusOne = newContext.SaveChanges(); Console.WriteLine(countjuventusOne); }
对比
注:Add 方法有主键时会报错
加载关联数据
预加载,Eager Lading
显式加载,Explicit Loading
懒加载,Lazy Loading
预加载
//查询出 clubs 及其关联的 League
var clubs = context.Clubs.Include(x => x.League).ToList();
var clubs01 = context.Clubs
.Where(x => x.Id > 0)
.Include(x => x.League)
.FirstOrDefault();
//如果FirstOrDefault()放在Include前则不支持
//var clubs01 = context.Clubs
// .FirstOrDefault()
// .Where(x => x.Id > 0)
// .Include(x => x.League);
//Include 可以多次使用
var clubs02 = context.Clubs
.Where(x => x.Id > 0)
.Include(x => x.League)
.Include(x => x.Players)
.FirstOrDefault();
//级联添加关联数据。如下,添加Player的关联数据
var clubs02 = context.Clubs
.Where(x => x.Id > 0)
.Include(x => x.League)
.Include(x => x.Players)
.ThenInclude(y => y.Resume)
.FirstOrDefault();
//多个级联
var clubs02 = context.Clubs
.Where(x => x.Id > 0)
.Include(x => x.League)
.Include(x => x.Players)
.ThenInclude(y => y.Resume)
.Include(x => x.Players)
.ThenInclude(y => y.GamePlayers)
.FirstOrDefault();
//以下是错误的,因为如下第二个ThenInclude关联的是Resume
var clubs02 = context.Clubs
.Where(x => x.Id > 0)
.Include(x => x.League)
.Include(x => x.Players)
.ThenInclude(y => y.Resume)
.ThenInclude(y => y.GamePlayers)
.FirstOrDefault();
显式加载
example one:
var clubs02 = context.Clubs
.Where(x => x.Id > 0)
.Select(x => new
{
x.Id,
LeagueName = x.League.Name,
x.Name,
Players = x.Players
.Where(p => p.DateOfBirth > new DateTime(1990, 1, 1))
}).ToList();
//Context 无法追踪匿名类,只追踪它识别的类。
//在上面代码中,Players 是可以识别出来的,故可以完成追踪。如下:
foreach (var data in clubs02)
{
foreach (var player in data.Players)
{
player.Name += "~~";
}
}
context.SaveChanges();
example two:
using var context01 = new EFCoreDemoContext();
var info = context01.Clubs.First();
context01.Entry(info)
.Collection(x => x.Players) //关联属性是集合,使用Collection方法
.Query().Where(x => x.DateOfBirth > new DateTime(1990,1,1)) //针对关联集合添加筛选条件
.Load();
context01.Entry(info)
.Reference(x => x.League) //关联属性单个,使用Reference方法
.Load();
缺点:只能针对单个数据进行加载。如上只能针对 info 这一个 Club 进行加载,若针对 List<Club> 则无法加载。
懒加载
在 EF Core 中,这个特性默认是关闭的,可以手动开启。使用过程中存在问题较多,不建议使用。
多个多关联数据查询
Game 和 Player 是多对多关系,有个中间表 GamePlayer,但是在 EFCoreDemoContext 中的 DbSet 中没有体线,可以按以下方式查询
var gamePlayer = context.Set<GamePlayer>()
.Where(x => x.Player.Id > 0).ToList();
修改关联数据
修改关联数据
context 会记录 club 中 League 的状态,所以可以关联更改
var club = context.Clubs.Include(x => x.League)
.First();
club.League.Name += "@";
context.SaveChanges();
var game = context.Games
.Include(x => x.GamePlayers)
.ThenInclude(x => x.Player)
.First();
var firstPlayer = game.GamePlayers[0].Player;
firstPlayer.Name += "$";
{
using var newContext = new EFCoreDemoContext();
//newContext.Players.Update(firstPlayer); //update方法会把所有关联的属性都更新一遍。这里会更新 firstPlayer 上除了主键之外所有的属性
newContext.Entry(firstPlayer).State = EntityState.Modified; //这样设置就会只更改这一个 firstPlayer 数据
newContext.SaveChanges();
}
设置多对多关系
方法一:
var gamePlayer01 = new GamePlayer
{
GameId = 1,
PlayerId = 3
};
context.Add(gamePlayer01);
context.SaveChanges();
方法二
var game01 = context.Games.First();
game.GamePlayers.Add(new GamePlayer
{
PlayerId = 4
});
context.SaveChanges();
删除多对多关系
var gamePlayer02 = new GamePlayer
{
GameId = 1,
PlayerId = 3
}; //这里为了简便是直接 new 出来的 GamePlayer,更正确的方式是从数据库中查询出来这个关系
context.Remove(gamePlayer02);
context.SaveChanges();
修改多对多关系
删除原来的关系,再建立新的关系
设置一对一的关系
变化追踪状态下
var player = context.Players.First();
player.Resume = new Resume
{
Description = "2eew"
};
context.SaveChanges();
离线状态下
var player01 = context.Players
.AsNoTracking()
.OrderBy(x => x.Id)
.Last();
player01.Resume = new Resume
{
Description = "2eew"
};
{
using var newContext = new EFCoreDemoContext();
newContext.Attach(player01);
newContext.SaveChanges();
}
此时,如果直接使用上述代码,但是更改了Description,对 player01.Resume 进行设置,则会报错。因为再 Resume 中的 Player 的 Id 是唯一索引,不能重复。
但是,如果使用 Include 将 Resume 添加到内存中,则可以更改。更改过程中,会先删除之前的 Resume 和 Player 的关系,再添加新的关系。
var player02 = context.Players
.Include(x => x.Resume)
.OrderBy(x => x.Id)
.Last();
player02.Resume = new Resume
{
Description = "asdfds"
};
context.SaveChanges();
执行原生的 SQL
数据库操作(视图、存储过程)
在 EF Core 中不可以直接操作数据库(生成视图、创建存储过程等),需要将对数据库的操作脚本放到 Migration 中,然后由 Migration 来操作数据库。
具体操作步骤:
-
使用 add-migration xxx 先生成一个 Migration
-
在 Migration 中 的Up 和 Down 方法中编写需要的 SQL 语句
-
使用 update-database(-verbose)更新数据库
无主键的 Entity
.net core 3.1 中允许无主键的 Entity,它们不会被追踪,映射到没有主键的 Table 或 View。
如下代码,PlayerClub 无主键,在 DbContext 中添加了 PlayerClub 的DbSet 之后,需要在 OnModelCreating 进行如下设置。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//...........
modelBuilder.Entity<PlayerClub>()
.HasNoKey() //设置 无主键
.ToView("PlayerClubView"); //映射到视图
}
针对这种没有主键的 Entity,查询出来的结果都是无法追踪的。
原生 SQL 查询
有如下四个查询方法(DbSet 上的方法):
-
FromSQLRaw(“select * …”)
-
FromSQLRawAsync(“select * …”)
-
FromSQLInterpolated($“select * where x = {var}”)
-
FromSQLInterpolatedAsync(“select * where x = {var}”)
FromSQLInterpolated 和 FromSQLInterpolatedAsync 为字符串插值形式,可以直接将对应参数值直接放到花括号中。
示例如下:
var clubsSQL = context.Clubs
.FromSqlRaw("select * from dbo.Clubs")
.Include(x => x.League)
.ToList();
var clubsSQL = context.Clubs
.FromSQLInterpolated($"select * from dbo.Clubs where Id > {0}")
.FromSQLInterpolated($"EXEC dbo.xxx where Id > {0}")
.ToList();
原生 SQL 查询的要求
- 必须返回 Entity 类型的所有(标量)属性
- 字段名和 Entity 的属性名匹配
- 无法包含关联的数据
- 只能查询已知的 Entity
执行非查询的 SQL 语句
有如下方法:
- Context.Database.ExecuteSQLRaw()
- Context.Database.ExecuteSQLRawAsync()
- Context.Database.ExecuteSQLInterpolated()
- Context.Database.ExecuteSQLInterpolatedAsync()
ExecuteSQLInterpolated 和 ExecuteSQLInterpolatedAsync 为字符串插值形式,可以直接将对应参数值直接放到花括号中。
以上方法:
- 无法用与查询
- 只能返回影响的行数
var count = context.Database
.ExecuteSqlRaw("EXEC dbo.RemoveGamePlayersProcedurce {0}", 2);
count = context.Database
.ExecuteSQLInterpolated($"EXEC dbo.RemoveGamePlayersProcedurce {2}");
如何在 ASP.NET Core 项目中配置 EF Core
在 DbContext 类(EFCoreDemoContext)中添加构造函数
public EFCoreDemoContext(DbContextOptions<EFCoreDemoContext> options)
: base(options)
{
}
在appsettings.json 设置连接字符串
"ConnectionStrings": {
"LoaclDB": "Server=(localdb)\\ProjectsV84;Database=TestDB1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
在 Startup.cs 中注册 EFCoreDemoContext:在 ConfigureServices 方法中添加如下代码
services.AddDbContext<EFCoreDemoContext>(options => options.UseSqlServer(Configuration.GetConnectionString("LoaclDB")));
这样配置后,EFCoreDemoContext 就可以在项目中依赖注入地使用了。