【ASP.NET Core學習】Entity Framework 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)

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 = currentValues[property];
            var databaseValue = databaseValues[property];

            //這里賦值多個選擇方案,1. 使用當前值 2. 使用數據庫值 3. 處理后的值(例如余額,數據庫余額 - 當前余額 & 大於0)
            currentValues[property] = currentValue;

            // 刷新原始值,這里原始值是做並發檢查
            entry.OriginalValues.SetValues(databaseValues);

            return false;
        }
    }
}
View Code

數據庫更新為我們預期的值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM