代码之家  ›  专栏  ›  技术社区  ›  Biri

使用实体框架记录每个数据更改

  •  29
  • Biri  · 技术社区  · 16 年前

    客户需要与进行修改的实际用户一起将每次数据更改记录到日志记录表中。应用程序正在使用一个SQL用户访问数据库,但我们需要记录“真实”用户id。

    在t-sql中,我们可以通过为每个表插入和更新写入触发器,并使用上下文信息存储用户id。我们将用户id传递给存储过程,将用户id存储在上下文信息中,触发器可以使用此信息将日志行写入日志表。

    我找不到使用EF进行类似操作的地方或方法。因此,主要目标是:如果我通过EF对数据进行更改,我希望以半自动的方式将确切的数据更改记录到表中(因此我不希望在保存对象之前检查每个字段的更改)。我们正在使用EntitySQL。

    不幸的是,我们必须坚持使用SQL2000,所以SQL2008中引入的数据更改捕获不是一个选项(但可能这对我们来说也不是正确的方法)。

    注意:通过使用ObjectContext.SavingChanges eventhandler,我可以在其中插入SQL语句来初始化contextinfo。但是,我不能混合使用EF和标准SQL。因此,我可以获得EntityConnection,但无法使用它执行T-SQL语句。或者我可以获取EntityConnection的连接字符串并基于它创建一个SqlConnection,但它将是一个不同的连接,因此contextinfo不会影响EF进行的保存。

    我在SavingChanges处理程序中尝试了以下操作:

    testEntities te = (testEntities)sender;
    DbConnection dc = te.Connection;
    DbCommand dcc = dc.CreateCommand();
    dcc.CommandType = CommandType.StoredProcedure;
    DbParameter dp = new EntityParameter();
    dp.ParameterName = "userid";
    dp.Value = textBox1.Text;
    dcc.CommandText = "userinit";
    dcc.Parameters.Add(dp);
    dcc.ExecuteNonQuery();
    

    与SqlParameter相同,而不是EntityParameter:不能使用SqlParameter。

    StringBuilder cStr = new StringBuilder("declare @tx char(50); set @tx='");
    cStr.Append(textBox1.Text);
    cStr.Append("'; declare @m binary(128); set @m = cast(@tx as binary(128)); set context_info @m;");
    
    testEntities te = (testEntities)sender;
    DbConnection dc = te.Connection;
    DbCommand dcc = dc.CreateCommand();
    dcc.CommandType = CommandType.Text;
    dcc.CommandText = cStr.ToString();
    dcc.ExecuteNonQuery();
    

    因此,我一直致力于在实体框架和ADO.NET之间建立一座桥梁。

    8 回复  |  直到 7 年前
        1
  •  13
  •   Craig Stuntz    16 年前

    处理上下文如何。 SavingChanges ?

        2
  •  13
  •   Matt Johnson-Pint    14 年前

    我发现附加到连接的StateChanged事件并观察从not open到open的更改是最容易的。然后我调用设置上下文的proc,它每次都工作,即使EF决定重置连接。

    private int _contextUserId;
    
    public void SomeMethod()
    {
        var db = new MyEntities();
        db.Connection.StateChange += this.Connection_StateChange;
        this._contextUserId = theCurrentUserId;
    
        // whatever else you want to do
    }
    
    private void Connection_StateChange(object sender, StateChangeEventArgs e)
    {
        // only do this when we first open the connection
        if (e.OriginalState == ConnectionState.Open ||
            e.CurrentState != ConnectionState.Open)
            return;
    
        // use the existing open connection to set the context info
        var connection = ((EntityConnection) sender).StoreConnection;
        var command = connection.CreateCommand();
        command.CommandText = "proc_ContextInfoSet";
        command.CommandType = CommandType.StoredProcedure;
        command.Parameters.Add(new SqlParameter("ContextUserID", this._contextUserId));
        command.ExecuteNonQuery();
    }
    
        3
  •  10
  •   Biri    16 年前

    最后,在克雷格的帮助下,这里是一个概念证明。它需要更多的测试,但乍一看它正在工作。

    第一:我创建了两个表,一个用于数据,一个用于日志。

    -- This is for the data
    create table datastuff (
        id int not null identity(1, 1),
        userid nvarchar(64) not null default(''),
        primary key(id)
    )
    go
    
    -- This is for the log
    create table naplo (
        id int not null identity(1, 1),
        userid nvarchar(64) not null default(''),
        datum datetime not null default('2099-12-31'),
        primary key(id)
    )
    go
    

    第二:为insert创建触发器。

    create trigger myTrigger on datastuff for insert as
    
        declare @User_id int,
            @User_context varbinary(128),
            @User_id_temp varchar(64)
    
        select @User_context = context_info
            from master.dbo.sysprocesses
            where spid=@@spid
    
        set @User_id_temp = cast(@User_context as varchar(64))
    
        declare @insuserid nvarchar(64)
    
        select @insuserid=userid from inserted
    
        insert into naplo(userid, datum)
            values(@User_id_temp, getdate())
    
    go
    

    您还应该为update创建一个触发器,这将稍微复杂一些,因为它需要检查每个字段是否有更改的内容。

    日志表和触发器应该扩展以存储创建/更改的表和字段,但我希望您能理解。

    第三:创建一个存储过程,将用户id填充到SQL上下文信息中。

    create procedure userinit(@userid varchar(64))
    as
    begin
        declare @m binary(128)
        set @m = cast(@userid as binary(128))
        set context_info @m
    end
    go
    

    我们已经准备好了SQL端。C部分来了。

    创建项目并将EDM添加到项目中。EDM应包含datastuff表(或需要监视更改的表)和SP。

    现在对entity对象执行一些操作(例如添加一个新的datastuff对象)并钩住SavingChanges事件。

    using (testEntities te = new testEntities())
    {
        // Hook to the event
        te.SavingChanges += new EventHandler(te_SavingChanges);
    
        // This is important, because the context info is set inside a connection
        te.Connection.Open();
    
        // Add a new datastuff
        datastuff ds = new datastuff();
    
        // This is coming from a text box of my test form
        ds.userid = textBox1.Text;
        te.AddTodatastuff(ds);
    
        // Save the changes
        te.SaveChanges(true);
    
        // This is not needed, only to make sure
        te.Connection.Close();
    }
    

    在SavingChanges中,我们注入代码来设置连接的上下文信息。

    // Take my entity
    testEntities te = (testEntities)sender;
    
    // Get it's connection
    EntityConnection dc = (EntityConnection )te.Connection;
    
    // This is important!
    DbConnection storeConnection = dc.StoreConnection;
    
    // Create our command, which will call the userinit SP
    DbCommand command = storeConnection.CreateCommand();
    command.CommandText = "userinit";
    command.CommandType = CommandType.StoredProcedure;
    
    // Put the user id as the parameter
    command.Parameters.Add(new SqlParameter("userid", textBox1.Text));
    
    // Execute the command
    command.ExecuteNonQuery();
    

        4
  •  3
  •   Davy Landman    16 年前

    您是否尝试过将存储过程添加到实体模型中?

        5
  •  2
  •   CatGuardian    10 年前

    只需使用DbContext或ObjectContext强制执行SET-CONTEXT_-INFO即可:

    ...
    FileMoverContext context = new FileMoverContext();
    context.SetSessionContextInfo(Environment.UserName);
    ...
    context.SaveChanges();
    

    FileMoverContext继承自DbContext并具有SetSessionContextInfo方法。 下面是我的SetSessionContextInfo(…)的外观:

    public bool SetSessionContextInfo(string infoValue)
    {
       try
       {
          if (infoValue == null)
             throw new ArgumentNullException("infoValue");
    
          string rawQuery =
                       @"DECLARE @temp varbinary(128)
                         SET @temp = CONVERT(varbinary(128), '";
    
          rawQuery = rawQuery + infoValue + @"');
                        SET CONTEXT_INFO @temp";
          this.Database.ExecuteSqlCommand(rawQuery);
    
          return true;
       }
       catch (Exception e)
       {
          return false;
       }
    }
    

    现在,您只需设置一个数据库触发器,它可以访问CONTEXT_INFO(),并使用它设置一个数据库字段。

        6
  •  2
  •   Yorgo    6 年前

    我们用不同的方法解决了这个问题。

    • 从生成的实体容器类继承一个类
    • 使基本实体类抽象。您可以通过单独文件中的分部类定义来实现
    • 在继承的类中,使用方法定义中的new关键字隐藏SavingChanges方法
      1. a、 打开实体连接
      2. 调用base.SaveChanges()

    在代码中,必须使用继承的类。

        7
  •  0
  •   MAQ    9 年前

    1. 首先为所有CRUD操作创建一个通用存储库,如下面所示,这始终是一个好方法。 公共类GenericRepository:IGenericRepository,其中T:class

    2. 现在编写您的操作,如“publicvirtualvoidupdate(T entityToUpdate)”。

    3. 请参阅下面粘贴的文件/类以定义“LogEntity”函数。在这个函数中,在update和delete的情况下,我们将通过主键获得旧的实体以插入审计表中。为了识别主键并获取其值,我使用了反射。

    请在下面查找完整类的参考:

     public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        internal SampleDBContext Context;
        internal DbSet<T> DbSet;
    
        /// <summary>
        /// Constructor to initialize type collection
        /// </summary>
        /// <param name="context"></param>
        public GenericRepository(SampleDBContext context)
        {
            Context = context;
            DbSet = context.Set<T>();
        }
    
        /// <summary>
        /// Get query on current entity
        /// </summary>
        /// <returns></returns>
        public virtual IQueryable<T> GetQuery()
        {
            return DbSet;
        }
    
        /// <summary>
        /// Performs read operation on database using db entity
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="orderBy"></param>
        /// <param name="includeProperties"></param>
        /// <returns></returns>
        public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>,
                                                IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
        {
            IQueryable<T> query = DbSet;
    
            if (filter != null)
            {
                query = query.Where(filter);
            }
    
            query = includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
    
            if (orderBy == null)
                return query.ToList();
            else
                return orderBy(query).ToList();
        }
    
        /// <summary>
        /// Performs read by id operation on database using db entity
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public virtual T GetById(object id)
        {
            return DbSet.Find(id);
        }
    
        /// <summary>
        /// Performs add operation on database using db entity
        /// </summary>
        /// <param name="entity"></param>
        public virtual void Insert(T entity)
        {
            //if (!entity.GetType().Name.Contains("AuditLog"))
            //{
            //    LogEntity(entity, "I");
            //}
            DbSet.Add(entity);
        }
    
        /// <summary>
        /// Performs delete by id operation on database using db entity
        /// </summary>
        /// <param name="id"></param>
        public virtual void Delete(object id)
        {
            T entityToDelete = DbSet.Find(id);
            Delete(entityToDelete);
        }
    
        /// <summary>
        /// Performs delete operation on database using db entity
        /// </summary>
        /// <param name="entityToDelete"></param>
        public virtual void Delete(T entityToDelete)
        {
            if (!entityToDelete.GetType().Name.Contains("AuditLog"))
            {
                LogEntity(entityToDelete, "D");
            }
    
            if (Context.Entry(entityToDelete).State == EntityState.Detached)
            {
                DbSet.Attach(entityToDelete);
            }
            DbSet.Remove(entityToDelete);
        }
    
        /// <summary>
        /// Performs update operation on database using db entity
        /// </summary>
        /// <param name="entityToUpdate"></param>
        public virtual void Update(T entityToUpdate)
        {
            if (!entityToUpdate.GetType().Name.Contains("AuditLog"))
            {
                LogEntity(entityToUpdate, "U");
            }
            DbSet.Attach(entityToUpdate);
            Context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    
        public void LogEntity(T entity, string action = "")
        {
            try
            {
                //*********Populate the audit log entity.**********
                var auditLog = new AuditLog();
                auditLog.TableName = entity.GetType().Name;
                auditLog.Actions = action;
                auditLog.NewData = Newtonsoft.Json.JsonConvert.SerializeObject(entity);
                auditLog.UpdateDate = DateTime.Now;
                foreach (var property in entity.GetType().GetProperties())
                {
                    foreach (var attribute in property.GetCustomAttributes(false))
                    {
                        if (attribute.GetType().Name == "KeyAttribute")
                        {
                            auditLog.TableIdValue = Convert.ToInt32(property.GetValue(entity));
    
                            var entityRepositry = new GenericRepository<T>(Context);
                            var tempOldData = entityRepositry.GetById(auditLog.TableIdValue);
                            auditLog.OldData = tempOldData != null ? Newtonsoft.Json.JsonConvert.SerializeObject(tempOldData) : null;
                        }
    
                        if (attribute.GetType().Name == "CustomTrackAttribute")
                        {
                            if (property.Name == "BaseLicensingUserId")
                            {
                                auditLog.UserId = ValueConversion.ConvertValue(property.GetValue(entity).ToString(), 0);
                            }
                        }
                    }
                }
    
                //********Save the log in db.*********
                new UnitOfWork(Context, null, false).AuditLogRepository.Insert(auditLog);
            }
            catch (Exception ex)
            {
                Logger.LogError(string.Format("Error occured in [{0}] method of [{1}]", Logger.GetCurrentMethod(), this.GetType().Name), ex);
            }
        }
    }
    
    CREATE TABLE [dbo].[AuditLog](
    [AuditId] [BIGINT] IDENTITY(1,1) NOT NULL,
    [TableName] [nvarchar](250) NULL,
    [UserId] [int] NULL,
    [Actions] [nvarchar](1) NULL,
    [OldData] [text] NULL,
    [NewData] [text] NULL,
    [TableIdValue] [BIGINT] NULL,
    [UpdateDate] [datetime] NULL,
     CONSTRAINT [PK_DBAudit] PRIMARY KEY CLUSTERED 
    (
    [AuditId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = 
    OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
    
        8
  •  0
  •   A_Arnold    6 年前

    这就是我以前发现的 here 我修改了它,因为它不起作用

    private object GetPrimaryKeyValue(DbEntityEntry entry)
            {
                var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
                object o = objectStateEntry.EntityKey.EntityKeyValues[0].Value;
                return o;
            }
    
             private bool inExcludeList(string prop)
            {
                string[] excludeList = { "props", "to", "exclude" };
                return excludeList.Any(s => s.Equals(prop));
            }
    
            public int SaveChanges(User user, string UserId)
            {
                var modifiedEntities = ChangeTracker.Entries()
                    .Where(p => p.State == EntityState.Modified).ToList();
                var now = DateTime.Now;
    
                foreach (var change in modifiedEntities)
                {
    
                    var entityName = ObjectContext.GetObjectType(change.Entity.GetType()).Name;
                    var primaryKey = GetPrimaryKeyValue(change);
                    var DatabaseValues = change.GetDatabaseValues();
    
                    foreach (var prop in change.OriginalValues.PropertyNames)
                    {
                        if(inExcludeList(prop))
                        {
                            continue;
                        }
    
                        string originalValue = DatabaseValues.GetValue<object>(prop)?.ToString();
                        string currentValue = change.CurrentValues[prop]?.ToString();
    
                        if (originalValue != currentValue)
                        {
                            ChangeLog log = new ChangeLog()
                            {
                                EntityName = entityName,
                                PrimaryKeyValue = primaryKey.ToString(),
                                PropertyName = prop,
                                OldValue = originalValue,
                                NewValue = currentValue,
                                ModifiedByName = user.LastName + ", " + user.FirstName,
                                ModifiedById = UserId,
                                ModifiedBy = user,
                                ModifiedDate = DateTime.Now
                            };
    
                            ChangeLogs.Add(log);
                        }
                    }
                }
                return base.SaveChanges();
            }
    
    
    
    public class ChangeLog 
        {
            public int Id { get; set; }
            public string EntityName { get; set; }
            public string PropertyName { get; set; }
            public string PrimaryKeyValue { get; set; }
            public string OldValue { get; set; }
            public string NewValue { get; set; }
            public string ModifiedByName { get; set; }
    
    
    
            [ForeignKey("ModifiedBy")]
            [DisplayName("Modified By")]
            public string ModifiedById { get; set; }
            public virtual User ModifiedBy { get; set; }
    
    
            [Column(TypeName = "datetime2")]
            public DateTime? ModifiedDate { get; set; }
        }