Skip to content

Commit 35f3f20

Browse files
committed
Add support for left outer join via navigation property (NH-2379)
1 parent 3a2599d commit 35f3f20

File tree

7 files changed

+279
-9
lines changed

7 files changed

+279
-9
lines changed

src/NHibernate.Test/Linq/JoinTests.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
using System;
22
using System.Linq;
3-
using System.Linq.Expressions;
4-
using NHibernate.DomainModel.Northwind.Entities;
53
using NUnit.Framework;
64

75
namespace NHibernate.Test.Linq
86
{
97
[TestFixture]
108
public class JoinTests : LinqTestCase
119
{
12-
protected override void Configure(Cfg.Configuration configuration)
13-
{
14-
configuration.SetProperty(Cfg.Environment.ShowSql, "true");
15-
base.Configure(configuration);
16-
}
17-
18-
1910
[Test]
2011
public void OrderLinesWith2ImpliedJoinShouldProduce2JoinsInSql()
2112
{
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System;
2+
using System.Linq;
3+
using NHibernate.Cfg.MappingSchema;
4+
using NHibernate.Linq;
5+
using NHibernate.Mapping.ByCode;
6+
using NUnit.Framework;
7+
8+
namespace NHibernate.Test.NHSpecificTest.NH2379
9+
{
10+
[TestFixture]
11+
public class Fixture : TestCaseMappingByCode
12+
{
13+
protected override HbmMapping GetMappings()
14+
{
15+
var mapper = new ModelMapper();
16+
mapper.Class<Order>(rc =>
17+
{
18+
rc.Table("Orders");
19+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
20+
rc.Property(x => x.Name);
21+
rc.Set(x => x.OrderLines, m =>
22+
{
23+
m.Inverse(true);
24+
m.Key(k =>
25+
{
26+
k.Column("OrderId");
27+
k.NotNullable(true);
28+
});
29+
m.Cascade(Mapping.ByCode.Cascade.All.Include(Mapping.ByCode.Cascade.DeleteOrphans));
30+
m.Access(Accessor.NoSetter);
31+
}, m => m.OneToMany());
32+
});
33+
mapper.Class<OrderLine>(rc =>
34+
{
35+
rc.Table("OrderLines");
36+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
37+
rc.Property(x => x.Name);
38+
rc.ManyToOne(x => x.Order, m => m.Column("OrderId"));
39+
});
40+
41+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
42+
}
43+
44+
protected override void OnSetUp()
45+
{
46+
using (var session = OpenSession())
47+
using (var transaction = session.BeginTransaction())
48+
{
49+
var o1 = new Order {Name = "Order 1"};
50+
session.Save(o1);
51+
52+
var o2 = new Order {Name = "Order 2"};
53+
session.Save(o2);
54+
55+
session.Save(new OrderLine {Name = "Order Line 2 - 1", Order = o2});
56+
57+
var o3 = new Order {Name = "Order 3"};
58+
session.Save(o3);
59+
60+
session.Save(new OrderLine {Name = "Order Line 3 - 1", Order = o3});
61+
session.Save(new OrderLine {Name = "Order Line 3 - 2", Order = o3});
62+
63+
var o4 = new Order {Name = "Order 4"};
64+
session.Save(o4);
65+
66+
session.Save(new OrderLine {Name = "Order Line 4 - 1", Order = o4});
67+
session.Save(new OrderLine {Name = "Order Line 4 - 2", Order = o4});
68+
session.Save(new OrderLine {Name = "Order Line 4 - 3", Order = o4});
69+
70+
transaction.Commit();
71+
}
72+
}
73+
74+
protected override void OnTearDown()
75+
{
76+
using (var session = OpenSession())
77+
using (var transaction = session.BeginTransaction())
78+
{
79+
session.Delete("from System.Object");
80+
81+
session.Flush();
82+
transaction.Commit();
83+
}
84+
}
85+
86+
[Test]
87+
public void InnerJoin()
88+
{
89+
//
90+
// select
91+
// order0_.Id as col_0_0_,
92+
// orderlines1_.Id as col_1_0_
93+
// from
94+
// Orders order0_
95+
// inner join
96+
// OrderLines orderlines1_
97+
// on order0_.Id=orderlines1_.OrderId
98+
//
99+
100+
using (var session = OpenSession())
101+
using (session.BeginTransaction())
102+
{
103+
var result = (from o in session.Query<Order>()
104+
from ol in o.OrderLines
105+
select new {OrderId = o.Id, OrderLineId = (Guid?) ol.Id}).ToList();
106+
107+
Assert.AreEqual(6, result.Count);
108+
}
109+
}
110+
111+
[Test]
112+
public void LeftOuterJoin()
113+
{
114+
//
115+
// select
116+
// order0_.Id as col_0_0_,
117+
// orderlines1_.Id as col_1_0_
118+
// from
119+
// Orders order0_
120+
// left outer join
121+
// OrderLines orderlines1_
122+
// on order0_.Id=orderlines1_.OrderId
123+
//
124+
125+
using (var session = OpenSession())
126+
using (session.BeginTransaction())
127+
{
128+
var result = (from o in session.Query<Order>()
129+
from ol in o.OrderLines.DefaultIfEmpty()
130+
select new {OrderId = o.Id, OrderLineId = (Guid?) ol.Id}).ToList();
131+
132+
Assert.AreEqual(7, result.Count);
133+
}
134+
}
135+
136+
[Test, Ignore("Not fixed yet. (NH-3175)")]
137+
public void LeftOuterJoinWithInnerRestriction()
138+
{
139+
//
140+
// select
141+
// order0_.Id as col_0_0_,
142+
// orderlines1_.Id as col_1_0_
143+
// from
144+
// Orders order0_
145+
// left outer join
146+
// OrderLines orderlines1_
147+
// on order0_.Id=orderlines1_.OrderId
148+
// and orderlines1_.Name like ('Order Line 3')
149+
//
150+
151+
using (var session = OpenSession())
152+
using (session.BeginTransaction())
153+
{
154+
var result = (from o in session.Query<Order>()
155+
from ol in o.OrderLines.Where(x => x.Name.StartsWith("Order Line 3")).DefaultIfEmpty()
156+
select new {OrderId = o.Id, OrderLineId = (Guid?) ol.Id}).ToList();
157+
158+
Assert.AreEqual(5, result.Count);
159+
}
160+
}
161+
162+
[Test]
163+
public void LeftOuterJoinWithOuterRestriction()
164+
{
165+
//
166+
// select
167+
// order0_.Id as col_0_0_,
168+
// orderlines1_.Id as col_1_0_
169+
// from
170+
// Orders order0_
171+
// left outer join
172+
// OrderLines orderlines1_
173+
// on order0_.Id=orderlines1_.OrderId
174+
// where
175+
// orderlines1_.Name like ('Order Line 3')
176+
//
177+
178+
using (var session = OpenSession())
179+
using (session.BeginTransaction())
180+
{
181+
var result = (from o in session.Query<Order>()
182+
from ol in o.OrderLines.DefaultIfEmpty().Where(x => x.Name.StartsWith("Order Line 3"))
183+
select new {OrderId = o.Id, OrderLineId = (Guid?) ol.Id}).ToList();
184+
185+
Assert.AreEqual(2, result.Count);
186+
}
187+
}
188+
}
189+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace NHibernate.Test.NHSpecificTest.NH2379
5+
{
6+
public class Order
7+
{
8+
private readonly IEnumerable<OrderLine> _orderLines = new List<OrderLine>();
9+
public virtual Guid Id { get; set; }
10+
public virtual string Name { get; set; }
11+
12+
public virtual IEnumerable<OrderLine> OrderLines
13+
{
14+
get { return _orderLines; }
15+
}
16+
}
17+
18+
public class OrderLine
19+
{
20+
public virtual Guid Id { get; set; }
21+
public virtual string Name { get; set; }
22+
public virtual Order Order { get; set; }
23+
}
24+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,8 @@
664664
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Domain.cs" />
665665
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Fixture.cs" />
666666
<Compile Include="Component\Basic\ComponentWithUniqueConstraintTests.cs" />
667+
<Compile Include="NHSpecificTest\NH2379\Order.cs" />
668+
<Compile Include="NHSpecificTest\NH2379\Fixture.cs" />
667669
<Compile Include="NHSpecificTest\NH3070\Fixture.cs" />
668670
<Compile Include="NHSpecificTest\NH2789\Entities.cs" />
669671
<Compile Include="NHSpecificTest\NH2789\Fixture.cs" />
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Collections.Generic;
2+
using NHibernate.Linq.Clauses;
3+
using Remotion.Linq;
4+
using Remotion.Linq.Clauses;
5+
using Remotion.Linq.Clauses.ExpressionTreeVisitors;
6+
using Remotion.Linq.Clauses.Expressions;
7+
using Remotion.Linq.Clauses.ResultOperators;
8+
9+
namespace NHibernate.Linq.Visitors
10+
{
11+
public class LeftJoinRewriter : QueryModelVisitorBase
12+
{
13+
public static void ReWrite(QueryModel queryModel)
14+
{
15+
new LeftJoinRewriter().VisitQueryModel(queryModel);
16+
}
17+
18+
public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel, int index)
19+
{
20+
var subQuery = fromClause.FromExpression as SubQueryExpression;
21+
if (subQuery == null)
22+
return;
23+
24+
var subQueryModel = subQuery.QueryModel;
25+
if (!IsLeftJoin(subQueryModel))
26+
return;
27+
28+
var mainFromClause = subQueryModel.MainFromClause;
29+
var join = NhJoinClause.Create(mainFromClause);
30+
31+
var innerSelectorMapping = new QuerySourceMapping();
32+
innerSelectorMapping.AddMapping(fromClause, subQueryModel.SelectClause.Selector);
33+
34+
queryModel.TransformExpressions(ex => ReferenceReplacingExpressionTreeVisitor.ReplaceClauseReferences(ex, innerSelectorMapping, false));
35+
36+
queryModel.BodyClauses.RemoveAt(index);
37+
queryModel.BodyClauses.Insert(index, @join);
38+
InsertBodyClauses(subQueryModel.BodyClauses, queryModel, index + 1);
39+
40+
var innerBodyClauseMapping = new QuerySourceMapping();
41+
innerBodyClauseMapping.AddMapping(mainFromClause, new QuerySourceReferenceExpression(@join));
42+
43+
queryModel.TransformExpressions(ex => ReferenceReplacingExpressionTreeVisitor.ReplaceClauseReferences(ex, innerBodyClauseMapping, false));
44+
}
45+
46+
private static void InsertBodyClauses(IEnumerable<IBodyClause> bodyClauses, QueryModel destinationQueryModel, int destinationIndex)
47+
{
48+
foreach (var bodyClause in bodyClauses)
49+
{
50+
destinationQueryModel.BodyClauses.Insert(destinationIndex, bodyClause);
51+
++destinationIndex;
52+
}
53+
}
54+
55+
private static bool IsLeftJoin(QueryModel subQueryModel)
56+
{
57+
return subQueryModel.ResultOperators.Count == 1 &&
58+
subQueryModel.ResultOperators[0] is DefaultIfEmptyResultOperator;
59+
}
60+
}
61+
}

src/NHibernate/Linq/Visitors/QueryModelVisitor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel quer
2222
{
2323
SubQueryFromClauseFlattener.ReWrite(queryModel);
2424

25+
LeftJoinRewriter.ReWrite(queryModel);
26+
2527
NestedSelectRewriter.ReWrite(queryModel, parameters.SessionFactory);
2628

2729
// Remove unnecessary body operators

src/NHibernate/NHibernate.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@
295295
<Compile Include="Linq\NestedSelects\Tuple.cs" />
296296
<Compile Include="Linq\NestedSelects\SelectClauseRewriter.cs" />
297297
<Compile Include="Linq\NestedSelects\ExpressionHolder.cs" />
298+
<Compile Include="Linq\Visitors\LeftJoinRewriter.cs" />
298299
<Compile Include="Linq\Visitors\NhPartialEvaluatingExpressionTreeVisitor.cs" />
299300
<Compile Include="Linq\QuerySourceNamer.cs" />
300301
<Compile Include="Linq\ReWriters\AddJoinsReWriter.cs" />

0 commit comments

Comments
 (0)