2016-07-21 19 views
5

diese dumme Domain Bedenken Sie:NHibernate QueryOver Coalesce eine Eigenschaft auf eine andere Eigenschaft

namespace TryHibernate.Example 
{ 
    public class Employee 
    { 
     public int Id { get; set; } 
     public string Name { get; set; } 
    } 

    public class WorkItem 
    { 
     public int Id { get; set; } 
     public string Description { get; set; } 
     public DateTime StartDate { get; set; } 
     public DateTime EndDate { get; set; } 
    } 

    public class Task 
    { 
     public int Id { get; set; } 
     public Employee Assignee { get; set; } 
     public WorkItem WorkItem { get; set; } 
     public string Details { get; set; } 
     public DateTime? StartDateOverride { get; set; } 
     public DateTime? EndDateOverride { get; set; } 
    } 
} 

Die Idee ist, dass jeder Arbeitseinheit an mehrere Mitarbeiter mit unterschiedlichen Details zugeordnet werden kann, möglicherweise überschrieben Start-/Enddaten der Arbeits Artikel selbst. Wenn diese Überschreibungen null sind, sollten sie stattdessen vom Arbeitselement übernommen werden.

Jetzt möchte ich eine Abfrage mit Einschränkungen für die effektive Daten durchführen. Ich habe diese zuerst versucht:

IList<Task> tasks = db.QueryOver<Task>(() => taskAlias) 
    .JoinAlias(() => taskAlias.WorkItem,() => wiAlias) 
    .Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate) <= end) 
    .And(() => taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) 
    .List(); 

Leider hat es nicht kompiliert als Coalesce eine Konstante erwartet, nicht eine Eigenschaft Ausdruck.

OK, ich habe dies versucht:

.Where(() => (taskAlias.StartDateOverride == null 
        ? wiAlias.StartDate 
        : taskAlias.StartDateOverride) <= end) 
    .And(() => (taskAlias.EndDateOverride == null 
        ? wiAlias.EndDate 
        : taskAlias.EndDateOverride) >= start) 

Dies wirft Nullreferenceexception. Nicht sicher, warum, aber wahrscheinlich entweder, weil NHibernate diesen ternären Operator nicht richtig übersetzt (und versucht, ihn stattdessen tatsächlich aufzurufen) oder weil == null nicht genau der richtige Weg ist, um nach Nullen zu suchen. Jedenfalls habe ich nicht einmal erwartet, dass es funktioniert.

Schließlich diese funktioniert:

IList<Task> tasks = db.QueryOver<Task>(() => taskAlias) 
    .JoinAlias(() => taskAlias.WorkItem,() => wiAlias) 
    .Where(Restrictions.LeProperty(
     Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, 
      Projections.Property(() => taskAlias.StartDateOverride), 
      Projections.Property(() => wiAlias.StartDate)), 
     Projections.Constant(end))) 
    .And(Restrictions.GeProperty(
     Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, 
      Projections.Property(() => taskAlias.EndDateOverride), 
      Projections.Property(() => wiAlias.EndDate)), 
     Projections.Constant(start))) 
    .List(); 

Aber es gibt keine Möglichkeit, die ich sauberen Code aufrufen können. Vielleicht kann ich bestimmte Ausdrücke in separate Methoden extrahieren, um sie ein wenig zu bereinigen, aber es wäre viel besser, Ausdruckssyntax zu verwenden als diese hässlichen Projektionen. Gibt es einen Weg, es zu tun? Gibt es einen Grund, warum NHibernate keine Eigenschaftsausdrücke in der Erweiterung Coalesce unterstützt?

Eine offensichtliche Alternative ist, alles auszuwählen und dann die Ergebnisse mit Linq oder was auch immer zu filtern. Aber es könnte ein Performance-Problem mit einer großen Anzahl von Gesamtreihen werden. Hier

ist voll Code, falls jemand will es versuchen:

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()))) 
    .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
    .BuildSessionFactory()) 
{ 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     Employee empl = new Employee() { Name = "Joe" }; 
     WorkItem wi = new WorkItem() 
     { 
      Description = "Important work", 
      StartDate = new DateTime(2016, 01, 01), 
      EndDate = new DateTime(2017, 01, 01) 
     }; 
     Task task1 = new Task() 
     { 
      Assignee = empl, 
      WorkItem = wi, 
      Details = "Do this", 
     }; 
     db.Save(task1); 
     Task task2 = new Task() 
     { 
      Assignee = empl, 
      WorkItem = wi, 
      Details = "Do that", 
      StartDateOverride = new DateTime(2016, 7, 1), 
      EndDateOverride = new DateTime(2017, 1, 1), 
     }; 
     db.Save(task2); 
     Task taskAlias = null; 
     WorkItem wiAlias = null; 
     DateTime start = new DateTime(2016, 1, 1); 
     DateTime end = new DateTime(2016, 6, 30); 
     IList<Task> tasks = db.QueryOver<Task>(() => taskAlias) 
      .JoinAlias(() => taskAlias.WorkItem,() => wiAlias) 
      // This doesn't compile: 
      //.Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate) <= end) 
      //.And(() => taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) 
      // This throws NullReferenceException: 
      //.Where(() => (taskAlias.StartDateOverride == null ? wiAlias.StartDate : taskAlias.StartDateOverride) <= end) 
      //.And(() => (taskAlias.EndDateOverride == null ? wiAlias.EndDate : taskAlias.EndDateOverride) >= start) 
      // This works: 
      .Where(Restrictions.LeProperty(
       Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, 
        Projections.Property(() => taskAlias.StartDateOverride), 
        Projections.Property(() => wiAlias.StartDate)), 
       Projections.Constant(end))) 
      .And(Restrictions.GeProperty(
       Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, 
        Projections.Property(() => taskAlias.EndDateOverride), 
        Projections.Property(() => wiAlias.EndDate)), 
       Projections.Constant(start))) 
      .List(); 
     foreach (Task t in tasks) 
      Console.WriteLine("Found task: {0}", t.Details); 
    } 
} 

und die Konfiguration ist denkbar einfach:

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

Noch ein Downvote Maniac? Ich habe meine Nachforschungen angestellt, ich habe tatsächlich eine funktionierende Lösung gefunden, ich habe mein Problem klar dargelegt und sogar ein Beispiel gegeben. Als ob -2 Rep tatsächlich etwas bedeutet! –

+0

Ich stimme dem obigen Kommentar voll zu. Alles, was ich brauchte, war ein neues Konsolenprojekt zu erstellen, zwei NuGet-Pakete zu installieren, den bereitgestellten Code zu kopieren und einzufügen und damit zu spielen. Keine Vermutungen, keine Tippfehler, alles kompiliert/tut genau das, was im Beitrag erklärt oder im Code kommentiert wird. Es ist wirklich außergewöhnlich, so gute 'mcve' hier für ein nicht triviales Thema zu finden. –

Antwort

5

mit diesem Beginnen wir:

// This doesn't compile: 
//.Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate) <= end) 
//.And(() => taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) 

und ändern Sie es zu:

.Where(() => taskAlias.StartDateOverride.Coalesce(wiAlias.StartDate) <= end) 
.And(() => taskAlias.EndDateOverride.Coalesce(wiAlias.EndDate) >= start) 

jetzt wird es kompilieren. Aber zur Laufzeit erzeugt es das gleiche NullReferenceException. Nicht gut.

Es stellt sich heraus, dass NHibernate in der Tat versucht, das Coalesce Argument auszuwerten. Dies kann leicht durch Betrachten der Klassenimplementierung ProjectionExtensions gesehen werden. Die folgende Methode behandelt die Coalesce Übersetzung:

internal static IProjection ProcessCoalesce(MethodCallExpression methodCallExpression) 
{ 
    IProjection projection = ExpressionProcessor.FindMemberProjection(methodCallExpression.Arguments[0]).AsProjection(); 
    object obj = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]); 
    return Projections.SqlFunction("coalesce", (IType) NHibernateUtil.Object, projection, Projections.Constant(obj)); 
} 

Beachten Sie die unterschiedliche Behandlung des ersten Arguments (FindMemberExpresion) vs zweiten Argument (FindValue). Nun, FindValue versucht einfach, den Ausdruck zu bewerten.

Jetzt wissen wir, was das Problem verursacht. Ich habe keine Ahnung, warum es so umgesetzt wird, also werde ich mich darauf konzentrieren, eine Lösung zu finden.

Glücklicherweise ist die ExpressionProcessor Klasse öffentlich und ermöglicht Ihnen auch, eine benutzerdefinierte Methoden über RegisterCustomMethodCall/RegisterCustomProjection Methoden zu registrieren. Was uns zur Lösung führt:

  • ein Verfahren benutzerdefinierte Erweiterungen erstellen Ähnliche Coalesce (lassen Sie sie zum Beispiel IfNull nennen)
  • Registrieren eines benutzerdefinierten Prozessor
  • sie verwenden statt Coalesce

Hier ist die Implementierung:

public static class CustomProjections 
{ 
    static CustomProjections() 
    { 
     ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, ""), ProcessIfNull); 
     ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, 0), ProcessIfNull); 
    } 

    public static void Register() { } 

    public static T IfNull<T>(this T objectProperty, T replaceValueIfIsNull) 
    { 
     throw new Exception("Not to be used directly - use inside QueryOver expression"); 
    } 

    public static T? IfNull<T>(this T? objectProperty, T replaceValueIfIsNull) where T : struct 
    { 
     throw new Exception("Not to be used directly - use inside QueryOver expression"); 
    } 

    private static IProjection ProcessIfNull(MethodCallExpression mce) 
    { 
     var arg0 = ExpressionProcessor.FindMemberProjection(mce.Arguments[0]).AsProjection(); 
     var arg1 = ExpressionProcessor.FindMemberProjection(mce.Arguments[1]).AsProjection(); 
     return Projections.SqlFunction("coalesce", NHibernateUtil.Object, arg0, arg1); 
    } 
} 

Da diese Methoden nie aufgerufen werden, müssen Sie sicherstellen, dass der benutzerdefinierte Prozessor registriert wird, indem Sie die Methode Register aufrufen. Es ist eine leere Methode, nur um sicherzustellen, dass der statische Konstruktor der Klasse aufgerufen wird, wo die eigentliche Registrierung stattfindet.

So in Ihrem Beispiel ist am Anfang:

CustomProjections.Register();

dann in der Abfrage verwenden:

.Where(() => taskAlias.StartDateOverride.IfNull(wiAlias.StartDate) <= end) 
.And(() => taskAlias.EndDateOverride.IfNull(wiAlias.EndDate) >= start) 

und es wird erwartet funktionieren.

P.S. Die obige Implementierung funktioniert sowohl für Konstanten- als auch für Ausdrucksargumente und ist daher ein sicherer Ersatz für die Coalesce.

+0

Super! Und indem ich Generics entferne, kann ich es 'Coalesce' nennen, ohne Namenskonflikte, * und * habe' NHibernateUtil.Date' anstelle von 'NHibernateUtil.Object' (obwohl es mit beiden funktioniert). Der Nachteil ist, dass ich Überladungen für jeden Typ erstellen muss, den ich brauche, aber zu der Zeit ist es nur einer. –

+0

Interessante Idee, ich habe nicht in diese Richtung gedacht. Ein bisschen ärgerlich, wenn Sie es für viele Arten tun müssen, aber funktioniert! Eigentlich habe ich eine andere Lösung, aber es ist ein bisschen hacky. Ersetzen Sie anstelle der benutzerdefinierten Erweiterungsmethoden den Handler für die beiden "Coalesce" -Methoden (sie werden auf die gleiche Weise registriert). Um die vorherigen Handler zu entfernen, müssen wir jedoch über Reflektion auf ihr privates Wörterbuch zugreifen. Und natürlich können Sie NHibernate fix anfordern/vorschlagen (dauert einige Zeit), wenden Sie das Update im Quellcode an und verwenden Sie benutzerdefinierte NHibernate Build (Wartung, Updates) usw. Viele Optionen :-) –

Verwandte Themen