Question

I have read on the inverse and cascade mapping attributes, and would like to know:

  • Whether is it possible to use them in my scenario? And if it is,
  • How to parameterize them accordingly?

Let's say I have two classes, Customer and Invoice, both needing traceability, TraceableEntity.

I'm using the Repository pattern for all of my entities, so the repositories are injected an NHibernate.ISession in there constructor. Indeed, I have a repository for each entity Customer and Invoice.

Because I'm in need of the user login, I think it is no concern for the business model, so I set it inside the repository Save method, since only the ISession is aware of the user used to connect to the underlying database, and the repository depends on it. This way, the business model is not polluted with useless information.

Aside, because of this need of traceability, I lose the power and ease of the inverse and cascade mapping attributes, or else, I don't know how to use them for my specific needs.

Let's take a look at the BaseRepository.Save() method.

public abstract class BaseRepository<T> where T : TraceableEntity {
    public BaseRepository(ISession session) { Session = session; }

    public ISession Session { get; private set; }

    public T Save(T instance) {
        if (instance.IsNew && instance.IsDirty) 
            instance.Creator = readLoginFromConnectionString();
        else if (!instance.IsNew && (instance.IsDirty || instance.IsDeleted))
            instance.Updater = readLoginFromConnectionString();
        Session.SaveOrUpdate(instance);
        return instance;
    }
}

TraceableEntity

public abstract class TraceableEntity {
    public TraceableEntity() { 
        Created = DateTime.Today; 
        IsNew = true; 
    }

    public virtual DateTime Created { get; set; }
    public virtual string Creator { get; set; }
    public virtual DateTime? Deleted { get; set; }
    public virtual int Id { get; protected set; }
    public virtual bool IsDeleted { get; set; }
    public virtual bool IsDirty { get; set; }
    public virtual bool IsNew { get; set; }
    public virtual DateTime? Updated { get; set; }
    public virtual string Updater { get; set; }
}

Customer

public class Customer : TraceableEntity {
    public Customer() : base() { Invoices = new List<Invoice>(); }

    public virtual Name { get; set; }
    public virtual Number { get; set; }
    public virtual IList<Invoice> Invoices { get; private set; }
}

Customer.hbm.xml

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"    
                   namespace="MyProject.Model" 
                   assembly="MyProject">
    <class name="Customer" table="CUSTOMERS">
        <id name="Id" column="CUST_ID" type="Int32" unsaved-value="0">
            <generator class="sequence-identity">
                <param name="sequence">CUST_ID_SEQ</param>
            </generator>
        </id>
        <property name="Name" column="CUST_NAME" type="String" length="128" not-null="true" />
        <property name="Number" column="CUST_NUMBER" type="String" length="12" not-null="true" />
        <property name="Creator" column="CUST_CREATOR_USR_ID" type="String" length="15" not-null="true" />
        <property name="Created" column="CUST_CREATED_DT" type="DateTime" not-null="true" />
        <property name="Updater" column="CUST_UPDATER_USR_ID" type="String" length="15" />
        <property name="Updated" column="CUST_UPDATED_DT" type="DateTime" not-null="false" />
        <property name="Deleted" column="CUST_DELETED_DT" type="DateTime" not-null="false" />
        <bag name="Invoices" table="INVOICES" fetch="join" lazy="true" inverse="true">
            <key column="CUST_ID" foreign-key="INV_CUST_ID_FK" />
            <one-to-many class="Invoice" />
        </bag>
    </class>
</hibernate-mapping>

Invoice

public class Invoice : TraceableEntity {
    public Invoice() : base() { }

    public virtual Customer Customer { get; set; }
    public virtual DateTime InvoiceDate { get; set; }
    public virtual string Number { get; set; }
    public virtual float Total { get; set; }
}

Invoice.hbm.xml

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"    
                   namespace="MyProject.Model" 
                   assembly="MyProject">
    <class name="Invoice" table="INVOICES">
        <id name="Id" column="INV_ID" type="Int32" unsaved-value="0">
            <generator class="sequence-identity">
                <param name="sequence">INV_ID_SEQ</param>
            </generator>
        </id>
        <property name="InvoiceDate" column="INV_DT" type="DateTime" not-null="true" />
        <property name="Number" column="INV_NUMBER" type="String" length="12" not-null="true" />
        <property name="Total" column="INV_TOTAL" type="decimal" not-null="true" />
        <property name="Creator" column="INV_CREATOR_USR_ID" type="String" length="15" not-null="true" />
        <property name="Created" column="INV_CREATED_DT" type="DateTime" not-null="true" />
        <property name="Updater" column="INV_UPDATER_USR_ID" type="String" length="15" />
        <property name="Updated" column="INV_UPDATED_DT" type="DateTime" not-null="false" />
        <property name="Deleted" column="INV_DELETED_DT" type="DateTime" not-null="false" />
        <many-to-one name="Customer" class="Customer" column="CUST_ID" />
    </class>
</hibernate-mapping>

This being said, I wish to know whether there is another maybe better way of doing this, because actually, I need, in the CustomerRepository, to override the BaseRepository.Save() method only to make a call to the InvoiceRepository.Save() method as follows:

public class CustomerRepository : BaseRepository<Customer> {
    public CustomerRepository(ISession session) : base(session) { }

    public override Customer Save(Customer instance) {
        instance = base.Save(instance);
        var invoices = new InvoiceRepository(session);
        instance.Invoices.ToList().ForEach(inv => {
            inv.Customer = instance;
            invoices.Save(inv)
        });
    }
}

public class InvoiceRepository : BaseRepository<Invoice> {
    public InvoiceRepository(ISession session) : base(session) { }        
}

Plus, I wonder if it is possible for the invoices to "know" who is the customer without having to assign the Customer property on save, and let NHibernate magic works it for me?

Was it helpful?

Solution

Add a listener to the pre-event and do your custom logic implementing either one of IPreDeleteEventListener, IPreInsertEventListener, IPreUpdateEventListener within the NHibernate.Event namespace.

A neat example by Ayende Rahien: NHibernate IPreUpdateEventListener & IPreInsertEventListener.

public class AuditEventListener : IPreInsertEventListener, IPreUpdateEventListener {
    public bool OnPreInsert(OnPreInsert @event) {
        var audit = @event.Entity as IHaveAuditInformation;
        if (audit == null) return false;

        var time = DateTime.Now;
        var name = WindowsIdentity.GetCurrent().Name;

        Set(@event.Persister, @event.State, "CreatedAt", time);
        Set(@event.Persister, @event.State, "CreatedBy", name);

        audit.CreatedAt = time;
        audit.CreatedBy = name;

        return false;
    }

    public bool OnPreUpdate(OnPreUpdate @event) {
        var audit = @event.Entity as IHaveAuditInformation;
        if (audit == null) return false;

        var time = DateTime.Now;
        var name = WindowsIndentity.GetCurrent().Name;

        Set(@event.Persister, @event.State, "UpdatedAt", time);
        Set(@event.Persister, @event.State, "UpdatedBy", name);

        audit.UpdatedAt= time;
        audit.UpdatedBy = name;

        return false;
    }
}

The same can be done with the IPreDeleteEventListener.

Notice the return value false. This should actually be one of the two OnPreEventResult enum values.

  • OnPreEventResult.Continue (false)
  • OnPreEventResult.Break (true)

As per the answer from @Radim Köhler to this question:

So, since the enum doesn't exist, instead of return true or false, I prefered to return the boolean value through another method call which actually says what it does explicitely.

private bool AbortOperation() { return true; }
private bool ContinueOperation() { return false; }

And replacing the return false by return ContinueOperation(). This makes the code clearer and reveals the exact intention and behaviour of the pre-event methods.

After the interfaces are implemented, just add the listener to the configuration.

var listener = new AuditEventListener();
Configuration cfg = new Configuration();
c.SetListener(ListenerType.PreDelete, listener);
c.SetListener(ListenerType.PreInsert, listener);
c.SetListener(ListenerType.PreUpdate, listener);

Now, all remain to do is a clean call to ISession.SaveOrUpdate(), having used the cascade="all" mapping attribute, and you're done!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top