【ASP.NET Core學習】在ASP.NET Core 種使用Entity Framework Core介紹,包括如何添加Entity Framwork 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) { } }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)