2013-07-08 9 views
7

私は、データベースにソフト削除を実装するフレームワークを持っています(DeletedDateというNullable DateTime)。私はそうのような主要なエンティティの要求を処理するためのリポジトリを使用しています:ExpressionVisitorを使用して結合でソフト削除されたレコードを除外します

/// <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); 

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

    return query; 
} 

リポジトリは、子供と呼ばれるナビゲーションプロパティを持つ親と呼ばれるエンティティによって使用されているのであれば、AllIncluding方法が適切ソフト削除された親レコードを除外するだろうが、ソフト削除された子レコードは引き続き含まれます。

データベースに送信されたクエリを見ると、SQL結合句 "AND Children.DeletedDate IS NULL"に追加するだけで、正しい結果が返されます。

私の研究中、this postが私の必要としているようですが、私の実装ではポスターと同じ結果が得られませんでした。コードを実行すると、クエリの子要素に何も起きないように見えます。:

BaseClassの:

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; } 
    } 
} 

親クラス:

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 
    } 
} 

子供クラス:

:ここ

は私の現在の関連するコード(nugetからQueryInterceptorを使用して注意してください)です

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 } } 

コンテキストクラス:

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>(); 
     } 
    } 
} 

リポジトリクラス:

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クラス:

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クラス:

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クラス:

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インタフェース:

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インタフェース:

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

私は自分のプロジェクトで同じことをする必要があります。あなたはこれに解決策を得ましたか? – Colin

+0

まだ、私は動的に式を構築する方法を検討し始めましたが、別のプロジェクトでは取り除かれました。これを行う方法が必要なような気がするのですが、私は表現やExpressionVisitorクラスにまだ精通していません。 –

+1

SQLサーバーのバージョンに応じて、削除されていない句を持つビューとEFが対話する方が簡単です。次に、トリガーの代わりにいくつかを追加するだけで、すべてがうまくいくはずです。 – Aron

答えて

0

私は決して表現の訪問者を把握することはできませんでしたし、すでに十分な時間を費やしていました。だから、私はDeletedDateがnullでない場合にレコードを削除することによって、これをTable Triggerで処理するだけでした。

ソフト削除の元の目的は、アプリケーションで誰がレコードを削除したのかを追跡することでした。変更を保存するコンテキストでMod Userを設定していましたが、削除時にはこれが更新されないため、削除を行った人の監査はありません。

私は既に監査していた各テーブルに対して、「After Update」と「After Delete」トリガを持っていました。監査すると、各テーブルの監査テーブルが関連付けられました。トリガは、基本的に、更新または削除があるときはいつでも古いレコードを監査テーブルに挿入します。監査テーブルとトリガーは、ストアドプロシージャを使用して作成されています

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; 

トリガが理想的ではないが、それらは削除されていないと私はもはやソフト削除されたレコードを心配する必要があるユーザーを監査の目標を達成。

0

問題は、あなたのAllIncludingに含める()ステートメントを使用してcondititionを追加したいということです方法。 query interceptorパッケージはInclude()メソッドをサポートしていません。この作業を行うための解決策は、Includeステートメントを使用しないことだけです。

すべてはあなたが以下のような何かをしたときに動作します:上記が表示されますあなたをSQLに変換されるので

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

そのVatTypes.IsDeleted = 0は、クエリに追加されている場合。

includeAllメソッドが本当に必要ですか。これはデータベースからすべてを読み込んでいるため、パフォーマンスの観点から見ると巨大なオーバーヘッドに似ています。

編集:古いポストをもう一度読み終えたら、実際にはInterceptWithメソッドをInclude()ステートメントで使用できるように見えます。おそらくそれは、Include()に問題があるExpressionVisitorです。私はしばらく時間を見つけると、これを試してみると、あなたに戻ってきます。

-1

私は個人的に私はテーブルに "IsDeleted"列を追加するデザインパターンが嫌いです。理由は数多くあります。

  1. このパターンは内部プラットフォームを生成し、データベース内にデータベースがあります。テーブルに内部データベース(select * from table where IsDeleted = 0) と(delete from table becomes update table set IsDeleted = 1
  2. 余分なデータにアクセスするために必要な
  3. カスタムAPIは、パフォーマンスが低下し
  4. 余分なデータは、あなたが監査をしたい場合は、それを正しく行うには、監査の目的のために有用ではありません。

あなたが遭遇した痛み点は2です。カスタムAPI。 Entity Frameworkは、SQLデータベース内に存在する奇妙なデータストアではなく、SQLデータベースに対して動作するように作成されました。

私がこの問題を発見した解決策は、SQL Server Viewsを使用することです。 MS SQL Serverはビューをサポートしています。このビューはソフト削除をオンにして行をフィルタリングできます。私はTRIGGER INSTEAD OF INSERT,UPDATE, DELETEをビューに追加して、挿入/更新/削除をデータベースの正しいアクションにマッピングします。

ただし、任意の形式の抽象化を使用すると、パフォーマンスが低下することがあります。この場合、主な取引はSELECTです。 SQL Server Enterprise Editionでは、書き込みアクセスを犠牲にして、すべての選択肢をスピードアップするために、ビューにインデックスを追加すること(SQL Serverで自動的にインデックスを使用させること)が可能です。それは

  • ValidFrom DateTime NOT NULL
  • ValidTo DateTime NULL
  • EditedBy VARCHAR NOT NULL
...私が代わりに次のスキーマを使用する IsDeleted列の好む点4の場合と同様にポイント3.

の世話をします

新しい行を作成するときは、ValidFromをUTCNOW()に、そしてEditedByをCURRENTUSER()に設定します。行を更新するときは、古い行のValidToをUTCNOW()に設定し、正しい値で新しい行を作成します。 削除すると、古い行のValidToがUTCNOW()に設定されます。

このスキーマを使用すると、任意の時点でテーブルの完全な履歴ビューを作成できます。完全な監査。:)

+0

私はSoft Delete実装を嫌いますが、誰かがレコードを削除したことを追跡する必要があります。監査は、古いレコードを監査テーブルに挿入するトリガーでセットアップされます。編集/削除ユーザーはC#コードで設定されているため、SQL CURRENTUSER()を使用することはできません。これは、アプリケーションが偽装するユーザーであり、すべての操作で同じになるためです。私は潜在的にビューを使用することができます。または、必要なデータが監査テーブルに記録されたら、削除トリガーでレコードを削除することができます。あなたの提案をありがとう、私は調査する必要があります。 –

関連する問題