2013-07-08 12 views
7

Mam framework, który implementuje Soft Deletes w bazie danych (Nullable DateTime nazywany DeletedDate). Używam repozytorium obsługiwać główne wnioski podmiot tak:Używanie ExpressionVisitor do wykluczania miękkich usuniętych rekordów w złączeniach

/// <summary> 
/// Returns a Linq Queryable instance of the entity collection. 
/// </summary> 
public IQueryable<T> All 
{ 
    get { return Context.Set<T>().Where(e => e.DeletedDate == null); } 
} 

Działa to doskonale, ale problem mam jest, kiedy to właściwości nawigacyjnych, i jak upewnić się, że tylko aktywne rekordy są pytani. Repozytorium metoda w kwestii zaczyna tak:

/// <summary> 
/// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded. 
/// </summary> 
/// <param name="includeProperties">Connected objects to be included in the result set.</param> 
/// <returns>An IQueryable collection of entity.</returns> 
public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
{ 
    IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null); 

    foreach (var includeProperty in includeProperties) 
    { 
     query = query.Include(includeProperty); 
    } 

    return query; 
} 

Więc jeśli repozytorium jest używany przez podmiot o nazwie macierzystego, który ma właściwość nawigacji o nazwie Dzieci, metoda AllIncluding byłoby właściwie odfiltrować miękkie usunięte rekordy rodzicem, ale w dalszym ciągu będą uwzględnione miękkie usunięte rekordy dzieci.

Patrząc na zapytanie wysłane do bazy danych, wydaje się, że wszystko, co należy zrobić, to dodać do klauzuli dołączenia sql "AND Children.DeletedDate IS NULL", a zapytanie zwróci poprawne wyniki.

Podczas moich poszukiwań znalazłem this post, który wydaje się być dokładnie tym, czego potrzebuję, jednak moja implementacja nie daje takich samych wyników jak plakat. Przechodząc przez kod, nic nie wydaje się dziać w części "Dzieci" zapytania.

Oto mój obecny odpowiedni kod (Uwaga: Korzystanie QueryInterceptor z Nuget):

klasy bazowej:

using System; 
using System.ComponentModel.DataAnnotations; 
using System.ComponentModel.DataAnnotations.Schema; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// Serves as the Base Class for All Data Model Classes 
    /// </summary> 
    public class BaseClass 
    { 
     /// <summary> 
     /// Default constructor, sets EntityState to Unchanged. 
     /// </summary> 
     public BaseClass() 
     { 
      this.StateOfEntity = DomainClasses.StateOfEntity.Unchanged; 
     } 

     /// <summary> 
     /// Indicates the current state of the entity. Not mapped to Database. 
     /// </summary> 
     [NotMapped] 
     public StateOfEntity StateOfEntity { get; set; } 

     /// <summary> 
     /// The entity primary key. 
     /// </summary> 
     [Key, Column(Order = 0), ScaffoldColumn(false)] 
     [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] 
     public int Id { get; set; } 

     /// <summary> 
     /// The date the entity record was created. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [Column(Order = 1, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime AddDate { get; set; } 

     /// <summary> 
     /// The UserName of the User who created the entity record. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [StringLength(56), Column(Order = 2), ScaffoldColumn(false)] 
     public string AddUser { get; set; } 

     /// <summary> 
     /// The date the entity record was modified. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [Column(Order = 3, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime ModDate { get; set; } 

     /// <summary> 
     /// The UserName of the User who modified the entity record. 
     /// </summary> 
     [StringLength(56), Column(Order = 4), ScaffoldColumn(false)] 
     public string ModUser { get; set; } 

     /// <summary> 
     /// Allows for Soft Delete of records. 
     /// </summary> 
     [Column(Order = 5, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime? DeletedDate { get; set; } 
    } 
} 

nadrzędna Klasa:

using System.Collections.Generic; 
using System.ComponentModel.DataAnnotations; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// The Parent Entity. 
    /// </summary> 
    public class Parent : BaseClass 
    { 
     /// <summary> 
     /// Instantiates a new instance of Parent, initializes the virtual sets. 
     /// </summary> 
     public Parent() 
     { 
      this.Children = new HashSet<Child>(); 
     } 

     #region Properties 

     /// <summary> 
     /// The Parent's Name 
     /// </summary> 
     [StringLength(50), Required, Display(Name="Parent Name")] 
     public string Name { get; set; } 

     #endregion 

     #region Relationships 
     /// <summary> 
     /// Relationship to Child, 1 Parent = Many Children. 
     /// </summary> 
     public virtual ICollection<Child> Children { get; set; } 

     #endregion 
    } 
} 

Klasa dziecka:

using System.Collections.Generic; 
using System.ComponentModel.DataAnnotations; 
using System.ComponentModel.DataAnnotations.Schema; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// The Child entity. One Parent = Many Children 
    /// </summary> 
    public class Child : BaseClass 
    { 
     #region Properties 

     /// <summary> 
     /// Child Name. 
     /// </summary> 
     [Required, StringLength(50), Display(Name="Child Name")] 
     public string Name { get; set; } 

     #endregion 

     #region Relationships 
     /// <summary> 
     /// Parent Relationship. 1 Parent = Many Children. 
     /// </summary> 
     public virtual Parent Parent { get; set; } 

     #endregion 
    } 
} 
0 Klasa

Kontekst:

using DomainClasses; 
using System; 
using System.Data; 
using System.Data.Entity; 
using System.Linq; 

namespace DataLayer 
{ 
    public class DemoContext : DbContext, IDemoContext 
    { 
     /// <summary> 
     /// ActiveSession object of the user performing the action. 
     /// </summary> 
     public ActiveSession ActiveSession { get; private set; } 

     public DemoContext(ActiveSession activeSession) 
      : base("name=DemoDb") 
     { 
      ActiveSession = activeSession; 
      this.Configuration.LazyLoadingEnabled = false; 
     } 

     #region Db Mappings 

     public IDbSet<Child> Children { get; set; } 
     public IDbSet<Parent> Parents { get; set; } 

     #endregion 

     public override int SaveChanges() 
     { 
      var changeSet = ChangeTracker.Entries<BaseClass>(); 

      if (changeSet != null) 
      { 
       foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged)) 
       { 
        entry.Entity.ModDate = DateTime.UtcNow; 
        entry.Entity.ModUser = ActiveSession.UserName; 

        if (entry.State == EntityState.Added) 
        { 
         entry.Entity.AddDate = DateTime.UtcNow; 
         entry.Entity.AddUser = ActiveSession.UserName; 
        } 
        else if (entry.State == EntityState.Deleted) 
        { 
         entry.State = EntityState.Modified; 
         entry.Entity.DeletedDate = DateTime.UtcNow; 
        } 
       } 
      } 

      return base.SaveChanges(); 
     } 

     public new IDbSet<T> Set<T>() where T : BaseClass 
     { 
      return ((DbContext)this).Set<T>(); 
     } 
    } 
} 

Repository Klasa:

using DomainClasses; 
using QueryInterceptor; 
using System; 
using System.Data.Entity; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    /// <summary> 
    /// Entity Repository to be used in Business Layer. 
    /// </summary> 
    public class EntityRepository<T> : IEntityRepository<T> where T : BaseClass 
    { 
     public IDemoContext Context { get; private set; } 

     /// <summary> 
     /// Main Constructor for Repository. Creates an instance of DemoContext (derives from DbContext). 
     /// </summary> 
     /// <param name="activeSession">UserName of the User performing the action.</param> 
     public EntityRepository(ActiveSession activeSession) 
      : this(new DemoContext(activeSession)) 
     { 
     } 

     /// <summary> 
     /// Constructor for Repository. Allows a context (i.e. FakeDemoContext) to be passed in for testing. 
     /// </summary> 
     /// <param name="context">IDemoContext to be used in the repository. I.e. FakeDemoContext.</param> 
     public EntityRepository(IDemoContext context) 
     { 
      Context = context; 
     } 

     /// <summary> 
     /// Returns a Linq Queryable instance of the entity collection. 
     /// </summary> 
     public IQueryable<T> All 
     { 
      get { return Context.Set<T>().Where(e => e.DeletedDate == null); } 
     } 

     /// <summary> 
     /// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded. 
     /// </summary> 
     /// <param name="includeProperties">Connected objects to be included in the result set.</param> 
     /// <returns>An IQueryable collection of entity.</returns> 
     public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
     { 
      IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null); 

      InjectConditionVisitor icv = new InjectConditionVisitor(); 

      foreach (var includeProperty in includeProperties) 
      { 
       query = query.Include(includeProperty); 
      } 

      return query.InterceptWith(icv); 
     } 

     /// <summary> 
     /// Finds a single instance of the entity by the Id. 
     /// </summary> 
     /// <param name="id">The primary key for the entity.</param> 
     /// <returns>An instance of the entity.</returns> 
     public T Find(int id) 
     { 
      return Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id); 
     } 

     /// <summary> 
     /// Takes a single entity or entity graph and reads the explicit state, then applies the necessary State changes to Update or Add the entities. 
     /// </summary> 
     /// <param name="entity">The entity object.</param> 
     public void InsertOrUpdate(T entity) 
     { 
      if (entity.StateOfEntity == StateOfEntity.Added) 
      { 
       Context.Set<T>().Add(entity); 
      } 
      else 
      { 
       Context.Set<T>().Add(entity); 
       Context.ApplyStateChanges(); 
      } 
     } 

     /// <summary> 
     /// Deletes the instance of the entity. 
     /// </summary> 
     /// <param name="id">The primary key of the entity.</param> 
     public void Delete(int id) 
     { 
      var entity = Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id); 
      entity.StateOfEntity = StateOfEntity.Deleted; 
      Context.Set<T>().Remove(entity); 
     } 

     /// <summary> 
     /// Saves the transaction. 
     /// </summary> 
     public void Save() 
     { 
      Context.SaveChanges(); 
     } 

     /// <summary> 
     /// Disposes the Repository. 
     /// </summary> 
     public void Dispose() 
     { 
      Context.Dispose(); 
     } 
    } 
} 

InjectConditionVisitor Klasa:

using System; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class InjectConditionVisitor : ExpressionVisitor 
    { 
     private QueryConditional queryCondition; 

     public InjectConditionVisitor(QueryConditional condition) 
     { 
      queryCondition = condition; 
     } 

     public InjectConditionVisitor() 
     { 
      queryCondition = new QueryConditional(x => x.DeletedDate == null); 
     } 

     protected override Expression VisitMember(MemberExpression ex) 
     { 
      // Only change generic types = Navigation Properties 
      // else just execute the normal code. 
      return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(queryCondition, ex) ?? base.VisitMember(ex); 
     } 

     /// <summary> 
     /// Create the where expression with the adapted QueryConditional 
     /// </summary> 
     /// <param name="condition">The condition to use</param> 
     /// <param name="ex">The MemberExpression we're visiting</param> 
     /// <returns></returns> 
     private Expression CreateWhereExpression(QueryConditional condition, Expression ex) 
     { 
      var type = ex.Type;//.GetGenericArguments().First(); 
      var test = CreateExpression(condition, type); 
      if (test == null) 
       return null; 
      var listType = typeof(IQueryable<>).MakeGenericType(type); 
      return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType); 
     } 

     /// <summary> 
     /// Adapt a QueryConditional to the member we're currently visiting. 
     /// </summary> 
     /// <param name="condition">The condition to adapt</param> 
     /// <param name="type">The type of the current member (=Navigation property)</param> 
     /// <returns>The adapted QueryConditional</returns> 
     private LambdaExpression CreateExpression(QueryConditional condition, Type type) 
     { 
      var lambda = (LambdaExpression)condition.Conditional; 
      var conditionType = condition.Conditional.GetType().GetGenericArguments().FirstOrDefault(); 
      // Only continue when the condition is applicable to the Type of the member 
      if (conditionType == null) 
       return null; 
      if (!conditionType.IsAssignableFrom(type)) 
       return null; 

      var newParams = new[] { Expression.Parameter(type, "bo") }; 
      var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement); 
      var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body); 
      lambda = Expression.Lambda(fixedBody, newParams); 

      return lambda; 
     } 
    } 
} 

QueryConditional Klasa:

using DomainClasses; 
using System; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class QueryConditional 
    { 
     public QueryConditional(Expression<Func<BaseClass, bool>> ex) 
     { 
      Conditional = ex; 
     } 

     public Expression<Func<BaseClass, bool>> Conditional { get; set; } 
    } 
} 

ParameterRebinder Klasa:

using System.Collections.Generic; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class ParameterRebinder : ExpressionVisitor 
    { 
     private readonly Dictionary<ParameterExpression, ParameterExpression> map; 

     public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map) 
     { 
      this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>(); 
     } 

     public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp) 
     { 
      return new ParameterRebinder(map).Visit(exp); 
     } 

     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      ParameterExpression replacement; 

      if (map.TryGetValue(node, out replacement)) 
       node = replacement; 

      return base.VisitParameter(node); 
     } 
    } 
} 

IEntityRepository Interfejs:

using System; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public interface IEntityRepository<T> : IDisposable 
    { 
     IQueryable<T> All { get; } 
     IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); 
     T Find(int id); 
     void InsertOrUpdate(T entity); 
     void Delete(int id); 
     void Save(); 
    } 
} 

IDemoContext Interfejs:

using DomainClasses; 
using System; 
using System.Data.Entity; 

namespace DataLayer 
{ 
    public interface IDemoContext : IDisposable 
    { 
     ActiveSession ActiveSession { get; } 

     IDbSet<Child> Children { get; } 
     IDbSet<Parent> Parents { get; } 

     int SaveChanges(); 

     IDbSet<T> Set<T>() where T : BaseClass; 
    } 
} 
+0

Muszę zrobić to samo w moim projekcie. Czy masz rozwiązanie tego? – Colin

+0

Jeszcze nie, zacząłem szukać dynamicznie budowania ekspresji, ale zostałem wzięty pod uwagę przy innym projekcie. Czuję, że musi być sposób, aby to zrobić, nie jestem jeszcze zaznajomiony z wyrażeniami i klasą ExpressionVisitor. –

+1

W zależności od wersji serwera SQL łatwiej jest mieć interakcję EF z widokami, które mają klauzulę, która nie została usunięta. Wtedy możesz dodać tylko niektóre, zamiast wyzwalaczy, i wszystko powinno działać. – Aron

Odpowiedz

0

Nigdy nie udało mi się rozgryźć gościa z ekspresji i spędziłem na nim wystarczająco dużo czasu. Więc skończyłem po prostu posługując się tym w wyzwalaczu tabeli, usuwając rekord, jeśli wartość DeletedDate nie była zerowa.

Pierwotnym celem miękkiego usunięcia było śledzenie, kto usunął rekord w aplikacji. Ustawiałem Mod Użytkownika w kontekście zapisywania zmian, ale po skasowaniu to nie jest aktualizowane, więc nie ma audytu tego, kto dokonał usunięcia.

Mam już wyzwalacz "Po aktualizacji" i "Po usunięciu" dla każdej z auditowanych przeze mnie tabeli oraz powiązaną tabelę kontroli dla każdej tabeli. Wyzwalacze po prostu wstawiają stary rekord do tabeli kontroli w dowolnym momencie aktualizacji lub usunięcia. Tabele kontrolne i wyzwalacze są tworzone poprzez procedury przechowywanej:

CREATE PROCEDURE [dbo].[CreateAuditTable](
    @TableName NVARCHAR(100), 
    @SchemaName NVARCHAR(50) 
) 
as 
/* 
----------------------------------------------------------------------------------------------------- 
* Procedure Name : dbo.CreateAuditTable 
* Author   : Josh Jay 
* Date    : 03/15/2013 
* Description  : Creates an Audit table from an existing table. 
----------------------------------------------------------------------------------------------------- 
Sl No  Date Modified  Modified By   Changes 
-------  -------------  ----------------- ------------------------------------------------- 
    1   07/01/2013   Josh Jay   Removed the table alias parameter and replaced usage with table name. 
    2   08/28/2013   Josh Jay   Modified the Update Statement to Delete the Row if it is a Soft Delete. 
----------------------------------------------------------------------------------------------------- 

Ex: 
EXEC dbo.CreateAuditTable 
    @TableName = 'Product', 
    @SchemaName = 'dbo' 

*/ 
BEGIN 
DECLARE @IssueCount INT = 0, 
     @IssueList NVARCHAR(MAX) = NULL, 
     @LineBreak NVARCHAR(50) = REPLICATE('-',50), 
     @CreateTableScript NVARCHAR(MAX) = NULL, 
     @CreateDeleteScript NVARCHAR(MAX) = NULL, 
     @CreateUpdateScript NVARCHAR(MAX) = NULL, 
     @ColumnNamesSection NVARCHAR(MAX) = NULL, 
     @TableObjectId INT, 
     @msg varchar(1024); 

--1) Check if table exists 
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName) 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The table ' + @SchemaName + '.' + @Tablename + ' does not exist.'; 
     END; 

--2) Check if audit table exists 
    IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName + '_Audit') 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The audit table ' + @SchemaName + '.' + @Tablename + '_Audit already exists. To recreate the audit table, please drop the existing audit table and try again.'; 
     END; 

--3) Check for existing triggers 
    IF EXISTS (SELECT 1 FROM sys.triggers tr INNER JOIN sys.tables t on tr.parent_id = t.object_id 
       WHERE t.schema_id = SCHEMA_ID(@SchemaName) AND t.name = @TableName AND tr.name LIKE 'tg_%Audit_%') 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') At least one audit trigger exists on the ' + @SchemaName + '.' + @Tablename + ' table. To recreate the audit table, please drop the audit triggers.'; 
     END; 

--4) Print errors if there are any 
    IF @IssueCount > 0 
     BEGIN 
      PRINT('There were ' + CONVERT(VARCHAR,@IssueCount) + ' issues found when attempting to create the audit table. Please correct the issues below before trying again.'); 
      PRINT(@LineBreak); 
      PRINT(@IssueList); 
      RETURN; 
     END; 

--5) Build Scripts 
    select 
     @CreateTableScript = 
      'CREATE TABLE [' + SS.name + '].[' + ST.name + '_Audit]' + CHAR(10) + 
      '(' + CHAR(10) + 
      CHAR(9) + '[AuditId] INT IDENTITY(1,1) NOT NULL CONSTRAINT [pk_' + @SchemaName + '.' + @Tablename + '_Audit_AuditId] PRIMARY KEY,' + CHAR(10) + 
      CHAR(9) + '[AuditDate] DATETIME NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditDate] DEFAULT (getutcdate()),' + CHAR(10) + 
      CHAR(9) + '[AuditIsDelete] BIT NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditIsDelete] DEFAULT ((0))', 
     @CreateDeleteScript = 
      'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Delete]' + CHAR(10) + 
      'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) + 
      'After Delete' + CHAR(10) + 
      'As Begin' + CHAR(10) + 
      CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) + 
      CHAR(9) + CHAR(9) + 'Return' + CHAR(10) + 
      CHAR(10) + 
      CHAR(9) + 'INSERT INTO' + CHAR(10) + 
      CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) + 
      CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]', 
     @CreateUpdateScript = 
      'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Update]' + CHAR(10) + 
      'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) + 
      'After Update' + CHAR(10) + 
      'As Begin' + CHAR(10) + 
      CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) + 
      CHAR(9) + CHAR(9) + 'Return' + CHAR(10) + 
      CHAR(10) + 
      CHAR(9) + 'INSERT INTO' + CHAR(10) + 
      CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) + 
      CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]' 
    from 
     sys.tables ST 
     INNER JOIN 
     sys.schemas SS ON ST.schema_id = SS.schema_id 
    WHERE 
     ST.name = @TableName AND 
     ST.type = 'U' AND 
     SS.name = @SchemaName 

    SELECT 
     @CreateTableScript = @CreateTableScript + ',' + CHAR(10) + CHAR(9) + '[' + ISC.COLUMN_NAME + '] ' + ISC.DATA_TYPE + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH IS NOT NULL AND ISC.DATA_TYPE <> 'xml' THEN '(' + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CONVERT(varchar,ISC.CHARACTER_MAXIMUM_LENGTH) END + ')' ELSE '' END + ' NULL', 
     @ColumnNamesSection = ISNULL(@ColumnNamesSection,'') + ',' + CHAR(10) + CHAR(9) + CHAR(9) + CHAR(9) + '[' + ISC.COLUMN_NAME + ']' 
    FROM 
     INFORMATION_SCHEMA.COLUMNS ISC 
    WHERE 
     ISC.TABLE_NAME = @TableName AND 
     ISC.TABLE_SCHEMA = @SchemaName 
    ORDER BY 
     ISC.ORDINAL_POSITION ASC 

    SET @CreateTableScript = @CreateTableScript + CHAR(10) + ');' 

    SET @CreateDeleteScript = @CreateDeleteScript + @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + ')' + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + '1 as [AuditIsDelete]' + 
     @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) + 
     'End;' 

    SET @CreateUpdateScript = @CreateUpdateScript + @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + ')' + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + '0 as [AuditIsDelete]' + 
     @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) + 
     'declare @SoftDelete bit, 
      @Id int 

    select 
     @SoftDelete = case when i.DeletedDate is not null then 1 else 0 end, 
     @Id = i.Id 
    from 
     inserted i; 

    if @SoftDelete = 1 
     begin 
      INSERT INTO 
       [' + @SchemaName + '].[' + @TableName + '_Audit] (
        [AuditIsDelete] 
        ' + @ColumnNamesSection + ' 
       ) 
       SELECT 
        1 as [AuditIsDelete] 
        ' + @ColumnNamesSection + ' 
       FROM 
        inserted 

      delete from ' + @SchemaName + '.' + @TableName + ' where Id = @Id 
     end;' + CHAR(10) + 

     'End;' 

--6) Print and Run Scripts 
    BEGIN TRY 
     BEGIN TRANSACTION; 

     EXEC(@CreateTableScript); 

     EXEC(@CreateDeleteScript); 

     EXEC(@CreateUpdateScript); 

     --Test Try Catch: 
     --SELECT 1/0 

     COMMIT TRANSACTION; 

     PRINT('The audit table was successfully created.') 
    END TRY 
    BEGIN CATCH 
     ROLLBACK TRANSACTION; 

     set @msg = 
      'db_name()=' + isnull(db_name(), 'NULL') + '; ERROR_MESSAGE()=' + 
      isnull(ERROR_MESSAGE(), 'NULL') + 
      '; ERROR_PROCEDURE()=' + isnull(ERROR_PROCEDURE(), 'NULL') + 
      '; ERROR_LINE()=' + isnull(CONVERT(varchar(10), ERROR_LINE()), 'NULL') +  
      '; ERROR_NUMBER()=' + isnull(CONVERT(varchar(10), ERROR_NUMBER()), 'NULL') + 
      '; ERROR_SEVERITY()=' + isnull(CONVERT(varchar(10), ERROR_SEVERITY()), 'NULL') + 
      '; ERROR_STATE()=' + isnull(CONVERT(varchar(10), ERROR_STATE()), 'NULL'); 

     PRINT(CHAR(10) + 'Create Audit Table Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateTableScript); 
     PRINT(@LineBreak); 

     PRINT(CHAR(10) + 'Create Audit Delete Trigger Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateDeleteScript); 
     PRINT(@LineBreak); 

     PRINT(CHAR(10) + 'Create Audit Update Trigger Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateUpdateScript); 
     PRINT(@LineBreak); 

     raiserror (@msg, 18, 1); 
    END CATCH 
END; 

Podczas wyzwalacze nie są idealne, ale zrealizować cele audytu użytkownika, który usunięty i już nie trzeba się martwić o miękkich usuniętych rekordów.

0

Problemem jest to, że chcesz dodać conditition pomocą include() oświadczenie w AllIncluding metoda. Pakiet queryceptor nie obsługuje metod Include(). Jedynym rozwiązaniem, które pozwala uzyskać tę pracę, nie jest użycie instrukcji Include.

Wszystko działa, kiedy robisz coś jak następuje:

Articles.Select(x => new { 
Vat = x.VatTypes 
}) 
.InterceptWith(Visitor); 

Więc gdy powyżej jest tłumaczony do sql cię zobaczy, że Gdzie VatTypes.IsDeleted = 0 jest dodany do zapytania.

Czy naprawdę konieczne jest użycie metody includeAll, wygląda to na OGROMNY narzut z perspektywy wydajności, ponieważ ładujesz wszystko z bazy danych.

EDYCJA: Po ponownym przeczytaniu kilku starszych postów wygląda na to, że metoda InterceptWith z użyciem instrukcji Include() powinna być możliwa. Być może jest to ExpressionVisitor, który ma problemy z Include(). Jeśli znajdę trochę czasu, spróbuję tego i skontaktuję się z tobą.

-1

Osobiście nienawidzę wzoru, w którym dodaje się kolumnę "IsDeleted" do tabeli. Powody są liczne.

  1. Wzór tworzy wewnętrzną platformę, w bazie danych znajduje się baza danych.
  2. niestandardowe API wymagane do uzyskania dostępu do wewnętrznej bazy danych (select * from table where IsDeleted = 0) i (delete from table becomes update table set IsDeleted = 1)
  3. Dodatkowe dane w tabeli zmniejsza wydajność
  4. Extra danych nie jest przydatna do celów kontroli, jeśli chcesz inspekcję, robić to właściwie.

Punktem bólu, który spotkałeś, jest 2. Niestandardowy interfejs API. Entity Framework został stworzony do pracy z bazami danych SQL, a nie z pewnym dziwnym magazynem danych, który istnieje wewnątrz bazy danych SQL.

Rozwiązaniem, które znalazłem w tym problemie, jest zastosowanie widoków SQL Server. MS SQL Server obsługuje Widoki, które można filtrować w wierszach z włączonym miękkim usuwaniem. Dodałbym wtedy TRIGGER INSTEAD OF INSERT,UPDATE, DELETE do widoku, aby zmapować twoje wstawki/aktualizacje/usunięcia do właściwych akcji w twojej bazie danych.

Jednak przy użyciu dowolnej formy abstrakcji wydajność spada. W tym przypadku głównym przedmiotem handlu jest SELECT. Z SQL Server Enterprise Edition możliwe jest dodanie indeksu do widoku (i SQL Server automagicznie używać indeksu), aby przyspieszyć wszystkie wybrane, kosztem prawa zapisu. Który dba o punkcie 3.

Co do punktu 4. Wolę zamiast kolumny IsDeleted aby wykorzystać następujący schemat ...

  • ValidFrom DateTime NOT NULL
  • ValidTo DateTime NULL
  • EditedBy VARCHAR NOT NULL

Po utworzeniu nowego wiersza ustawia się wartość ValidFrom na UTCNOW() i EditedBy na CURRENTUSER(). Podczas aktualizowania wiersza ustawia się ValidTo starego wiersza na UTCNOW() i tworzy nowy wiersz z poprawnymi wartościami. Po usunięciu ustaw wartość ValidTo starego wiersza na UTCNOW().

Ten schemat pozwala uzyskać pełny, historyczny widok tabeli w DOWOLNYM PUNKCIE W CZASIE. Pełny audyt.:)

+0

Nie podoba mi się również wdrożenie Soft Delete, ale dla sposób audytu jest zaimplementowany, konieczne jest śledzenie, kto usunął rekord. Inspekcja jest skonfigurowana z wyzwalaczami wstawiającymi stary rekord do tabeli kontroli. Użytkownik edytujący/usuwający jest ustawiony w kodzie C#, więc nie będzie działał przy użyciu SQL CURRENTUSER(), ponieważ byłby to użytkownik, którego aplikacja podszywa się i byłby taki sam dla wszystkich działań. Mogłabym używać widoków lub mógłbym usunąć wyzwalacz usunąć rekord, gdy niezbędne dane są rejestrowane w tabeli kontroli. Dzięki za sugestie, będę musiał zbadać. –

Powiązane problemy