Startseite > .Net > NHibernate: Resultate Transformieren mit DistinctRootEntityResultTransformer

NHibernate: Resultate Transformieren mit DistinctRootEntityResultTransformer

30. August 2011

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:

 
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:

private 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.

Schlagworte:
  1. 31. August 2011 um 07:52

    Schöner Artikel Johnny
    Da würden wir uns doch freuen wenn Du bei der DNUG Bern einen Vortrag machen würdest… Fallstricke bei OR-Mappern…

    • 31. August 2011 um 09:40

      Hallo Dänu,
      Das Thema tönt interessant. Aber um „Fallstricke bei OR-Mapper“ als Vortrag zu bringen müsste ich mehr haben als nur den DistinctRootEntityResultTransformer. Wenn Du dazu passende Probleme/Fallstricke hast können wir gerne weiterschauen.

      Gruss Johnny

  2. 9. September 2011 um 01:59

    Das ist eine der hässlichsten der vielen leaky Abstractions von Hibernate/NHibernate…

    Das Schöne ist, dass der Linq Provider von Hibernate es scheinbar richtig macht:

    // Act
    IList orders = session.Query().Fetch(o => o.Items).Where(o => o.Id == order.Id).ToList();

    Auch mit HQL ist es “schöner” als mit der Criteria-API:

    IList orders = session.CreateQuery(“select distinct o from Order o left join fetch o.Items where o.Id = :id “).SetParameter(“id”, order.Id).List();

    Bei allen Lösungen (inkl. DistinctRootEntityResultTransformer) ist anzumerken, dass die Filterung im Client geschieht. D.h. mit dem kartesischen Produkt des Joins werden “zu viele” Daten von der DB zum Server transportiert und erst im Client mit “CPU-Power” wieder denormalisiert.

    Das ist besonders Hässlich, weil Paging (SetFirstresult, SetMaxResults) mit diesen Lösungen nicht funktioniert! Paging wird auf dem SQL-Resultset ausgeführt, es werden also eine gegebne Anzahl Rows an den Client geliefert, was dann aber in einer völlig andern Anzahl Entities resultieren kann!
    Für Lösungen zu diesem Problem siehe:
    http://stackoverflow.com/questions/545940/do-you-know-of-anyway-to-get-a-distinct-result-set-without-using-the-resulttransf/
    http://stackoverflow.com/questions/545940/do-you-know-of-anyway-to-get-a-distinct-result-set-without-using-the-resulttransf/

    Übrigens: Kennst du das property create für die Hinernate Konfiguration?
    Damit könntest du dir in solchen Beispiel-Projekten die SQL-Create-Skripts ersparen. Oder gibt es einen Grund dagegen?

    Und noch ein winziges Detail: Verwendung des statischen Properties Transformers.DistinctRootEntity finde ich persönlich eleganter als new DistinctRootEntityResultTransformer().

    • 9. September 2011 um 06:38

      Hallo Jonas,
      Danke für deinen Input. Muss mir das create Property einmal genauer anschauen.

      Gruss Johnny

  3. 9. September 2011 um 08:50

    Hallo Jonny,
    das property ist das folgende:
    <property name=”hbm2ddl.auto”>create</property>

    … im obigen comment wurde das xml verschluckt …

  1. No trackbacks yet.
Die Kommentarfunktion ist geschlossen.
Folgen

Erhalte jeden neuen Beitrag in deinen Posteingang.

Schließe dich 278 Followern an

%d Bloggern gefällt das: