【ASP.NET Core學習】Entity Framework Core

来源:https://www.cnblogs.com/WilsonPan/archive/2019/11/11/11792722.html
-Advertisement-
Play Games

【ASP.NET Core學習】在ASP.NET Core 種使用Entity Framework Core介紹,包括如何添加Entity Framwork Core,創建模型和遷移到資料庫,查詢數據,保存數據,使用事務,處理併發衝突 ...


 這裡介紹在ASP.NET Core中使用EF Core,這裡資料庫選的是Sql Server

  1. 如何使用Sql Server
  2. 添加模型 && 資料庫遷移
  3. 查詢數據
  4. 保存數據

如何使用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=25
View Code

加上併發檢查的欄位會在where條件後面加上原始值,Timestamp也是一樣道理,只是Timestamp是每次(插入/更新)資料庫會更新這個欄位,數字遞增的形式。

5. 解決併發衝突

要解決上面衝突,先要介紹EF Core裡面三組數值

原始值:實體從資料庫載入時的值   (例子:Age = 25)

當前值:實體當前的值        (例子:Age = 32)

資料庫值:當前資料庫中的值     (例子:Age = 23)

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 背景 之前做的海量數據數據展示,在預處理速度和線上渲染上還有有所欠缺,本文中進行一些優化工作,使得九分鐘處理完一千多萬面數據的3 12級矢量切片,線上瀏覽數據請求時間控制在10s左右。 準備 軟體環境:PostGIS(3.0.0rc2 r17909)和 PostgreSQL( 12.0, compi ...
  • Eclipse部署多模塊項目到tomcat,啟動時找不到jar的解決方法。 ...
  • 在使用redis時,一般會設置一個過期時間,當然也有不設置過期時間的,也就是永久不過期。當設置了過期時間,redis是如何判斷是否過期,以及根據什麼策略來進行刪除的。 設置過期時間 expire key time(以秒為單位) 這是最常用的方式setex(String key, int second ...
  • scp是secure copy的簡寫,用於在Linux下進行遠程拷貝文件的命令,和它類似的命令有cp,不過cp只是在本機進行拷貝不能跨伺服器,而且scp傳輸是加密的。可能會稍微影響一下速度。當你伺服器硬碟變為只讀 read only system時,用scp可以幫你把文件移出來。另外,scp還非常不 ...
  • 你可能想創建一個在應用的任何地方都可以訪問的函數,這個教程將幫你實現 👏 很多教程都會說,你在 composer.json 這個文件中通過添加一個自動載入的文件,就可以實現這個需求。但我認為這不是一個好的方式,當你在 helpers.php 文件中添加了更多的函數時,可讀性將變得很差。 下麵我將介 ...
  • 在說正題之前先解釋一下交換機模式是個籠統的稱呼,它不是一個單獨的模式(包括了訂閱模式,路由模式和主題模式),交換機模式是一個比較常用的模式,主要是為了實現數據的同步。 首先,說一下訂閱模式,就和字面上的意思差不多主要就是一個生產者,多個消費者,同一個消息被多個消費者獲取,先看一下官網的圖示 整體執行 ...
  • 當創建隊列jobs、監聽器或訂閱伺服器以推送到隊列中時,您可能會開始認為,一旦分派,隊列工作器決定如何處理您的邏輯就完全由您自己決定了。 嗯……並不是說你不能從作業內部與隊列工作器交互,但是通常情況下,哪怕你做了,也是沒必要的。 這個神奇的騷操作的出現是因為“InteractsWithQueue”這 ...
  • 環境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔術方法並巧妙構造特殊屬性調用一系列函數或類方法以執行某種敏感操作的調用堆棧反序列化常用魔法函數前言我所理解的 POP Chain:利用魔 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...