复杂的NHibernate审计

4
我现在正在使用IPostUpdateEventListener接口进行更新审计日志记录,抓取旧值和新值,然后将每个更新的字段存储在“Audit”表中。这很好用,但有两个要求我难以满足:
1. 显示更新是哪个员工的。 2. 显示更新的“友好名称”。
对于第一个问题,我的第一反应是使用反射,在给定实体上查找并获取“Employee”属性,以找出它是为哪个员工更新的。但是,当你深入图形几个对象时,并没有自动方式返回给定的Employee对象,这个方法就会很快破裂。
解决第一个问题的想法从需要在每个对象上都有一个“Parent”属性,以便我可以遍历图形查找Employee类型(对我来说,这会使我们的域过于污染,只是一个简单的持久性问题),到使用单独的SQL作业来遍历外键并在事后填充Employee ID(迄今为止,我们都是基于代码的,我不想维护单独的SQL作业,因为那个SQL作业会非常恶心)。
至于第二个要求,我可以很好地得到实际更改的属性名称。对于我们80%-90%的字段,(正确格式化的)属性名称就是我们显示的内容,所以我可以根据Pascal大小写来分隔名称。然而,其余的字段由于各种原因不匹配。我们正在使用ASP.NET MVC和MvcContrib的Fluent HTML构建器,但即使我们修改了设置以至于在视图模型上有一个属性来覆盖字段名称应该是什么(因此将其放在代码中而不仅仅是视图中),也没有真正的方法将这些属性与被保存的域对象匹配。
最终的实用解决方案是在另一个服务中的每个更新操作之后调用审计日志记录服务,并根据需要传递字段名称和员工信息,但是,出于明显的原因,我真的不想去那里。
任何问题的想法都将不胜感激。搜索和思考几天都没有发现有用的东西-大多数人似乎只停留在简单的旧/新值记录或记录本身的“创建/更新”时间戳。
4个回答

3
我有一个类似于你的需求。在我的情况下,这是一个医疗保健应用程序,审计日志需要识别插入/更新适用的患者。
我的解决方案是定义一个接口,所有需要进行审计的类都需要实现该接口:
public interface IAuditedRecord
{
    IPatient OwningPatient { get; }

    ...
    // Other audit-related properties (user, timestamp)
}

审核过的课程然后以所需的任何方式实现这个接口。例如:
public class Medication : IAuditedRecord
{
    // One end of a bidirectional association. Populated by NHibernate.
    private IPatient _patient;

    IPatient OwningPatient { get { return _patient; } }
}

public class MedicationNote : IAuditedRecord
{
    // One end of a bidirectional association. Populated by NHibernate.
    private Medication _medication;

    IPatient OwningPatient { get { return _medication.OwningPatient; } }
}

然后,IPostInsertEventListenerIPostUpdateEventListener会获取OwningPatient属性以填充审计记录。

该解决方案的优点是将审计逻辑保留在事件侦听器中,这是唯一可以确定插入/更新将发生的地方,同时允许遍历对象和其所属患者之间的间接链接。

缺点是被审计类必须派生自特定接口。我认为这些好处超过了这个小成本。


有趣。我不久前也使用了类似的方法,只是在IEntity接口上我有一个setter,每个POCO都实现了它。我在存储库基类的构造函数中注入了User,当任何存储库类上调用persist(IEntity poco)操作时,它将调用持久化实体上的setter(我们将最后一个触摸记录的用户存储在记录本身上)。这使我们完全可以忽略事件监听器。然而,你的方法好处在于你可以挑选哪些类应该被审计。 - Nelson
有趣的是,这基本上就是我最终所做的。我在我们的IDomainEntity上定义了一个“EmployeeIdForAuditing”属性(只有getter),然后在基本的DomainEntity类中返回null。我们还需要知道每个条目是员工/公司级别的“编辑”,因此我们创建了一个带有信息的审计属性。任何标记有员工级别审计属性的IDomainEntity对象都需要实现该属性。这也使我能够编写一个测试来检查我们项目中所有对象是否符合这些要求。谢谢您发布这篇文章! - Darrell Mozingo

0
创建日志,您可以使用IPostInsertEventListener或IPostUpdateEventListener。如果您正在使用流畅的配置,则可以按照下面的示例进行配置。
引用: 事件将在提交后进行调用。
.ExposeConfiguration(c => c.EventListeners.PostCommitInsertEventListeners = new IPostInsertEventListener[] { new AuditEventPostInsert() })
.ExposeConfiguration(c => c.EventListeners.PostCommitUpdateEventListeners = new IPostUpdateEventListener[] { new AuditEventPostUpdate() });

0

这似乎是一个比简单审计跟踪更复杂的场景,因此我倾向于使用应用程序服务来处理。

  1. 它更易于测试。
  2. 它是明确的。
  3. 您不必诉诸反射或公共属性。

我们在应用程序中使用审计服务,即使我们的情况比您的简单得多,因为它是一个领域问题。我们的领域“知道”历史并与历史一起工作,它不仅仅是为了某些报告前端而创建的。

您是否记录正在进行更新的人(员工),还是员工是某种聚合根?


员工是一个聚合根。我们有一个简单的安全相关的“用户”来进行更改。审计仅用于记录和报告我们系统中的信息,但我们从未对其进行任何操作,这就是为什么我不愿将其移入服务中的原因。不幸的是,由于技术原因,我从一开始就有这种感觉,它最终必须结束在那里。 - Darrell Mozingo
如果你正在使用NHibernate作为ORM,我对你如何填充历史表很感兴趣,除非你正在使用触发器。干杯 - kkk

0
对于问题#1,我设计了以下内容: 在MyCommon程序集中,我声明了:

public interface IUserSessionStore
{
    UserSession Get();
    void Set(UserSession userSession);
}

每个请求都使用Ninject创建一个IUserSessionStore类的实例。这个类的实现简单地将UserSession对象存储在ASPNET MVC Session中。

有了这个,我可以在所有层中请求IUserSessionStore并获取UserSession,以便在每个对象中插入审核信息(下面的类将位于MyModel项目中):

public class AuditUpdater : DefaultSaveOrUpdateEventListener
{
    private IUserSessionStore userSessionStore;
    public AuditUpdater(IUserSessionStore userSessionStore)
    {
        this.userSessionStore = userSessionStore;
    }

    private Guid GetUserId()
    {
        return userSessionStore.Get().UserId;
    }

    private void UpdateAuditCreate(IAuditCreate auditable)
    {
        if (auditable != null)
        {
            auditable.CreationDate = DateTime.UtcNow;
            auditable.CreatedBy = GetUserId();
        }
    }

    .......
}

因此,您可以根据需要调整此内容以获取所需的员工信息。

很高兴听取更多建议!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接