【ASP.NET Core學習】Entity Framework Core
這裡介紹在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) { } }
5.在ConfigureServices注入DbContext
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<Data.EFCoreDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); }
經過上面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)
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 var (trySave, isSave) = (0, false); while (!isSave && trySave++ < 3) { try { _context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { _logger.LogError(ex, "database update error"); foreach (var entry in ex.Entries) { if (entry.Entity is Models.Student) { var currentValues = entry.CurrentValues; var databaseValues = entry.GetDatabaseValues(); foreach (var property in currentValues.Properties) { var currentValue = currentValues[property]; var databaseValue = databaseValues[property]; //這裡選擇儲存哪個值,這裡簡單選擇當前(30)儲存到資料庫,實際可能還需處理,如餘額,就需要資料庫當前餘額 - 當前數值 currentValues[property] = currentValue; } // 重新整理原始值 entry.OriginalValues.SetValues(databaseValues); } } } }View Code
資料庫更新為我們預期的值
轉發請標明出處:https://www.cnblogs.com/WilsonPan/p/11792722.html
示例程式碼:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/EFCore