這里介紹在ASP.NET Core中使用EF Core,這里數據庫選的是Sql Server
- 如何使用Sql Server
- 添加模型 && 數據庫遷移
- 查詢數據
- 保存數據
如何使用Sql Server
dotnet tool install --global dotnet-ef
2. 添加包Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
3. 添加包Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
4. 添加DbContext

public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { } }
5.在ConfigureServices注入DbContext

public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<Data.EFCoreDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); }
經過上面5步,我們就可以在項目中使用數據庫,在需要的地方注入DbContext即可
添加模型

[Table("School")] public class School { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Display(Name = "學校名稱")] [Required(ErrorMessage = "學校名稱不能為空")] [StringLength(100, ErrorMessage = "學校名稱最大長度為100")] public string Name { get; set; } [Display(Name = "學校地址")] [Required(ErrorMessage = "學校地址不能為空")] [StringLength(200, ErrorMessage = "學校地址最大長度為200")] public string Address { get; set; } public List<Student> Students { get; set; } [Display(Name = "創建時間")] [DataType(DataType.DateTime), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] public DateTime CreateTime { get; set; } [Display(Name = "最后更新時間")] [DataType(DataType.DateTime), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] public DateTime? LastUpdateTime { get; set; } }
學生類

public class Student { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Display(Name = "學生姓名")] [Required(ErrorMessage = "學生姓名不能為空")] [StringLength(50, ErrorMessage = "學生姓名最大長度為50")] public string Name { get; set; } [Display(Name = "年齡")] [Required(ErrorMessage = "年齡不能為空")] [Range(minimum: 10, maximum: 100, ErrorMessage = "學生年齡必須在(10 ~ 100)之間")] public int Age { get; set; } public School School { get; set; } [Display(Name = "創建時間")] [DataType(DataType.DateTime), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] public DateTime CreateTime { get; set; } [Display(Name = "最后更新時間")] [DataType(DataType.DateTime), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] public DateTime? LastUpdateTime { get; set; } }
配置默認值

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Models.School>() .Property(p => p.CreateTime) .HasDefaultValueSql("getdate()"); modelBuilder.Entity<Models.Student>() .Property(p => p.CreateTime) .HasDefaultValueSql("getdate()"); }
模型定義好之后,我們需要把模型添加到DbContext
public DbSet<Models.School> Schools { get; set; } public DbSet<Models.Student> Students { get; set; }
dotnet ef migrations add DatabaseInit
2. 更新到數據庫
dotnet ef migrations add DatabaseInit
查看數據庫,我們可以看到下面關系圖
在Student表里面多了一個SchoolId,這個我們是沒有定義,是EF Core生成的陰影屬性,當然我們也可以顯示定義這個字段
實體類定義我們用到數據注釋和Fluent API約束實體類生成,下面列取經常用到的
注釋 | 用途 |
---|---|
Key | 主鍵 |
Required | 必須 |
MaxLength | 最大長度 |
NotMapped | 不映射到數據庫 |
ConcurrencyCheck | 並發檢查 |
Timestamp
|
時間戳字段 |
查詢數據

var query = from a in _context.School join b in _context.Student on a.Id equals b.School.Id select new { SchoolName = a.Name, StudentName = b.Name };
對應生成的Sql
SELECT [s].[Name] AS [SchoolName], [t].[Name] AS [StudentName] FROM [School] AS [s] INNER JOIN ( SELECT [s0].[Id], [s0].[Age], [s0].[CreateTime], [s0].[LastUpdateTime], [s0].[Name], [s0].[SchoolId], [s1].[Id] AS [Id0], [s1].[Address], [s1].[CreateTime] AS [CreateTime0], [s1].[LastUpdateTime] AS [LastUpdateTime0], [s1].[Name] AS [Name0] FROM [Student] AS [s0] LEFT JOIN [School] AS [s1] ON [s0].[SchoolId] = [s1].[Id] ) AS [t] ON [s].[Id] = [t].[Id0]
和我們預期有點不一致,預期是兩個表的全連接,為什么出現這個,原因是Student里面的導航屬性School,Linq遇到導航屬性是通過連表得到,為了驗證這個,我們不使用陰影屬性,顯示加上SchoolId試試

var query = from a in _context.School join b in _context.Student on a.Id equals b.SchoolId select new { SchoolName = a.Name, StudentName = b.Name };
對應生成的Sql
SELECT [s].[Name] AS [SchoolName], [s0].[Name] AS [StudentName] FROM [School] AS [s] INNER JOIN [Student] AS [s0] ON [s].[Id] = [s0].[SchoolId]
這次生成的Sql就很簡潔,跟預期一樣,所以如果使用聯接查詢,最好是避免使用陰影屬性
兩個Sql的執行計划


二、GroupBy查詢

var query = from a in _context.School join b in _context.Student on a.Id equals b.SchoolId group a by a.Name into t where t.Count() > 0 orderby t.Key select new { t.Key, Count = t.Count(), };
對應生成的Sql
SELECT [s].[Name] AS [Key], COUNT(*) AS [Count] FROM [School] AS [s] INNER JOIN [Student] AS [s0] ON [s].[Id] = [s0].[SchoolId] GROUP BY [s].[Name] HAVING COUNT(*) > 0 ORDER BY [s].[Name]
EF Core 支持的聚合運算符如下所示
- 平均值
- 計數
- LongCount
- 最大值
- 最小值
- Sum
三、左連接

var query = from a in _context.School join b in _context.Student on a.Id equals b.SchoolId into t1 from t in t1.DefaultIfEmpty() select new { SchoolName = a.Name, StudentName = t.Name }; var list = query.AsNoTracking().ToList();
對應生成的Sql
SELECT [s].[Name] AS [SchoolName], [s0].[Name] AS [StudentName] FROM [School] AS [s] LEFT JOIN [Student] AS [s0] ON [s].[Id] = [s0].[SchoolId]
四、小結
全聯接時避免使用導航屬性連表
默認情況是跟蹤查詢,這表示可以更改這些實體實例,然后通過 SaveChanges() 持久化這些更改,
如果只需要讀取,不需要修改可以指定非跟蹤查詢AsNoTracking
非跟蹤查詢可以在每個查詢后面指定,還可以在上下文實例級別更改默認跟蹤行為
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
保存數據

_context.School.Add(new Models.School { Name = "暨南大學", Address = "廣州市黃埔大道西601號", Students = new System.Collections.Generic.List<Models.Student>() { new Models.Student { Name= "黃偉", Age = 21, }, }, }); _context.SaveChanges();
同時在School,Student表保存數據,自動維護Student表的SchoolId字段數據
二、級聯刪除

var school = _context.School.Include(m => m.Students).FirstOrDefault(m => m.Name == "濟南大學"); _context.School.Remove(school); _context.SaveChanges();

--1. 讀取濟南大學和他所有學生 SELECT [t].[Id], [t].[Address], [t].[CreateTime], [t].[LastUpdateTime], [t].[Name], [s0].[Id], [s0].[Age], [s0].[CreateTime], [s0].[LastUpdateTime], [s0].[Name], [s0].[SchoolId] FROM ( SELECT TOP(1) [s].[Id], [s].[Address], [s].[CreateTime], [s].[LastUpdateTime], [s].[Name] FROM [School] AS [s] WHERE [s].[Name] = N'濟南大學' ) AS [t] LEFT JOIN [Student] AS [s0] ON [t].[Id] = [s0].[SchoolId] ORDER BY [t].[Id], [s0].[Id] --2. 循環每個學生刪除 SET NOCOUNT ON; DELETE FROM [Student] WHERE [Id] = @p0; SELECT @@ROWCOUNT; SET NOCOUNT ON; DELETE FROM [Student] WHERE [Id] = @p0; SELECT @@ROWCOUNT; SET NOCOUNT ON; DELETE FROM [Student] WHERE [Id] = @p0; SELECT @@ROWCOUNT; --3. 刪除學校 SET NOCOUNT ON; DELETE FROM [School] WHERE [Id] = @p1; SELECT @@ROWCOUNT;
級聯刪除要用Include把子項也包含到實體
三、使用事務
默認情況下,如果數據庫提供程序支持事務,則會在事務中應用對 SaveChanges() 的單一調用中的所有更改。 如果其中有任何更改失敗,則會回滾事務且所有更改都不會應用到數據庫。 這意味着,SaveChanges() 可保證完全成功,或在出現錯誤時不修改數據庫。
對於大多數應用程序,此默認行為已足夠。 如果應用程序要求被視為有必要,則應該僅手動控制事務中間調用多次SaveChanges()也不會直接保存到數據庫,最后transaction.Commit()

using (var transaction = _context.Database.BeginTransaction()) { var school = _context.School.Add(new Models.School { Name = "濟南大學", Address = "山東省濟南市南辛庄西路336號", }); _context.SaveChanges(); System.Threading.Thread.Sleep(2000); //for testing _context.Student.Add(new Models.Student { Name = "張三", Age = 29, School = school.Entity }); _context.SaveChanges(); transaction.Commit(); }
下面是Sql Server Profiler

注意兩次RPC:Completed時間,每次調用SaveChanges提交到數據庫執行,外面包一層事務,所以事務里面要盡可能的控制操作最少,時間最少
四、並發沖突
EF Core實現的是樂觀並發,有關樂觀並發和悲觀並發這里就不展開。
EF處理並發分兩種情況,單個屬性並發檢查和時間戳(又叫行版本),單個屬性只保證單個字段並發修改,時間戳是保證整條數據的並發修改
我們在Student的Age加上[ConcurrencyCheck],在School加上行版本
[ConcurrencyCheck] public int Age { get; set; }
[Timestamp] public byte[] RowVersion { get; set; }
1. 模擬Age並發沖突

var student = _context.Student.Single(m => m.Id == 1); student.Age = 32; #region 模擬另外一個用戶修改了Age var task = Task.Run(() => { var options = HttpContext.RequestServices.GetService<DbContextOptions<Data.EFCoreDbContext>>(); using (var context = new Data.EFCoreDbContext(options)) { var student = context.Student.Single(m => m.Id == 1); student.Age = 23; context.SaveChanges(); } }); task.Wait(); #endregion try { _context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { _logger.LogError(ex, "database update error"); }
2. 數據庫數據
可以看到是Task里面的更新成功了
3. 異常信息
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded
異常信息描述很明確,就是數據庫操作期望1行被影響,實際是0行,數據可能被修改或刪除自從實體加載后
4. SQL

exec sp_executesql N'SET NOCOUNT ON; UPDATE [Student] SET [Age] = @p0 WHERE [Id] = @p1 AND [Age] = @p2; SELECT @@ROWCOUNT; ',N'@p1 int,@p0 int,@p2 int',@p1=1,@p0=23,@p2=25 exec sp_executesql N'SET NOCOUNT ON; UPDATE [Student] SET [Age] = @p0 WHERE [Id] = @p1 AND [Age] = @p2; SELECT @@ROWCOUNT; ',N'@p1 int,@p0 int,@p2 int',@p1=1,@p0=32,@p2=25
加上並發檢查的字段會在where條件后面加上原始值,Timestamp也是一樣道理,只是Timestamp是每次(插入/更新)數據庫會更新這個字段,數字遞增的形式。
5. 解決並發沖突
要解決上面沖突,先要介紹EF Core里面三組數值
原始值:實體從數據庫加載時的值 (例子:Age = 25)
當前值:實體當前的值 (例子:Age = 32)
數據庫值:當前數據庫中的值 (例子:Age = 23)

public void SaveToDb() { var student = _context.Student.Single(m => m.Id == 1); student.Age = 32; //模擬另外一個用戶修改了Age var task = Task.Run(() => { var options = HttpContext.RequestServices.GetService<DbContextOptions<Data.EFCoreDbContext>>(); using (var context = new Data.EFCoreDbContext(options)) { var student = context.Student.Single(m => m.Id == 1); student.Age = 23; context.SaveChanges(); } }); task.Wait(); //到這,另外一個線程已經將Age修改成23 var trySave = 0; //若並發沖突異常,重試3次 while (trySave++ < 3) { if (TrySaveData()) break; } bool TrySaveData() { try { _context.SaveChanges(); return true; } catch (DbUpdateConcurrencyException ex) { _logger.LogError(ex, $"database update concurrency exception : retry: {trySave}"); //3次嘗試保存失敗,拋出異常等上層處理,不應該吃掉異常,不然返回成功,實際保存沒成功 if (trySave >= 3) throw ex; //若沖突不是當前處理的對象,拋出異常等上層處理 if (!ex.Entries.Any(m => m.Entity is Models.Student)) throw ex; var entry = ex.Entries.Select(m => m).Single(m => m.Entity is Models.Student); //獲取當前實體值 var currentValues = entry.CurrentValues; //獲取數據庫值 var databaseValues = entry.GetDatabaseValues(); //這里獲取當前需要修改的字段 var property = currentValues.Properties.FirstOrDefault(m => m.Name == nameof(student.Age)); var currentValue = currentValues[property]; var databaseValue = databaseValues[property]; //這里賦值多個選擇方案,1. 使用當前值 2. 使用數據庫值 3. 處理后的值(例如余額,數據庫余額 - 當前余額 & 大於0) currentValues[property] = currentValue; // 刷新原始值,這里原始值是做並發檢查 entry.OriginalValues.SetValues(databaseValues); return false; } } }

數據庫更新為我們預期的值
轉發請標明出處:https://www.cnblogs.com/WilsonPan/p/11792722.html
示例代碼:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/EFCore