這裏介紹在ASP.NET Core中使用EF Core,這裏數據庫選的是Sql Serverhtml
如何使用Sql Servergit
dotnet tool install --global dotnet-ef
2. 添加包Microsoft.EntityFrameworkCore.Designgithub
dotnet add package Microsoft.EntityFrameworkCore.Design
3. 添加包Microsoft.EntityFrameworkCore.SqlServersql
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便可app
添加模型ide
[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; } }
學生類ui
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; } }
配置默認值spa
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的執行計劃
2、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 支持的聚合運算符以下所示
3、左鏈接
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]
4、小結
全聯接時避免使用導航屬性連表
默認狀況是跟蹤查詢,這表示能夠更改這些實體實例,而後經過 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字段數據
2、級聯刪除
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把子項也包含到實體
3、使用事務
默認狀況下,若是數據庫提供程序支持事務,則會在事務中應用對 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提交到數據庫執行,外面包一層事務,因此事務裏面要儘量的控制操做最少,時間最少
4、併發衝突
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)
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); } } } }
數據庫更新爲咱們預期的值
轉發請標明出處:https://www.cnblogs.com/WilsonPan/p/11792722.html
示例代碼:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/EFCore