7

データベースにソフト削除を実装するフレームワークがあります(DeletedDateと呼ばれるNullable DateTime)。次のように、リポジトリを使用してメイン エンティティのリクエストを処理しています。

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

したがって、Children と呼ばれるナビゲーション プロパティを持つ Parent と呼ばれるエンティティによってリポジトリが使用されている場合、AllInclusive メソッドは、論理的に削除された親レコードを適切に除外しますが、論理的に削除された子レコードは引き続き含まれます。

データベースに送信されたクエリを見ると、実行する必要があるのは、SQL 結合句に " AND Children.DeletedDate IS NULL " を追加することだけで、クエリは正しい結果を返すようです。

調査中に、まさに必要なものと思われるこの投稿を見つけましたが、私の実装ではポスターと同じ結果が得られません。コードをステップ実行すると、クエリの Children 部分には何も起こらないようです。

これが私の現在の関連コードです(注:nugetからQueryInterceptorを使用)

ベースクラス:

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

子クラス:

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

3 に答える 3

0

問題は、AllInclude メソッドで Include() ステートメントを使用して条件を追加することです。queryinterceptor パッケージは、Include() メソッドをサポートしていません。これを機能させる唯一の解決策は、Include ステートメントを使用しないことです。

次のようなことをすると、すべてが機能します。

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

したがって、上記を sql に変換すると、Where VatTypes.IsDeleted = 0 がクエリに追加されることがわかります。

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

編集:いくつかの古い投稿をもう一度読んだ後、実際には Include() ステートメントで InterceptWith メソッドを使用できるはずです。おそらく、Include() に問題があるのは ExpressionVisitor です。時間を見つけたら、これを試して、あなたに戻ってきます。

于 2013-07-17T11:25:13.227 に答える
0

私はビジターという表現を理解することができず、すでに十分な時間を費やしていました。そのため、DeletedDate が null でない場合にレコードを削除することで、Table Trigger でこれを処理することになりました。

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

監査対象のテーブルごとに「更新後」トリガーと「削除後」トリガー、および各テーブルに関連付けられた監査テーブルが既にありました。トリガーは基本的に、更新または削除があるたびに古いレコードを監査テーブルに挿入します。監査テーブルとトリガーは、ストアド プロシージャによって作成されます。

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;

トリガーは理想的ではありませんが、削除したユーザーを監査するという目標を達成し、論理的に削除されたレコードについて心配する必要がなくなりました。

于 2013-09-19T15:58:22.880 に答える
-1

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

  1. パターンは内部プラットフォームを生成し、データベース内にデータベースがあります。
  2. 内部データベースへのアクセスに必要なカスタム API ( select * from table where IsDeleted = 0) および ( delete from table becomes update table set IsDeleted = 1)
  3. テーブル内の余分なデータはパフォーマンスを低下させます
  4. 余分なデータは監査目的には役に立ちません。監査が必要な場合は、適切に行ってください。

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

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

ただし、何らかの形の抽象化を使用すると、パフォーマンスが低下することがわかります。この場合、主なトレードオフはSELECT. SQL Server Enterprise Edition では、書き込みアクセスを犠牲にして、ビューにインデックスを追加し (SQL Server に自動的にインデックスを使用させる)、すべての選択を高速化することができます。それはポイント3の世話をします。

ポイント4については、IsDeleted列の代わりに次のスキーマを使用することを好みます...

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

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

このスキーマを使用すると、いつでもテーブルの完全な履歴を表示できます。完全な監査。:)

于 2013-08-07T05:02:10.070 に答える