2016-07-16 7 views
3

Betrachten Sie diese erfundene Domäne sortieren:NHibernate QueryOver jeder der Eins-zu-viele Sammlungen

namespace TryHibernate.Example { 

public class Computer 
{ 
    public int Id { get; set; } 
    public IList<Device> Devices { get; set; } 
} 

public class Device 
{ 
    public int Id { get; set; } 
    public Maker Maker { get; set; } 
} 

public class Maker 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

} // namespace 

Wenn ich nur alle Computer abfragen, werden ihre Geräte zufällig bestellt werden. Ich möchte sie nach dem Namen des Herstellers bestellen lassen. Ich kann es wirklich nicht mit HasMany().OrderBy() tun, weil, soweit ich sagen kann OrderBy kann nur lokale Spalten verwenden (so kann ich es zum Beispiel durch Device.Id sortieren). Außerdem wäre es nett, die Reihenfolge pro Abfrage zu steuern, also suche ich nach einer QueryOver Lösung.

Die weitest ich bekommen konnte, war dies:

using (ISessionFactory sessionFactory = Fluently.Configure() 
    .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
    .Mappings(m => m.AutoMappings.Add(
     AutoMap.AssemblyOf<Computer>(new ExampleConfig()) 
      .Conventions.Add(DefaultLazy.Never()) 
      .Conventions.Add(DefaultCascade.All()))) 
    .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
    .BuildSessionFactory()) 
{ 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     Computer comp = new Computer(); 
     comp.Devices = new List<Device>(); 
     Device dev1 = new Device(); 
     comp.Devices.Add(dev1); 
     dev1.Maker = new Maker() { Name = "IBM"}; 
     Device dev2 = new Device(); 
     comp.Devices.Add(dev2); 
     dev2.Maker = new Maker() { Name = "Acer"}; 
     db.Persist(comp); 
     db.Flush(); 
    } 
    using (ISession db = sessionFactory.OpenSession()) 
    { // This is the part I'm having trouble with: 
     Device devAlias = null; 
     Maker makerAlias = null; 
     IList<Computer> comps = db.QueryOver<Computer>() 
      .JoinAlias(c => c.Devices,() => devAlias) 
      .JoinAlias(() => devAlias.Maker,() => makerAlias) 
      .OrderBy(() => makerAlias.Name).Asc 
      .List(); 
     Console.WriteLine(comps.Count); 
     foreach (Device dev in comps[0].Devices) 
     { 
      Console.WriteLine(dev.Maker.Name); 
     } 
    } 
} 

Aber natürlich ist es nicht tun, was ich will. Es versucht, die ganze Liste nach dem Namen des Herstellers zu sortieren. Es ist auch erfolgreich, wie ich aus der SQL sehen kann, und ich bekomme tatsächlich ein nutzloses kartesisches Produkt von Computern mit Geräten, die vom Gerätehersteller sortiert wurden.

Aber dann gibt es eine andere SQL-Abfrage, um die Geräte zu holen, dieses Mal ohne zu sortieren. Ich denke, NHibernate hat keine Ahnung, dass meine Joins die Kinder holen sollen.

Die Frage ist, wie kann ich diese zweite Abfrage steuern? Zum Beispiel, um Geräte nach dem Namen des Herstellers zu bestellen, oder um jede Computer.Devices Liste zu erhalten, die nur Geräte enthält, die beispielsweise von IBM (falls vorhanden) hergestellt wurden. Ich denke, ich brauche dafür eine Unterabfrage, aber wo stecke ich sie an?

Nur der Vollständigkeit halber, hier ist meine config:

class ExampleConfig : DefaultAutomappingConfiguration 
{ 
    public override bool ShouldMap(Type type) 
    { 
     return type.Namespace == "TryHibernate.Example"; 
    } 
} 

Antwort

1

Nach einer Weile mit Bozhidar Antwort zu kämpfen, habe ich es etwas funktioniert. Ich hatte eine manuelle Zuordnung für den Computer zu tun, wie folgt aus:

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="all" default-lazy="false"> 
    <class name="TryHibernate.Example.Computer, TryHibernate" table="Computer"> 
    <id name="Id" type="System.Int32, mscorlib" column="Id" generator="identity" /> 
    <bag name="Devices"> 
     <key column="ComputerId" /> 
     <one-to-many class="TryHibernate.Example.Device, TryHibernate" /> 
     <loader query-ref="DeviceLoader" /> 
    </bag> 
    </class> 

    <sql-query name="DeviceLoader"> 
    <load-collection alias="Device" role="TryHibernate.Example.Computer.Devices" /> 
    SELECT Device.Id, Device.Maker_Id, Device.ComputerId, Maker.Name 
    FROM Device JOIN Maker ON (Maker.Id = Device.Maker_Id) 
    WHERE Device.ComputerId=? 
    ORDER BY Maker.Name 
    </sql-query> 
</hibernate-mapping> 

Dies geht in eine eingebettete Ressource mit dem Namen Computer.hbm.xml. Dann funktioniert der Code wie folgt aus:

using (ISessionFactory sessionFactory = Fluently.Configure() 
    .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
    .Mappings(m => m.AutoMappings.Add(
     AutoMap.AssemblyOf<Employee>(new ExampleConfig()) 
      .Conventions.Add(DefaultLazy.Never()) 
      .Conventions.Add(DefaultCascade.All()))) 
    .Mappings(m => m.HbmMappings.AddFromAssembly(Assembly.GetExecutingAssembly())) 
    .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
    .BuildSessionFactory()) 
{ 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     Computer comp = new Computer(); 
     comp.Devices = new List<Device>(); 
     Device dev1 = new Device(); 
     comp.Devices.Add(dev1); 
     dev1.Maker = new Maker() { Name = "IBM" }; 
     Device dev2 = new Device(); 
     comp.Devices.Add(dev2); 
     dev2.Maker = new Maker() { Name = "Acer" }; 
     db.Transaction.Begin(); 
     db.Persist(comp); 
     db.Transaction.Commit(); 
    } 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     IList<Computer> comps = db.QueryOver<Computer>().List(); 
     Console.WriteLine(comps.Count); 
     foreach (Device dev in comps[0].Devices) 
     { 
      Console.WriteLine(dev.Maker.Name); 
     } 
    } 
} 

Allerdings kann ich nicht sagen, ich bin glücklich mit diesem. Erstens scheint es zu viel Arbeit zu sein, um etwas zu bestellen, das SQL out-of-the-box unterstützt.Und es kann nicht auf Abfrageebene angepasst werden, was den Zweck meiner Frage vereitelt. Dann macht die Tasche Tag mich ziemlich unruhig fühlen. Es bedeutet eine ungeordnete Sammlung. Sicher, es scheint , um die Reihenfolge zu bewahren, aber gibt es irgendwelche Garantien? Und dummer NHibernate erlaubt einfach nicht, Liste dort zu verwenden, weil Sie sehen, es wird kein "wahres" Mapping sein. Sie benötigen eine dumme "Index" -Spalte, um eine richtige Liste zu erhalten!

All dies nur etwas Triviales zu sortieren. Lässt mich denken, warum Dinge wie jOOQ zum Leben erwachen. .Net braucht so etwas (oder jemanden?), Verzweifelt. Verwenden Sie einfach typesafe Embedded SQL mit einem Code-Generator und automatische Konvertierung zu/von POCOs.

+0

"Es scheint zu viel Arbeit zu sein, nur etwas zu bestellen, was SQL out-of-the-box unterstützt" - stimme völlig zu. Es sollte einfacher sein, aber es ist nicht so. NHibernate hat oft diese Fähigkeit, sowohl reif als auch ... dumm zugleich zu sein; Ganz zu schweigen von der Dokumentationsqualität ... Allerdings gibt es LINQ-Unterstützung in NH. Hast du das gesehen? –

+0

@Bozhidar, richtig, ich habe Linq gesehen, aber ich bin mir nicht sicher, ob es in diesem Fall verwendet werden kann, und ich bin mir nicht sicher, ob es sich um SQL oder In-Memory-Sortierung handelt, was sinnlos ist da kann ich das immer selbst auf der abgeholten Sammlung machen. –

1

Dies ist nicht wirklich von NHibernate unterstützt. Es gibt viele Funktionen, die Sie kombinieren können und denen Sie wahrscheinlich nahe kommen. Diese Lösungen neigen dazu, schnell zu brechen und sind schwer zu warten.

QueryOver: Sie können NHibernate anweisen, Kindern beizutreten und sie gleichzeitig in eine Sammlung einzufügen.

db.QueryOver<Computer>() 
    .JoinAlias(c => c.Devices,() => devAlias) 
    .JoinAlias(() => devAlias.Maker,() => makerAlias) 
    .Fetch(x => x.Devices).Eager 
    .OrderBy(() => makerAlias.Name).Asc 
    .List(); 

Dies funktioniert aufgrund des kartesischen Produktproblems gut mit einer Sammlung. Dein Fall ist wahrscheinlich schon zu komplex.

Zuordnung: Eine andere Lösung besteht darin, in der Zuordnungsdatei zu sortieren. Sie verwalten es wahrscheinlich mit dem Namen des Herstellers, wenn Sie etwas SQL in eine "Formel" hinzufügen. Ich würde es nicht tun. Es zerstört die Leistung der Anwendung, weil es alle diese Verbindungen und Bestellungen benötigt, nur um diese Dinge aus der Datenbank zu bekommen.

Sortieren nach dem Schreiben: Sie sollten überlegen, es in der richtigen Reihenfolge in die Datenbank zu bringen. Ordnen Sie es als Liste und nicht als Tasche zu. Um ehrlich zu sein: Geräte, die vom Hersteller bestellt werden, klingen eher nach einer Anzeige als nach einer Datenverwaltung und sollten daher beim Lesen gemacht werden. Es wäre auch schwierig, in der richtigen Reihenfolge zu bleiben, wenn z.B. Ein Hersteller ändert den Namen. Allerdings wollte ich diese Option der Vollständigkeit halber hinzugefügt haben, oft macht das Sinn.

KISS: Jetzt zur einfachsten Lösung, die Sie möglicherweise erhalten können.

-Trommel Roll-

Auftrag im Speicher Linq verwenden.

foreach(var computer in computers) 
{ 
    foreach(var device in computer.Devices.OrderBy(x => x.Maker.Name)) 
    { 

    } 
} 

Erledigt.

+0

Der 'Fetch'-Weg funktioniert bei mir nicht: Er sortiert immer noch keine Sammlungen, die den Computern in der resultierenden Liste gehören. –

+0

@SergeyTachenov: Kann sein, dass es nicht einmal funktioniert. Wenn es sich beispielsweise um eine Liste und nicht um eine Tüte handelt, darf die Bestellung nicht einmal geändert werden. Wie gesagt: Es ist leicht zu brechen. –

1

Ich würde vorschlagen, geben Sie sich eine Stunde. Schreiben Sie zu dieser Stunde eine Entitätsframework-Verarbeitung für diese spezifische Abfrage. Ich habe NHibernate in Entity-Framework konvertiert und höchstwahrscheinlich haben Sie eine zufällige XML-Verknüpfung zwischen den fraglichen Tabellen. Innerhalb des Entitätsrahmens sind diese 'Navigation'/'virtuellen Sammlungen' explizit innerhalb des ORM implementiert.

Wenn Sie nicht zu Ihrem Objekt innerhalb von NHibernate navigieren können, schlage ich vor, Sie fügen eine kleine Scheibe von Entity Framework zu einer separaten Klassenbibliothek hinzu, um sich mit dem ORM vertraut zu machen, der erklärt, warum Ihre mögliche Viele-zu-Viele-Beziehung nicht funktioniert. Wenn Sie eine Eins-zu-viele-Beziehung haben, sollte das so einfach sein.

+0

Der springende Punkt dieses Beispiels ist, NHibernate zu lernen, also wird EF mir hier offensichtlich nicht helfen. Und Sie haben einen anderen Punkt verpasst - es ist Fluent NHibernate, also kein XML, keine dummen Attribute, fast keine manuelle Zuordnung im Code. Der Code, der in der Frage präsentiert wird, übernimmt tatsächlich das Mapping. Kann EF das auch machen? –

+0

EF kann und tut Dinge für Sie abbilden. Es erstellt sogar eine 1 zu viele für viele zu viele, indem eine andere Navigationseigenschaft erstellt wird. Ich musste ein ganzes NHibernate-Setup zum Entity-Framework migrieren, und der Punkt, an den ich mich noch erinnere, waren implizite Joins von NH. – Programmer

+0

Ich mag es nicht, wie EF Abhängigkeiten in Domäne kriechen. Erinnert mich an JPA. Das mag ich nicht. Aber ich mag ORMs nicht. Und JPA ist nicht so schlecht, also werde ich EF vielleicht einen Versuch geben. –

2

Sie könnten dies erreichen, indem Sie die SQL-Select-Anweisung für Device es Abruf stören. Die eleganteste Lösung für mich ist die Verwendung von benutzerdefinierten Loader. Aber das ist
not supported through Fluent NHibernate. Zum Glück können Sie in Ihrem Projekt einzelne hbm.xml irgendwo schreiben und sie durch fließend hinzufügen wie folgt:

Device.hbm.xml

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true"> 
    <class name="TryHibernate.Example.Device, TryHibernate" table="Device"> 
     <id name="Id" type="System.Int32, mscorlib" column="Id" generator="identity" /> 
     <property name="Name" /> 
     <many-to-one name="Maker" column="MakerId" /> 
     <many-to-one name="Computer" column="ComputerId" /> 
     <loader query-ref="DeviceLoader" /> 
    </class> 

    <sql-query name="DeviceLoader"> 
     <return alias="dev" class="TryHibernate.Example.Device, TryHibernate" lock-mode="read"> 
      <return-property name="Computer" column="ComputerId" /> 
      <return-property name="Maker" column="MakerId" /> 
     </return> 

     SELECT Device.Id AS {dev.Id}, Device.Name As {dev.Name}, Device.MakerId AS {dev.MakerId}, Device.ComputerId AS {dev.ComputerId} 
     FROM Device INNER JOIN Maker ON Maker.Id = Device.MakerId 
     WHERE Device.Id=? 
     ORDER BY Device.ComputerId, Maker.Name 
    </sql-query> 
</hibernate-mapping> 

Dann, während der Sitzung Fabrikgebäude tun:

 
    using (ISessionFactory sessionFactory = Fluently.Configure() 
     .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
     .Mappings(m => m.AutoMappings.Add(
      AutoMap.AssemblyOf(new ExampleConfig()) 
       .Conventions.Add(DefaultLazy.Never()) 
       .Conventions.Add(DefaultCascade.All()))) 
     .Mappings(m => m.HbmMappings.AddFromAssembly(Assembly.GetExecutingAssembly())) 
     .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
     .BuildSessionFactory()) 

Ein anderer Gedanke, der mich traf, ist, dass Sie Subselect() für Ihr Mapping spezifizieren konnten; dies wie:

public class DeviceMap : ClassMap<Device> 
{ 
    public DeviceMap() 
    { 
     Table("Device"); 
     Subselect(@" 
    SELECT TOP 100 Device.Id , Device.Name, Device.MakerId , Device.ComputerId 
    FROM Device INNER JOIN Maker ON Maker.Id = Device.MakerId 
    ORDER BY Device.ComputerId, Maker.Name 
"); 

     Id(x => x.Id); 
     Map(x => x.Name); 

     References(x => x.Computer).Column("ComputerId"); 
     References(x => x.Maker).Column("MakerId"); 
    } 

Wie Sie feststellen, verwendete ich die TOP Klausel. Es ist notwendig, da die Unter select-Anweisung eine Unterabfrage wird und wie Sie

Die ORDER BY-Klausel wissen könnten, ist ungültig, Ansichten, Inline-Funktionen, abgeleitete Tabellen, Unterabfragen und allgemeine Tabellenausdrücke, es sei denn, TOP, OFFSET oder FOR XML wird ebenfalls angegeben.


Noch ein anderer ist jedoch eine IInterceptor Schnittstelle und verändern die SQL entsprechend zu implementieren. Wie so:

public class SqlStatementInterceptor : EmptyInterceptor 
{ 
    public override SqlString OnPrepareStatement(SqlString sql) 
    { 
     var result = sql; 

     if (sql.IndexOfCaseInsensitive("FROM Device") != -1) 
     { 
      var sqlText = string.Format("WITH cteDev AS ({0}{1}{0}){0}SELECT cteDev.* FROM cteDev INNER JOIN Maker ON cteDev.MakerId1_0_ = Maker.Id ORDER BY Maker.Name", Environment.NewLine, sql.ToString()); 

      result = sql 
       .Insert(0, "WITH cteDev AS (") 
       .Append(")SELECT cteDev.* FROM cteDev INNER JOIN Maker ON cteDev.MakerId1_0_ = Maker.Id ORDER BY Maker.Name"); 
      ; 

     } 

     Trace.WriteLine(result.ToString()); 

     return result; 
    } 
} 

*** Mit dieser Sie brauchen Konventionen zu verwenden und herauszufinden, wie NHibernate die Namen der Tabellen und Spalten erzeugt.

+0

Wow, das war ... unerwartet. Weniger als einen Tag nach Ablauf der Bounty, dachte ich, dass es einfach verschwinden wird. Ich habe nur deine erste Lösung ausprobiert, da andere scheinbar noch komplizierter sind und nichts mehr geben. Das erste funktionierte nicht ganz, und es gibt mir nicht die Möglichkeit, die Reihenfolge pro Abfrage zu kontrollieren, aber es wies mich immer noch in eine Richtung, die es mir ermöglichte, es zumindest irgendwie zu funktionieren. Siehe meine Antwort für Details. Wenn es keine besseren Antworten gibt, bekommst du das Kopfgeld sicher! –

+0

Probieren Sie die zweite Option - die 'Subselect (string)'. Die einzige Abwägung ist die "TOP" -Klausel, die nicht aufdringlich zu sein scheint - ich glaube, dass Sie genug für Ihre Situation, Anzahl der Datensätze, herausfinden könnten ... Außerdem ist es nicht zu schwer zu implementieren; es war der schnellste Ansatz für mich. –

+0

Wenn ich eine 'Subselect' mache, versucht es immer absolut lächerlich' INSERT INTO (SELECT ...) 'zu machen, wenn ich versuche Geräte hinzuzufügen. Es ist fast so, als ob es einfach den Tabellennamen mit meinem Subselect ersetzt, wo immer es sich anfühlt. –

Verwandte Themen