<style>.cnblogs_code { width: auto; border-radius: 4px; box-shadow: 3px 2px 7px rgba(189, 183, 183, 1) } .ws-title { background-color: rgba(40, 120, 156, 1); font-size: 25px; font-family: "微軟雅黑"; padding: 8px 24px; border-radius: 4px; text-align: left; box-shadow: 1px 4px 5px 1px rgba(136, 136, 136, 1); color: rgba(255, 255, 255, 1); clear: both } .ws-content { margin-left: 20px; margin-top: 12px }</style>
這里介紹在ASP.NET Core中使用EF Core,這里資料庫選的是Sql Server
- 如何使用Sql Server
- 添加模型 && 資料庫遷移
- 查詢資料
- 保存資料
如何使用Sql Server
1. 安裝dotnet-ef(已經安裝忽略)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) { } }View Code
5.在ConfigureServices注入DbContext
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<Data.EFCoreDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); }View Code
經過上面5步,我們就可以在專案中使用資料庫,在需要的地方注入DbContext即可
添加模型
我們就以學校 -> 學生這樣的模型(一對多)為例,欄位也盡量簡潔,這里不是展示設計,以展示操作EF Core為主,所以類定義未必是最合適的, 學校類
[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; } }View Code
學生類
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; } }View Code
配置默認值
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()"); }View Code
模型定義好之后,我們需要把模型添加到DbContext
public DbSet<Models.School> Schools { get; set; } public DbSet<Models.Student> Students { get; set; }然后需要更新模型到資料庫,執行下面兩條命令 1. 新增一個遷移
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 };View Code
對應生成的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 };View Code
對應生成的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(), };View Code
對應生成的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();View Code
對應生成的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();View Code
同時在School,Student表保存資料,自動維護Student表的SchoolId欄位資料
二、級聯洗掉
var school = _context.School.Include(m => m.Students).FirstOrDefault(m => m.Name == "濟南大學"); _context.School.Remove(school); _context.SaveChanges();View Code 對應生成的Sql
--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;View Code
級聯洗掉要用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(); }View Code
下面是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"); }View Code
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=25View Code
加上并發檢查的欄位會在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 =https://www.cnblogs.com/WilsonPan/p/ currentValues[property]; var databaseValue =https://www.cnblogs.com/WilsonPan/p/ databaseValues[property]; //這里賦值多個選擇方案,1. 使用當前值 2. 使用資料庫值 3. 處理后的值(例如余額,資料庫余額 - 當前余額 & 大于0) currentValues[property] = currentValue; // 重繪原始值,這里原始值是做并發檢查 entry.OriginalValues.SetValues(databaseValues); return false; } } }View Code
資料庫更新為我們預期的值
轉發請標明出處:https://www.cnblogs.com/WilsonPan/p/11792722.html
示例代碼:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/EFCore
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/94909.html
標籤:.NET Core
