我正在尝试为Nhibernate编写一个挂钩到PreUpdate事件的审计跟踪。我有一个AuditLogEntry类(when,who等),它包含一个AuditLogEntryDetails列表(即更改的各个属性)。如果我将AuditLogEntry类与正在审计的实体隔离,那么我的代码运行时没有错误。但是,如果我将AuditLogEntry列表添加到正在审核的实体中,那么我的代码会抛出一个
集合[DomainObjects.AuditTracking.AuditLogEntry.Details]未由 齐平()
当我尝试将修改后的列表保存在事件侦听器中时,断言失败。只有当审计项目在列表中已有一个(或多个)AuditLogEntry实例时,才会发生这种情况。如果没有条目,则创建新列表并将其添加到正在审计的实体中,这很好。
我认为通过将问题隔离到上面,它似乎是(懒惰)加载现有列表以添加AuditLogEntry的新实例。但是我无法继续前进。将“Lazy =”False“'添加到列表映射似乎没有帮助。我真的在使用NHibernate的早期,从HN 3.0 Cookbook和这个blog post借用了概念。我的代码与此非常相似,但尝试将审核历史记录添加到列表中正在审核的项目中(因此我认为我还需要在pre,而不是post update事件中执行此操作)。
有问题的实体接口/类的快照是:
public class AuditLogEntry : Entity
{
public virtual AuditEntryTypeEnum AuditEntryType { get; set; }
public virtual string EntityFullName { get; set; }
public virtual string EntityShortName { get; set; }
public virtual string Username { get; set; }
public virtual DateTime When { get; set; }
public virtual IList<AuditLogEntryDetail> Details { get; set; }
}
public interface IAuditTrackedEntity
{
Guid Id { get; }
IList<AuditLogEntry> ChangeHistory { get; set; }
}
public class AuditTrackedEntity : StampedEntity, IAuditTrackedEntity
{
public virtual IList<AuditLogEntry> ChangeHistory { get; set; }
}
public class LookupValue : AuditTrackedEntity
{
public virtual string Description { get; set; }
}
对于我的映射:
AuditTrackedEntry.hbm.xml:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="DomainObjects" namespace="DomainObjects.AuditTracking">
<class name="AuditLogEntry">
<id name="Id">
<generator class="guid.comb" />
</id>
<version name="Version" />
<property name="AuditEntryType"/>
<property name="EntityFullName"/>
<property name="EntityShortName"/>
<property name="Username"/>
<property name="When" column="`When`"/>
<list name ="Details" cascade="all">
<key column="AuditLogEntryId"/>
<list-index column="DetailsIndex" base="1"/>
<one-to-many class="AuditLogEntryDetail"/>
</list>
</class>
</hibernate-mapping>
lookupvalue.hbm.xml:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="DomainObjects" namespace="DomainObjects">
<class name="LookupValue">
<id name="Id">
<generator class="guid.comb" />
</id>
<discriminator type="string">
<column name="LookupValueType" unique-key="UQ_TypeName" not-null="true" />
</discriminator>
<version name="Version" />
<property name="Description" unique-key="UQ_TypeName" not-null="true" />
<property name="CreatedBy" />
<property name="WhenCreated" />
<property name="ChangedBy" />
<property name="WhenChanged" />
<list name ="ChangeHistory">
<key column="EntityId"/>
<list-index column="ChangeIndex" base="1"/>
<one-to-many class="DomainObjects.AuditTracking.AuditLogEntry"/>
</list>
</class>
</hibernate-mapping>
EventListener PreUpdate事件处理程序调用以下代码: 导致问题的行在代码块的末尾附近进行注释
public void TrackPreUpdate(IAuditTrackedEntity entity, object[] oldState, object[] state, IEntityPersister persister, IEventSource eventSource)
{
if (entity == null || entity is AuditLogEntry)
return;
var entityFullName = entity.GetType().FullName;
if (oldState == null)
{
throw new ArgumentNullException("No old state available for entity type '" + entityFullName +
"'. Make sure you're loading it into Session before modifying and saving it.");
}
var dirtyFieldIndexes = persister.FindDirty(state, oldState, entity, eventSource);
var session = eventSource.GetSession(EntityMode.Poco);
AuditLogEntry auditLogEntry = null;
foreach (var dirtyFieldIndex in dirtyFieldIndexes)
{
if (IsIngoredProperty(persister, dirtyFieldIndex))
continue;
var oldValue = GetStringValueFromStateArray(oldState, dirtyFieldIndex);
var newValue = GetStringValueFromStateArray(state, dirtyFieldIndex);
if (oldValue == newValue)
{
continue;
}
if (auditLogEntry == null)
{
auditLogEntry = new AuditLogEntry
{
AuditEntryType = AuditEntryTypeEnum.Update,
EntityShortName = entity.GetType().Name,
EntityFullName = entityFullName,
Username = Environment.UserName,
//EntityId = entity.Id,
When = DateTime.Now,
Details = new List<AuditLogEntryDetail>()
};
//**********************
// The next three lines cause a problem when included,
// collection [] was not processed by flush()
//**********************
if (entity.ChangeHistory == null)
entity.ChangeHistory = new List<AuditLogEntry>();
entity.ChangeHistory.Add(auditLogEntry);
session.Save(auditLogEntry);
}
var detail = new AuditLogEntryDetail
{
//AuditLogEntryId = auditLogEntry.Id,
PropertyName = persister.PropertyNames[dirtyFieldIndex],
OldValue = oldValue,
NewValue = newValue
};
session.Save(detail);
auditLogEntry.Details.Add(detail);
}
session.Flush();
}
如前所述,在此配置中,我得到断言失败“ collection []未由flush()”处理。如果我删除上面的三行和lookupcode.hmb.xml中的列表映射,那么一切都按预期工作,除了被审计的实体不再包含对它自己的审计项的引用。
答案 0 :(得分:4)
我们面临着非常相似的问题,完全相同的例外,但在不同的情况下。尚未找到解决方案...
我们有NH事件监听器实现用于审计日志的IPreUpdateEventListener
和OnPreUpdate
方法。一切都很好,简单的属性更新,脏检查很好,但懒惰集合有问题。更新某个具有延迟集合的对象并访问事件侦听器OnPreUpdate
方法中的任何对象字段时,会抛出与上述相同的异常。当lazy
设置为false时,问题就会消失。
所以似乎懒惰集合存在一些问题(并且在保存之前不会影响集合初始化)。我们的问题与创建新的收藏品无关;只读取现有对象,只有从事件监听器访问的字段才会导致问题。
所以在你的情况下,也许,lazy
只能为关联设置为false才能解决问题,但另一方面,你可能真的想让收集变得懒惰。很难说,如果问题有解决方案或者必须使用IInterceptor
。
答案 1 :(得分:2)
好的,我发现了你的问题,这条线确实导致了这个问题。
Details = new List<AuditLogEntryDetail>()
在保存之前无法初始化空集合,因为EntityPersister不会保留集合,但是如果集合尚未处理则会出错。
此外,一旦nHibernate调用事件侦听器,级联就不起作用(不确定这是否是设计的)。因此,即使您稍后将详细信息项添加到集合中,您也只是在详细信息上调用save,而不是父项,因此不会传播更改。我建议重新分解,以便按此顺序完成项目......
详细信息,然后保存,
AuditLogEntry,然后保存,
实体,然后更新。
答案 2 :(得分:2)
使用EventListener时遇到了完全相同的问题。我一个接一个地遍历属性以检测更改,包括枚举集合。但是当我使用NHibernateUtil.IsInitialized(collection)
添加对集合的检查时,问题就消失了。我不会忽略和忽略AssertionFailure异常,因为它可能有未知的副作用。