Каскадирование с прослеживаемостью с использованием NHibernate?

Я прочитал об атрибутах сопоставления inverse и cascade и хотел бы знать:

  • Возможно ли их использование в моем сценарии? И если да,
  • Как их соответствующим образом параметризовать?

Допустим, у меня есть два класса, Customer и Invoice, оба нуждаются в отслеживаемости, TraceableEntity.

Я использую шаблон репозитория для всех своих сущностей, поэтому репозитории вводятся в конструктор NHibernate.ISession. Действительно, у меня есть репозиторий для каждой сущности Customer и Invoice.

Поскольку мне нужен логин пользователя, я думаю, что это не касается бизнес-модели, поэтому я установил его в методе сохранения репозитория, поскольку только ISession знает о пользователе, используемом для подключения к базовой базе данных, и репозиторий зависит от него. Таким образом, бизнес-модель не загрязняется бесполезной информацией.

Кроме того, из-за этой потребности в отслеживаемости я теряю силу и простоту атрибутов сопоставления inverse и cascade, иначе я не знаю, как использовать их для своих конкретных нужд.

Давайте рассмотрим метод BaseRepository.Save().

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>

При этом я хотел бы знать, есть ли другой, возможно, лучший способ сделать это, потому что на самом деле мне нужно в CustomerRepository переопределить метод BaseRepository.Save() только для вызова метода InvoiceRepository.Save() следующим образом:

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

Кроме того, мне интересно, возможно ли, чтобы счета «знали», кто является клиентом, без необходимости назначать свойство «Клиент» при сохранении, и пусть магия NHibernate работает для меня?


person Will Marcouiller    schedule 17.02.2014    source источник


Ответы (1)


Добавьте прослушиватель к предварительному событию и выполните собственную логику, реализующую одно из IPreDeleteEventListener, IPreInsertEventListener, IPreUpdateEventListener в пространстве имен NHibernate.Event.

Хороший пример от 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;
    }
}

То же самое можно сделать с IPreDeleteEventListener.

Обратите внимание на возвращаемое значение false. На самом деле это должно быть одно из двух значений перечисления OnPreEventResult.

  • OnPreEventResult.Continue (ложь)
  • OnPreEventResult.Break (правда)

Согласно ответу @Radim Köhler на этот вопрос:

Итак, поскольку перечисление не существует, вместо возврата true или false я предпочел вернуть логическое значение через другой вызов метода, который на самом деле явно говорит, что он делает.

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

И замените return false на return ContinueOperation(). Это делает код более понятным и раскрывает точное назначение и поведение предсобытийных методов.

После того, как интерфейсы реализованы, просто добавьте прослушиватель в конфигурацию.

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

Теперь все, что осталось сделать, это чистый вызов ISession.SaveOrUpdate(), используя атрибут сопоставления cascade="all", и все готово!

person Will Marcouiller    schedule 18.02.2014