帶有審計表的實體框架核心(EF Core)

目錄

介紹

背景

使用代碼

主(Main)方法

運行程序

IAudited

Model1.cs

Model1.Partial.cs

部分類CUsers

TVUT類

局部類Model1

OnModelCreatingPartial

GetTableName

保存更改

摘要


介紹

本文提供了將Entity Framework Core與現有SQL Server表一起使用的示例。這些表具有更新觸發器,該觸發器將記錄的當前版本複製到相應的審計表,並使用新的TraceVersionUTimeStamp更新記錄。

示例表:

實體框架類:

背景

數據庫中的所有表都有四個附加列用於審計目的:

  1. UserIdint):修改記錄的用戶Id
  2. Deletedbit):指示是否刪除記錄
  3. TraceVersionint):記錄的版本號
  4. UTimeStampdatetime):上次修改的日期和時間

SQL操作執行以下操作:

  1. INSERTInsert上沒有觸發器,記錄將按原樣插入到表中。數據訪問層可確保Deleted=0TraceVersion=1UTimeStamp=當前日期和時間。
  2. UPDATE:有一個AFTER UPDATE觸發器。如果
    • Deleted=0:將表中的當前記錄插入到審計表中,然後更新當前記錄,將TraceVersion自增加1,並將UTimeStamp設置爲當前日期和時間。
    • Deleted=1:與Deleted=0一樣,但除此之外,更新的記錄(帶有Deleted=1)也插入到審計表中,並從主表中刪除。
  3. DELETEAFTER DELETE觸發器禁止該DELETE語句。記錄的刪除必須通過將Deleted列更新爲1(如軟刪除)來完成。

例如,以下語句將在數據庫中產生以下記錄:

INSERT ABC_Users(UserId,Deleted,TraceVersion,UTimeStamp,NTUser,FName,LName,Active) 
VALUES(1,0,1,GETDATE(),'gmeyer','George','Meyer',1)

將一條記錄插入到主表中:

Table

Id

UserId

Deleted

TraceVersion

UTimeStamp

NTUser

FName

LName

Active

ABC_Users

2

1

0

1

2019-09-10 11:08:23.340

gmeyer

George

Meyer

1

 

UPDATE ABC_Users SET LName='Meyers' WHERE Id=2

 

當前記錄(帶有TraceVersion=1)被插入到Audit表中。更新後的記錄爲TraceVersion=2

Table

Id

UserId

Deleted

Trace
Version

UTimeStamp

NTUser

FName

LName

Active

ABC_Users_Audit

2

1

0

1

2019-09-10 11:08:23.340

gmeyer

George

Meyer

1

ABC_Users

2

1

0

2

2019-09-10 11:17:03.640

gmeyer

George

Meyers

1

UPDATE ABC_Users SET Deleted=1

當前記錄(帶有TraceVersion=2)被插入到Audit表中。更新的記錄(帶有Deleted=1)將獲取TraceVersion=3並也添加到Audit表中。該記錄將從主表中刪除:

Table

Id

UserId

Deleted

Trace
Version

UTimeStamp

NTUser

FName

LName

Active

ABC_Users_Audit

2

1

0

1

2019-09-10 11:08:23.340

gmeyer

George

Meyer

1

ABC_Users_Audit

2

1

0

2

2019-09-10 11:17:03.640

gmeyer

George

Meyers

1

ABC_Users_Audit

2

1

0

3

2019-09-10 11:17:44.020

gmeyer

George

Meyers

1

ABC_Users中沒有記錄。

下面是創建表和觸發器以及插入管理員用戶的SQL語句:

DROP TABLE IF EXISTS ABC_Users
GO
CREATE TABLE [dbo].[ABC_Users](
    [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [UserId] [int] NOT NULL,
    [Deleted] [bit] NOT NULL,
    [TraceVersion] [int] NOT NULL,
    [UTimeStamp] [datetime] NOT NULL,
    [NTUser] [varchar](50) NOT NULL,
    [FName] [varchar](20) NOT NULL,
    [LName] [varchar](50) NOT NULL,
    [Active] [bit] NOT NULL,
 CONSTRAINT [IX_ABC_Users] UNIQUE NONCLUSTERED ([NTUser] ASC) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[ABC_Users]  WITH CHECK ADD  CONSTRAINT [FK_ABC_Users_ABC_Users] _
                               FOREIGN KEY([UserId])
REFERENCES [dbo].[ABC_Users] ([Id])
GO

DROP TABLE IF EXISTS ABC_Users_Audit
GO
CREATE TABLE [dbo].[ABC_Users_Audit](
    [Id] [int] NOT NULL,
    [UserId] [int] NOT NULL,
    [Deleted] [bit] NOT NULL,
    [TraceVersion] [int] NOT NULL,
    [UTimeStamp] [datetime] NOT NULL,
    [NTUser] [varchar](50) NOT NULL,
    [FName] [varchar](20) NOT NULL,
    [LName] [varchar](50) NOT NULL,
    [Active] [bit] NOT NULL,
 CONSTRAINT [PK_ABC_Users_Audit] PRIMARY KEY CLUSTERED ([Id] ASC, 
            [TraceVersion] ASC) ON [PRIMARY]) ON [PRIMARY]
GO

---------- AUDIT TRIGGER SCRIPT FOR TABLE ABC_Users---------------
CREATE TRIGGER [dbo].[trABC_Users_AUDIT_UD] ON [dbo].[ABC_Users]
AFTER UPDATE, DELETE
AS
/* If no rows were affected, do nothing */
IF @@ROWCOUNT=0
    RETURN

SET NOCOUNT ON
BEGIN TRY
    DECLARE @Counter INT, @Now DATETIME
    SET @Now = GETDATE()
    /* Check the action (UPDATE or DELETE) */
    SELECT @Counter = COUNT(*)
    FROM INSERTED
    IF @Counter = 0 -->; DELETE
        THROW 50000, 'DELETE action is prohibited for ABC_Users', 1

    /* Insert previous record to Audit */
    INSERT INTO ABC_Users_Audit([Id],[UserId],[Deleted], _
                [TraceVersion],[UTimeStamp],[NTUser],[FName],[LName],[Active])  
    SELECT d.[Id],d.[UserId],d.[Deleted],d.[TraceVersion],_
                  d.[UTimeStamp],d.[NTUser],d.[FName],d.[LName],d.[Active]
    FROM DELETED d

    /* Update master record TraceVersion, UTimeStamp */
    UPDATE main
    SET main.TraceVersion = d.TraceVersion + 1, main.UTimeStamp = @Now
    FROM ABC_Users main
    INNER JOIN DELETED d ON d.Id = main.Id
    INNER JOIN INSERTED i ON i.Id = main.Id

    /* Process deleted rows */
    IF NOT EXISTS(SELECT 1 FROM INSERTED WHERE Deleted = 1)
        RETURN
    /* Re-insert last updated master record into Audit table where Deleted = 1 */
    INSERT INTO ABC_Users_Audit([Id],[UserId],[Deleted],[TraceVersion],_
                                [UTimeStamp],[NTUser],[FName],[LName],[Active])  
    SELECT d.[Id],d.[UserId],d.[Deleted],d.[TraceVersion],d.[UTimeStamp],_
                             d.[NTUser],d.[FName],d.[LName],d.[Active]
    FROM ABC_Users d
    INNER JOIN INSERTED i ON d.Id = i.Id
    WHERE i.Deleted = 1

    /* Delete master record */
    DELETE c
    FROM ABC_Users c
    INNER JOIN INSERTED i ON c.Id = i.Id
    WHERE i.Deleted = 1
END TRY
BEGIN CATCH
    THROW
END CATCH
GO

ALTER TABLE [dbo].[ABC_Users] ENABLE TRIGGER [trABC_Users_AUDIT_UD]
GO

INSERT ABC_Users(UserId,Deleted,TraceVersion,UTimeStamp,NTUser,FName,LName,Active)
VALUES(1,0,1,GETDATE(),'admin','Admin','Admin',1)

實體框架爲每個更新的Entity創建一個SQL UPDATE語句,但不創建一個SELECT語句來檢索由觸發更新的TraceVersionUTimeStamp實體框架爲每個已刪除的Entity創建一條SQL DELETE語句,但是在這種情況下,需要使用一條UPDATE語句將列Deleted設置爲1

使用代碼

該項目是一個控制檯應用程序。

已安裝以下Nuget軟件包:

Install-Package Microsoft.Extensions.Logging.Console
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer

(Main)方法

Program.csMain方法完全按照上述語句插入、更新和刪除記錄,但是使用:SQLEntity Framework

static void Main(string[] args)
{
    try
    {
        AbcUsers user;
        var optionsBuilder =
            new DbContextOptionsBuilder<model1>()
            .UseSqlServer(GetConnectionString())
            .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
        Console.WriteLine("Adding user");
        using (var context = new Model1(optionsBuilder.Options))
        {
            var dateNow = DateTime.Now;
            user = new AbcUsers()
            {
                UserId = 1,
                Ntuser = "gmeyer",
                Fname = "George",
                Lname = "Meyer",
                Active = true
            };
            context.AbcUsers.Add(user);
            context.SaveChanges();
            Console.WriteLine("user.Id={0}", user.Id);
            WriteChangeTrackerCount(context);
        }
        Console.WriteLine("Updating user");
        using (var context = new Model1(optionsBuilder.Options))
        {
            context.AbcUsers.Attach(user);
            user.Lname = "Meyers";
            context.SaveChanges();
            Console.WriteLine("user.TraceVersion={0}", user.TraceVersion);
            WriteChangeTrackerCount(context);
        }
        Console.WriteLine("Deleting user");
        using (var context = new Model1(optionsBuilder.Options))
        {
            context.AbcUsers.Attach(user);
            context.AbcUsers.Remove(user);
            context.SaveChanges();
            Console.WriteLine("context.Entry(user).State={0}", context.Entry(user).State);
            WriteChangeTrackerCount(context);
        }
        Console.WriteLine("Test ok");
    }
    catch (Exception ex)
    {
        Console.WriteLine("Test not ok");
        Console.WriteLine(ex.ToString());
    }
    Console.WriteLine("Press any key to close");
    Console.ReadKey();
}

運行程序

若要運行該程序,應在SQL Server上創建一個數據庫,並在該數據庫中,使用CreateTables.sql腳本中給定的SQL腳本創建兩個表。應該在Program.cs方法GetConnectionString中相應地修改連接字符串。在提供的連接字符串中,數據庫稱爲DB1。運行項目應創建以下輸出:

Adding user
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.3 initialized 'Model1' _
      using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (77ms) [Parameters=[@p0='?' (DbType = Boolean), _
      @p1='?' (DbType = Boolean), @p2='?' (Size = 20) (DbType = AnsiString), _
      @p3='?' (Size = 50) (DbType = AnsiString), @p4='?' _
      (Size = 50) (DbType = AnsiString), @p5='?' (DbType = Int32), _
      @p6='?' (DbType = Int32), @p7='?' (DbType = DateTime)], _
      CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [ABC_Users] ([Active], [Deleted], [FName], [LName], _
                  [NTUser], [TraceVersion], [UserId], [UTimeStamp])
      VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);
      SELECT [Id]
      FROM [ABC_Users]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
user.Id=2
ChangeTracker.Entries().ToList().Count=1
Updating user
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.3 initialized 'Model1' _
      using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (181ms) [Parameters=[@p1='?' (DbType = Int32), _
      @p0='?' (Size = 50) (DbType = AnsiString), @p2='?' (DbType = Int32)], _
      CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [ABC_Users] SET [LName] = @p0
      WHERE [Id] = @p1 AND [TraceVersion] = @p2;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TraceVersion, UTimeStamp FROM ABC_Users WHERE Id=2
user.TraceVersion=2
ChangeTracker.Entries().ToList().Count=1
Deleting user
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.3 initialized 'Model1' _
      using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (27ms) [Parameters=[@p1='?' (DbType = Int32), _
      @p0='?' (DbType = Boolean), @p2='?' (DbType = Int32)], _
      CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [ABC_Users] SET [Deleted] = @p0
      WHERE [Id] = @p1 AND [TraceVersion] = @p2;
      SELECT @@ROWCOUNT;
context.Entry(user).State=Detached
ChangeTracker.Entries().ToList().Count=0
Test ok
Press any key to close

IAudited

接口IAudited由所有實體實現。它定義了所有實體都有列IdDeletedTraceVersionUTimeStamp

interface IAudited
{
    int Id { get; set; }
    bool Deleted { get; set; }
    int TraceVersion { get; set; }
    DateTime UTimeStamp { get; set; }
}

Model1.cs

實體框架模型Model1.cs是使用以下命令在Nuget軟件包管理器控制檯中創建的:

Scaffold-DbContext 'data source=localhost;initial catalog=DB1;
integrated security=True;' Microsoft.EntityFrameworkCore.SqlServer 
-Context Model1 -F -DataAnnotations -Tables ABC_Users

上面的命令中的連接字符串可能需要調整,但是不必再次運行此命令。

Model1.Partial.cs

Scaffold-DbContext生成的類的自定義代碼可以放在此處。

部分類CUsers

每個審計的表必須實現接口IAudited

public partial class CUsers : IAudited { }

對於每個表,必須添加與上面相似的行。

TVUT

此類包含字段TraceVersionUTimeStamp。它用於在更新語句後重新加載這兩個值。

public class TVUT
{
    public int TraceVersion { get; set; }
    public DateTime UtimeStamp { get; set; }
}

局部類Model1

Model1類由Scaffold-DbContext命令生成。與該類有關的任何自定義代碼都放置在partial類中。它具有TVUTDbSet,因此可以編寫查詢來檢索TraceVersionUTimeSTamp

public partial class Model1
{
    public DbSet<tvut> TVUTs { get; set; }
    ...
}

OnModelCreatingPartial

在這個方法中,設置實體的特殊屬性。該TVUT實體被標記爲沒有key,並且該AbcUsers實體的TraceVersion字段設置爲併發令牌。這意味着,該字段被添加到UPDATEDELETE語句的WHERE子句中,例如:

UPDATE [dbo].[CUsers]
SET [LName] = @0
WHERE (([Id] = @1) AND ([TraceVersion] = @2))

這種方式實現了樂觀併發。

partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<tvut>(e => e.HasNoKey());
    modelBuilder.Entity<abcusers>(entity => entity.Property
                 (e => e.TraceVersion).IsConcurrencyToken(true));
}

對於每個表,AbcUsers必須將與上述實體行相似的行添加到該OnModelCreatingPartial函數中。

GetTableName

如果在不帶該選項-UseDatabaseNames的情況下運行Scaffold-DbContext,則Entity Framework通過刪除下劃線字符並將除第一個字符外的所有字符轉換爲小寫字母,從而從表名創建實體類。此函數用於檢索給定實體對象的表名。

private string GetTableName(object entity)
{
    var entityType = Model.FindEntityType(entity.GetType());
    return entityType.GetTableName();
}

保存更改

方法SaveChanges被覆蓋。

public override int SaveChanges()
{
    var entriesAudited = ChangeTracker.Entries().Where(e => e.Entity is IAudited);
    var entriesAdded = entriesAudited.Where(e => e.State == EntityState.Added).ToList();
    var entriesModified = entriesAudited.Where(e => e.State == EntityState.Modified).ToList();
    var entriesDeleted = entriesAudited.Where(e => e.State == EntityState.Deleted).ToList();
    foreach (var item in entriesAdded)
    {
        var entity = (IAudited)item.Entity;
        (entity.Deleted, entity.TraceVersion, entity.UtimeStamp) = (false, 1, DateTime.Now);
    }
    foreach (var item in entriesDeleted)
    {
        item.State = EntityState.Unchanged;
        ((IAudited)item.Entity).Deleted = true;
    }
    var rowCount = 0;
    using (var scope = new TransactionScope())
    {
        rowCount = base.SaveChanges();
        foreach (var item in entriesModified)
        {
            var entity = (IAudited)item.Entity;
            var sql = $"SELECT TraceVersion, _
                      UTimeStamp FROM {GetTableName(entity)} WHERE Id={entity.Id}";
            var tu = TVUTs.FromSqlRaw(sql).ToList()[0];
            (entity.TraceVersion, entity.UtimeStamp) = (tu.TraceVersion, tu.UtimeStamp);
        }
        scope.Complete();
    }
    if (rowCount > 0)
        foreach (var item in entriesDeleted)
            item.State = EntityState.Detached;
    return rowCount;
}
  1. 檢索審計的條目。
  2. 對於在審計項目,每個加入的實體,字段DeletedTraceVersionUTimeStamp被填充。
  3. 對於審計條目中每個已刪除的實體,將實體設置爲不變,然後將該Deleted字段設置爲1。這種情況類似於軟刪除,但是記錄從主表移至審計表。
  4. 創建一個新事務。
  5. 基類的SaveChanges 調用。
  6. 對於每個修改的實體,都會構造一條SQL語句來檢索TraceVersionUTimeStamp。在DbSet TVUT上使用FromSqlRaw執行SQL語句。檢索到後,會將值分配給實體。由於這兩個值的重新加載,因此需要事務。其他人可以在base.SaveChanges()的結束和TVUTs.FromSqlRaw(sql)開始之間更新實體。
  7. 對於每個刪除的實體,其State更改爲Detached,因此將其從Model1中刪除。

摘要

該項目表明可以創建一個Entity Framework Model

  1. 確保所有新的(添加的)實體都獲得Deleted=falseTraceVersion=1並且UTimeStamp=當前日期和時間。
  2. 重新加載所有更新實體,列TraceVersionUTimeStamp由觸發器給出。
  3. 將所有刪除更改爲帶有列Deleted=1的更新,並在保存後分離這些實體。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章