Eine Herausforderung beim Arbeiten mit abgehängten Objekten ist es, nach dem Wiederanhängen herauszufinden, welche Datensätze eines Objektgraphen sich geändert haben. Zur Lösung dieser Herausforderung habe ich eine Implementierung der aus Entity Framework bekannten Self-Tracking-Entities umgesetzt.
Dabei stellt der Enum EntityState den aktuellen Zustand einer Entität dar. Das Interface IEntity, welche lediglich eine Eigenschaft State von diesem Typ vorgibt, ist von allen Self-Tracking-Entities zu implementieren. Alternativ dazu kann man auch von der Klasse Entity, welche zusätzlich ein paar Convenience-Methoden zum Ändern und Abfragen der Eigenschaft State bietet, abgeleitet werden.
public enum EntityState
{
Unmodified = 0,
Modified = 1,
Deleted = 2
}
public interface IEntity
{
EntityState State { get; set; }
}
public abstract class Entity : IEntity, ILifecycle
{
public virtual EntityState State { get; set; }
public virtual void MarkAsDeleted()
{
State = EntityState.Deleted;
}
public virtual void MarkAsModified()
{
State = EntityState.Modified;
}
public virtual void MarkAsUnModified()
{
State = EntityState.Unmodified;
}
public virtual LifecycleVeto OnDelete(NHibernate.ISession s)
{
return LifecycleVeto.NoVeto;
}
public virtual void OnLoad(NHibernate.ISession s, object id)
{
State = EntityState.Unmodified;
}
public virtual LifecycleVeto OnSave(NHibernate.ISession s)
{
return LifecycleVeto.NoVeto;
}
public virtual LifecycleVeto OnUpdate(NHibernate.ISession s)
{
return LifecycleVeto.NoVeto;
}
}
Self-Tracking-Entities sollten darüber hinaus sicherstellen, dass jeder Setter die Eigenschaft State auf Modified setzt. Hierfür bietet sich der Einsatz von Code-Generatoren an. Kann man das nicht sicherstellen, muss man diese Eigenschaft nach jeder Änderung manuell setzen, zum Beispiel im View-Model.
[Class(Name = "NHibernateSample.entities.Bestellung, NHibernateSample")]
public class Bestellung : Entity
{
public Bestellung()
{
Positionen = new Iesi.Collections.Generic.HashedSet<Position>();
}
protected int bestellungId;
protected DateTime bestellDatum { get; set; }
[Id(0, Name = "BestellungId")]
[Generator(1, Class = "hilo")]
public virtual int BestellungId
{
get { return bestellungId; }
set
{
bestellungId = value;
this.MarkAsModified();
}
}
[Property(0, Name = "BestellDatum")]
public virtual DateTime BestellDatum
{
get { return bestellDatum; }
set
{
bestellDatum = value;
this.MarkAsModified();
}
}
[Set(0, Name = "Positionen", Lazy = CollectionLazy.True,
Cascade = "NONE", Inverse = true)]
[Key(1, Column = "BestellungId")]
[OneToMany(2,
Class = "NHibernateSample.entities.Position,NHibernateSample")]
public virtual IesiCollections.ISet<Position> Positionen { get; set; }
public virtual void AddPosition(Position p)
{
Positionen.Add(p);
p.Bestellung = this;
this.MarkAsModified();
// p.State = EntityState.Modified;
}
}
[Class(Name = "NHibernateSample.entities.Position, NHibernateSample")]
public class Position : Entity
{
[Id(0, Name = "PositionId")]
[Generator(1, Class = "native")]
protected virtual int? PositionId { get; set; }
protected int anzahl;
[Property(0, Name = "Anzahl")]
public virtual int Anzahl
{
get { return anzahl; }
set
{
anzahl = value;
this.MarkAsModified();
}
}
private String pizza;
[Property(0, Name = "Pizza")]
public virtual String Pizza
{
get { return pizza; }
set
{
pizza = value;
this.MarkAsModified();
}
}
private Bestellung bestellung;
[ManyToOne(0, Name = "Bestellung", Column = "BestellungId")]
public virtual Bestellung Bestellung
{
get { return bestellung; }
set
{
bestellung = value;
this.MarkAsModified();
}
}
}
Für das Speichern des Objektgraphs wurden einige Erweiterungsmethoden für die Klasse Session eingerichtet. Attach führt je nach Status ein SaveOrUpdate oder ein Delete aus. AcceptChanges führt sämtliche Zustandsaufzeichnungen des übergebenen Sets wieder zurück. Dies ist nach dem Speichern von Objekte erforderlich. Somit wird u. a. verhindert, dass NHibernate versucht, eine zum Speichern oder Löschen markierte Entität erneut zu speichern bzw. zu löschen. AttachAll hängt ruft für alle Entitäten eines Sets Attach auf.
public static class SessionExtensions {
public static void Attach(this ISession session, IEntity obj) {
if (obj.State == EntityState.Modified)
{
session.SaveOrUpdate(obj);
}
else if (obj.State == EntityState.Deleted)
{
session.Delete(obj);
}
}
public static void AcceptChanges<T>(this Iesi.Collections.Generic.ISet<T> set)
{
foreach (IEntity entity in set)
{
entity.State = EntityState.Unmodified;
}
}
public static void AttachAll<T>(this ISession session, Iesi.Collections.Generic.ISet<T> set)
{
foreach (IEntity entity in set)
{
Attach(session, entity);
}
}
}
Das nachfolgende Listing zeigt, wie diese Erweiterungsmethoden einzusetzen sind. In der gezeigten Methode wird zunächst für die übergebene abgehängte Bestellung SaveOrUpdate aufgerufen; anschließend werden die Zustände der geänderten Bestellungen mit AttachAll an die Session übergeben. Am Ende werden mit MarkAsUnModified bzw. AcceptChanges die Zustände auf Unmodified zurückgesetzt.
class BestellungDAO
{
[…]
/// <summary>
/// Speichert Bestellung inkl. Positionen
/// </summary>
/// <param name="b"></param>
public void Save(Bestellung b)
{
using (ISession session = HibernateHelper.SessionFactory.OpenSession())
{
using (ITransaction trans = session.BeginTransaction())
{
session.SaveOrUpdate(b); // Keine Cascade !!
session.AttachAll(b.Positionen);
trans.Commit();
b.MarkAsUnModified();
b.Positionen.AcceptChanges();
}
}
}
}
Wird nun die nachfolgend gezeigte Methode ausgeführt, bleiben am Ende die Positionen 1 und 3 übrig, wobei bei Position 3 die Anzahl auf 4 erhöht wurde. Das schöne dabei ist, dass lediglich die geänderten Positionen vom zweiten Aufruf von Save an die Session übergeben wird.
public static void SaveBestellung()
{
BestellungDAO dao;
dao = new BestellungDAO();
Bestellung b = new Bestellung
{
BestellDatum = DateTime.Now,
};
Position p1 = new Position { Anzahl = 2, Pizza = "Test 1"};
b.AddPosition(p1);
Position p2 = new Position { Anzahl = 2, Pizza = "Test 2"};
b.AddPosition(p2);
Position p3 = new Position { Anzahl = 2, Pizza = "Test 3" };
b.AddPosition(p3);
dao.Save(b);
p2.Anzahl = 4;
p3.State = EntityState.Deleted;
dao.Save(b);
}
Weitere Möglichkeiten:
- Durch die Verwendung von Reflection und einer Tiefen- oder Breitensuche könnten alle geänderten Entitäten eines Objektgraphen mit nur einem Methodenaufruf an die Session übergeben werden.
- Diese Implementierung funktioniert lediglich für ISet aus den IesiCollections. Man sollte hier über eine allgemeine Implementierung nachdenken.