2016-07-21 61 views
5

Cân nhắc miền ngớ ngẩn này:NHibernate QueryOver liên hiệp một tài sản đối với tài sản khác

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; } 
    } 
} 

Ý tưởng là mỗi hạng mục công trình có thể được gán cho nhiều người lao động với các chi tiết khác nhau, có khả năng trọng bắt đầu/kết thúc ngày với các hạng mục công trình chinh no. Nếu những ghi đè đó là null, chúng sẽ được lấy từ mục công việc thay thế.

Bây giờ tôi muốn thực hiện truy vấn có giới hạn về ngày hiệu quả. Tôi đã thử này đầu tiên:

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(); 

Thật không may, nó không biên dịch như Coalesce hy vọng một, không phải là một biểu hiện bất động sản liên tục.

OK, tôi đã thử điều này:

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

này ném NullReferenceException. Bạn không chắc chắn lý do tại sao, nhưng có lẽ hoặc vì NHibernate không đúng cách dịch toán tử ternary (và cố gắng thực sự gọi nó thay vì) hoặc vì == null không chính xác là cách đúng để kiểm tra null. Dù sao, tôi thậm chí không mong đợi nó hoạt động.

Cuối cùng, chương trình này hoạt động:

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(); 

Nhưng không có cách nào tôi có thể gọi là mã sạch. Có lẽ tôi có thể trích xuất các biểu thức nhất định thành các phương pháp riêng biệt để làm sạch nó một chút, nhưng sẽ tốt hơn nếu sử dụng cú pháp biểu thức thay vì các dự báo xấu xí này. Có cách nào để làm điều đó? Có bất kỳ lý do nào đằng sau NHibernate không hỗ trợ biểu thức thuộc tính trong phần mở rộng Coalesce không?

Một lựa chọn hiển nhiên là chọn tất cả mọi thứ và sau đó lọc kết quả bằng LINQ hoặc bất kỳ thứ gì. Nhưng nó có thể trở thành một vấn đề hiệu suất với số lượng lớn của tổng số hàng.

Đây là mã đầy đủ trong trường hợp ai đó muốn thử nó:

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); 
    } 
} 

Và cấu hình thực sự là đơn giản:

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

Một cơn ác mộng khác? Tôi đã thực hiện nghiên cứu của mình, tôi đã thực sự đưa ra giải pháp làm việc, tôi đã nêu rõ vấn đề của mình và thậm chí đã cung cấp một ví dụ. Như thể -2 đại diện thực sự có nghĩa là một cái gì đó! –

+0

Tôi hoàn toàn đồng ý với nhận xét ở trên. Tất cả những gì tôi cần là tạo một dự án bàn điều khiển mới, cài đặt hai gói NuGet, sao chép/dán mã được cung cấp và bắt đầu chơi với nó. Không có dự đoán, không có lỗi chính tả, mọi thứ biên dịch/thực hiện chính xác những gì được giải thích trong bài đăng hoặc nhận xét trong mã. Nó thực sự đặc biệt để tìm tốt như vậy 'mcve' ở đây cho một vấn đề không tầm thường. –

Trả lời

5

Hãy bắt đầu với điều này:

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

và sửa đổi nó thành:

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

bây giờ nó sẽ biên dịch. Nhưng trong thời gian chạy nó tạo ra cùng một NullReferenceException. Không tốt.

Nó chỉ ra rằng NHibernate thực sự cố gắng để đánh giá các đối số Coalesce. Điều này có thể dễ dàng được nhìn thấy bằng cách xem xét thực hiện lớp học ProjectionExtensions. Các phương pháp sau đây xử lý các Coalesce dịch:

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)); 
} 

Thông báo việc xử lý khác nhau của các đối số đầu tiên (FindMemberExpresion) vs số thứ hai (FindValue). Vâng, FindValue chỉ đơn giản là cố gắng đánh giá biểu thức.

Bây giờ chúng tôi biết điều gì đang gây ra sự cố. Tôi không có ý tưởng tại sao nó được thực hiện theo cách đó, vì vậy sẽ tập trung vào việc tìm kiếm một giải pháp.

May mắn thay, lớp ExpressionProcessor là công khai và cũng cho phép bạn đăng ký phương thức tùy chỉnh thông qua phương thức RegisterCustomMethodCall/RegisterCustomProjection. Dẫn chúng ta đến giải pháp:

  • Tạo một phần mở rộng tùy chỉnh phương pháp tương tự để Coalesce (hãy gọi cho họ IfNull ví dụ)
  • đăng ký một bộ xử lý tùy chỉnh
  • Sử dụng chúng thay vì Coalesce

Đây là triển khai:

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); 
    } 
} 

Vì những phương pháp này không bao giờ được gọi, bạn cần đảm bảo rằng bộ xử lý tùy chỉnh được đăng ký bằng cách gọi phương thức Register. Đó là một phương thức trống để đảm bảo hàm khởi tạo tĩnh của lớp được gọi, nơi đăng ký thực sự xảy ra.

Vì vậy, trong ví dụ của bạn, bao gồm ít đầu:

CustomProjections.Register();

sau đó sử dụng bên trong truy vấn:

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

và nó sẽ làm việc như mong đợi.

P.S. Việc triển khai ở trên hoạt động cho cả đối số liên tục và biểu thức, do đó, nó thực sự là sự thay thế an toàn của Coalesce.

+0

Tuyệt vời! Và bằng cách loại bỏ generics, tôi thực sự có thể đặt tên nó là "Coalesce' không có xung đột tên, * và * có' NHibernateUtil.Date' thay vì 'NHibernateUtil.Object' (mặc dù nó hoạt động với một trong hai). Thương mại-off là tôi phải tạo ra quá tải cho mọi loại tôi cần, nhưng tại thời điểm nó chỉ là một. –

+0

Ý tưởng thú vị, tôi không nghĩ theo hướng đó. Một chút khó chịu nếu bạn cần phải làm điều đó cho nhiều loại, nhưng hoạt động! Trên thực tế tôi có một giải pháp khác, nhưng đó là một chút hacky. Thay vì các phương thức mở rộng tùy chỉnh, chỉ cần thay thế trình xử lý cho hai phương thức 'Coalesce' (chúng được đăng ký theo cùng một cách). Nhưng để loại bỏ các trình xử lý trước đó, chúng ta cần phải truy cập từ điển riêng của họ thông qua sự phản chiếu. Và tất nhiên bạn có thể yêu cầu/đề nghị sửa chữa NHibernate (sẽ mất thời gian), áp dụng các sửa chữa trong mã nguồn và sử dụng xây dựng NHibernate tùy chỉnh (bảo trì, cập nhật) vv Rất nhiều sự lựa chọn :-) –

Các vấn đề liên quan