Zum Inhalt

NHibernate: Resultate Transformieren mit DistinctRootEntityResultTransformer

Ich hatte vor einiger Zeit ein recht mühsames Problem: Obwohl ich Objekte nach deren Id (Primärschlüssel) aus der DB geholt habe sind diese mehrmals in meinem Resultat erschienen. Wie so oft war der Fehler eigentlich eine Kleinigkeit, doch solange man gar nicht auf die Idee kommt an der richtigen Stelle danach zu suchen steht man vor einem grossen Mysterium.

Ausgangslage

Für das stark vereinfachte Beispiel nutze ich die 3 Klassen Order, OrderItem und Product. Die Objekte dienen nur zum ablegen der Daten und verfügen über keine Geschäftslogik. Das Feld Id ist jeweils der Primärschlüssel der gleichnamigen Tabellen. OrderItem ist sowohl mit Product wie auch mit Order verbunden.

Ein Test schlägt fehl

Mit dem untenstehenden Test werden die nötigen Objekte angelegt und danach versucht die Order anhand der Id zu laden.

[TestMethod]
public void ReproduceTheProblem()
{
    using (ISession session = PersistenceManager.OpenSession())
    {
        // Arrange
        Order order = new Order { Number = "000001" };
        AddDataToOrder(session, order);

        // Act
        List<Order> orders = GetOrderById(session, order.Id);

        // Assert
        Assert.AreEqual(1, orders.Count);
        // ==> Assert.AreEqual failed. Expected:<1>. Actual:<2>.
    }
}

private static List<Order> GetOrderById(ISession session, int id)
{
    var result = session.CreateCriteria(typeof(Order))
                        .Add(Expression.Eq("Id", id))
                        .List<Order>();

    return result.ToList();
}

Das Resultat in diesem Test ist allerdings nicht wie erwartet eine 1, sondern eine 2. Schaut man sich das generierte SQL-Query an kann man auch erkennen was das Problem ist:

SELECT
    this_.Id as Id1_1_, 
    this_.Number as Number1_1_, 
    items2_.OrderId as OrderId3_, 
    items2_.Id as Id3_, 
    items2_.Id as Id2_0_, 
    items2_.OrderId as OrderId2_0_, 
    items2_.Quantity as Quantity2_0_, 
    items2_.ProductId as ProductId2_0_ 
FROM dbo.[Order] this_ 
left outer join dbo.OrderItem items2_ on this_.Id=items2_.OrderId 
WHERE this_.Id = 3;

Hier wird nicht einfach nur ein SELECT gemacht, sondern das Resultat wird noch mit einem JOIN verknüpft. Meine Erwartung war das nur eine Zeile mit den Daten für das von mir gewünschte Objekt zurück geliefert wird. Durch den JOIN werden nun aber auch alle dazugehörigen OrderItems geladen:

"2 Zeilen statt der erwarteten einen Zeile"

Ursache

Der JOIN wurde von NHibernate nicht einfach aus lauter Freude gemacht. Die dafür nötige Anweisung stand so im Mapping:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="NHDistinct" namespace="NHDistinct.Model">
  <class name="Order" table="`Order`" schema="dbo">
    <id name="Id">
      <generator class="identity" />
    </id>
    <property name="Number" />
    <bag name="Items" cascade="all-delete-orphan" inverse="true" fetch="join">
      <key column="OrderId"/>
      <one-to-many class="NHDistinct.Model.OrderItem"/>
    </bag>
  </class>
</hibernate-mapping>

Diese explizite Schreibweise hat den gleichen Effekt als würde man die Funktion zum holen der Daten so umschreiben:

1
2
3
4
5
6
7
8
9
rivate static List<Order> GetOrderById(ISession session, int id)
{
    var result = session.CreateCriteria(typeof(Order))
                        .Add(Expression.Eq("Id", id))
                        .SetFetchMode("Items", FetchMode.Join)
                        .List<Order>();

    return result.ToList();
}

Obwohl das Ergebnis gleich ist, sieht man so auf den ersten Blick das ein wenig mehr Daten kommen werden als man als Nutzer der Methode vermuten würde. (Klare Methodennamen wären wie so oft eine grosse Hilfe gewesen).

Lösung

Das Mapping durfte nicht verändert werden, da das so erzwungene Verhalten fürs gesamte Projekt gesehen Sinn machte. Auch war ein erzwungenes nicht laden der OrderItems für die weitere Verarbeitung ungünstig. Nach einigem Suchen wurde die Funktion schliesslich um eine Zeile erweitert:

private static List<Order> GetOrderByIdFixed(ISession session, int id)
{
    var result = session.CreateCriteria(typeof(Order))
                        .Add(Expression.Eq("Id", id))
                        .SetFetchMode("Items", FetchMode.Join)
    /*  NEU: ===>  */   .SetResultTransformer(new DistinctRootEntityResultTransformer()) 
                        .List<Order>();

    return result.ToList();
}

DistinctRootEntityResultTransformer nimmt das Resultat der Abfrage und transformiert dieses wieder in die Root Entitäten. NHibernate packt auch ohne diese Zeile die OrderItems ins Order-Objekt, so aber merkt es dass es nur einen Order gibt und liefert entsprechend auch nur noch eines zurück.

Fazit

Wenn man mit OR-Mappern arbeitet sollte man bei der Entwicklung immer einen Blick auf die generierten Abfragen werfen. Meistens macht es das Richtige aber für den kleinen Spezialfall den man nun gerade braucht gibt es halt ab und zu ein klein wenig Nacharbeit. Was wieder mal zeigt: Trotz OR-Mappern sollte man als Entwickler doch ein wenig Ahnung von SQL haben.

Danksagung

Ich möchte hier noch Patrick Weibel danken. Ich durfte für den Blogpost seine Klasse PersistenceManager.cs aus dem ORM-Vortrag bei der .Net User Group Bern verwenden. Die Klasse zusammen mit den zahlreichen Mappings zum Nachschauen hat mir ermöglicht ein Minimal-Beispiel zusammen zu stellen, das man fürs selber experimentieren auf BitBucket herunterladen kann.