Skip to content

Commit ba8fb89

Browse files
Join-fetching with property-ref results in select n+1 (#1492)
* Fixes #1226 (NH-2534)
1 parent c020a40 commit ba8fb89

File tree

6 files changed

+398
-48
lines changed

6 files changed

+398
-48
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System.Linq;
12+
using NHibernate.Engine;
13+
using NHibernate.Persister.Entity;
14+
using NUnit.Framework;
15+
using NHibernate.Linq;
16+
17+
namespace NHibernate.Test.NHSpecificTest.GH1226
18+
{
19+
using System.Threading.Tasks;
20+
[TestFixture]
21+
public class FixtureAsync : BugTestCase
22+
{
23+
protected override void OnSetUp()
24+
{
25+
base.OnSetUp();
26+
27+
using (var session = OpenSession())
28+
{
29+
using (var tx = session.BeginTransaction())
30+
{
31+
var bank = new Bank { Code = "01234" };
32+
session.Save(bank);
33+
34+
var account = new Account { Bank = bank };
35+
session.Save(account);
36+
37+
var account2 = new Account { Bank = bank };
38+
session.Save(account2);
39+
40+
tx.Commit();
41+
}
42+
}
43+
Sfi.Statistics.IsStatisticsEnabled = true;
44+
}
45+
46+
[Test]
47+
public async Task BankShouldBeJoinFetchedAsync()
48+
{
49+
// Simple case: nothing already in session.
50+
using (var session = OpenSession())
51+
using (var tx = session.BeginTransaction())
52+
{
53+
var countBeforeQuery = Sfi.Statistics.PrepareStatementCount;
54+
55+
var accounts = await (session.CreateQuery("from Account a left join fetch a.Bank").ListAsync<Account>());
56+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
57+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
58+
"One bank or more was lazily loaded.");
59+
60+
var countAfterQuery = Sfi.Statistics.PrepareStatementCount;
61+
var statementCount = countAfterQuery - countBeforeQuery;
62+
63+
await (tx.CommitAsync());
64+
65+
Assert.That(statementCount, Is.EqualTo(1));
66+
}
67+
}
68+
69+
[Test]
70+
public async Task InSessionBankShouldBeJoinFetchedAsync()
71+
{
72+
using (var session = OpenSession())
73+
using (var tx = session.BeginTransaction())
74+
{
75+
// #1226 bug only occurs if the Banks are already in the session cache.
76+
await (session.CreateQuery("from Bank").ListAsync<Bank>());
77+
78+
var countBeforeQuery = Sfi.Statistics.PrepareStatementCount;
79+
80+
var accounts = await (session.CreateQuery("from Account a left join fetch a.Bank").ListAsync<Account>());
81+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
82+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
83+
"One bank or more was lazily loaded.");
84+
85+
var countAfterQuery = Sfi.Statistics.PrepareStatementCount;
86+
var statementCount = countAfterQuery - countBeforeQuery;
87+
88+
await (tx.CommitAsync());
89+
90+
Assert.That(statementCount, Is.EqualTo(1));
91+
}
92+
}
93+
94+
[Test]
95+
public async Task AlteredBankShouldBeJoinFetchedAsync()
96+
{
97+
using (var s1 = OpenSession())
98+
{
99+
using (var tx = s1.BeginTransaction())
100+
{
101+
// Put them all in s1 cache.
102+
await (s1.CreateQuery("from Bank").ListAsync());
103+
await (tx.CommitAsync());
104+
}
105+
106+
string oldCode;
107+
const string newCode = "12345";
108+
// Alter the bank code with another session.
109+
using (var s2 = OpenSession())
110+
using (var tx2 = s2.BeginTransaction())
111+
{
112+
var accounts = await (s2.Query<Account>().ToListAsync());
113+
foreach (var account in accounts)
114+
account.Bank = null;
115+
await (s2.FlushAsync());
116+
var bank = await (s2.Query<Bank>().SingleAsync());
117+
oldCode = bank.Code;
118+
bank.Code = newCode;
119+
await (s2.FlushAsync());
120+
foreach (var account in accounts)
121+
account.Bank = bank;
122+
await (tx2.CommitAsync());
123+
}
124+
125+
// Check querying them with s1 is still consistent
126+
using (var tx = s1.BeginTransaction())
127+
{
128+
var accounts = await (s1.CreateQuery("from Account a left join fetch a.Bank").ListAsync<Account>());
129+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
130+
Assert.That(associatedBanks, Has.All.Not.Null,
131+
"One bank or more failed loading.");
132+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
133+
"One bank or more was lazily loaded.");
134+
Assert.That(associatedBanks, Has.All.Property(nameof(Bank.Code)).EqualTo(oldCode),
135+
"One bank or more has no more the old code.");
136+
137+
await (tx.CommitAsync());
138+
// Do not check statements count: we are in a special case defeating the eager fetching, because
139+
// we have stale data in session for the bank code.
140+
// But check that the new code, supposed to be unknown for the session, is not cached.
141+
var persister = Sfi.GetEntityPersister(typeof(Bank).FullName);
142+
var index = ((IUniqueKeyLoadable) persister).GetPropertyIndex(nameof(Bank.Code));
143+
var type = persister.PropertyTypes[index];
144+
var euk = new EntityUniqueKey(persister.EntityName, nameof(Bank.Code), newCode, type, Sfi);
145+
Assert.That(s1.GetSessionImplementation().PersistenceContext.GetEntity(euk),
146+
Is.Null, "Found a bank associated to the new code in s1");
147+
}
148+
}
149+
}
150+
151+
protected override void OnTearDown()
152+
{
153+
base.OnTearDown();
154+
155+
using (var session = OpenSession())
156+
using (var tx = session.BeginTransaction())
157+
{
158+
session.CreateQuery("delete from Account").ExecuteUpdate();
159+
session.CreateQuery("delete from Bank").ExecuteUpdate();
160+
tx.Commit();
161+
}
162+
}
163+
}
164+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System.Linq;
2+
using NHibernate.Engine;
3+
using NHibernate.Persister.Entity;
4+
using NUnit.Framework;
5+
6+
namespace NHibernate.Test.NHSpecificTest.GH1226
7+
{
8+
[TestFixture]
9+
public class Fixture : BugTestCase
10+
{
11+
protected override void OnSetUp()
12+
{
13+
base.OnSetUp();
14+
15+
using (var session = OpenSession())
16+
{
17+
using (var tx = session.BeginTransaction())
18+
{
19+
var bank = new Bank { Code = "01234" };
20+
session.Save(bank);
21+
22+
var account = new Account { Bank = bank };
23+
session.Save(account);
24+
25+
var account2 = new Account { Bank = bank };
26+
session.Save(account2);
27+
28+
tx.Commit();
29+
}
30+
}
31+
Sfi.Statistics.IsStatisticsEnabled = true;
32+
}
33+
34+
[Test]
35+
public void BankShouldBeJoinFetched()
36+
{
37+
// Simple case: nothing already in session.
38+
using (var session = OpenSession())
39+
using (var tx = session.BeginTransaction())
40+
{
41+
var countBeforeQuery = Sfi.Statistics.PrepareStatementCount;
42+
43+
var accounts = session.CreateQuery("from Account a left join fetch a.Bank").List<Account>();
44+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
45+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
46+
"One bank or more was lazily loaded.");
47+
48+
var countAfterQuery = Sfi.Statistics.PrepareStatementCount;
49+
var statementCount = countAfterQuery - countBeforeQuery;
50+
51+
tx.Commit();
52+
53+
Assert.That(statementCount, Is.EqualTo(1));
54+
}
55+
}
56+
57+
[Test]
58+
public void InSessionBankShouldBeJoinFetched()
59+
{
60+
using (var session = OpenSession())
61+
using (var tx = session.BeginTransaction())
62+
{
63+
// #1226 bug only occurs if the Banks are already in the session cache.
64+
session.CreateQuery("from Bank").List<Bank>();
65+
66+
var countBeforeQuery = Sfi.Statistics.PrepareStatementCount;
67+
68+
var accounts = session.CreateQuery("from Account a left join fetch a.Bank").List<Account>();
69+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
70+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
71+
"One bank or more was lazily loaded.");
72+
73+
var countAfterQuery = Sfi.Statistics.PrepareStatementCount;
74+
var statementCount = countAfterQuery - countBeforeQuery;
75+
76+
tx.Commit();
77+
78+
Assert.That(statementCount, Is.EqualTo(1));
79+
}
80+
}
81+
82+
[Test]
83+
public void AlteredBankShouldBeJoinFetched()
84+
{
85+
using (var s1 = OpenSession())
86+
{
87+
using (var tx = s1.BeginTransaction())
88+
{
89+
// Put them all in s1 cache.
90+
s1.CreateQuery("from Bank").List();
91+
tx.Commit();
92+
}
93+
94+
string oldCode;
95+
const string newCode = "12345";
96+
// Alter the bank code with another session.
97+
using (var s2 = OpenSession())
98+
using (var tx2 = s2.BeginTransaction())
99+
{
100+
var accounts = s2.Query<Account>().ToList();
101+
foreach (var account in accounts)
102+
account.Bank = null;
103+
s2.Flush();
104+
var bank = s2.Query<Bank>().Single();
105+
oldCode = bank.Code;
106+
bank.Code = newCode;
107+
s2.Flush();
108+
foreach (var account in accounts)
109+
account.Bank = bank;
110+
tx2.Commit();
111+
}
112+
113+
// Check querying them with s1 is still consistent
114+
using (var tx = s1.BeginTransaction())
115+
{
116+
var accounts = s1.CreateQuery("from Account a left join fetch a.Bank").List<Account>();
117+
var associatedBanks = accounts.Select(x => x.Bank).ToList();
118+
Assert.That(associatedBanks, Has.All.Not.Null,
119+
"One bank or more failed loading.");
120+
Assert.That(associatedBanks, Has.All.Matches<object>(NHibernateUtil.IsInitialized),
121+
"One bank or more was lazily loaded.");
122+
Assert.That(associatedBanks, Has.All.Property(nameof(Bank.Code)).EqualTo(oldCode),
123+
"One bank or more has no more the old code.");
124+
125+
tx.Commit();
126+
// Do not check statements count: we are in a special case defeating the eager fetching, because
127+
// we have stale data in session for the bank code.
128+
// But check that the new code, supposed to be unknown for the session, is not cached.
129+
var persister = Sfi.GetEntityPersister(typeof(Bank).FullName);
130+
var index = ((IUniqueKeyLoadable) persister).GetPropertyIndex(nameof(Bank.Code));
131+
var type = persister.PropertyTypes[index];
132+
var euk = new EntityUniqueKey(persister.EntityName, nameof(Bank.Code), newCode, type, Sfi);
133+
Assert.That(s1.GetSessionImplementation().PersistenceContext.GetEntity(euk),
134+
Is.Null, "Found a bank associated to the new code in s1");
135+
}
136+
}
137+
}
138+
139+
protected override void OnTearDown()
140+
{
141+
base.OnTearDown();
142+
143+
using (var session = OpenSession())
144+
using (var tx = session.BeginTransaction())
145+
{
146+
session.CreateQuery("delete from Account").ExecuteUpdate();
147+
session.CreateQuery("delete from Bank").ExecuteUpdate();
148+
tx.Commit();
149+
}
150+
}
151+
}
152+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3+
assembly="NHibernate.Test"
4+
namespace="NHibernate.Test.NHSpecificTest.GH1226">
5+
<class name="Account">
6+
<id name="Id">
7+
<generator class="guid"/>
8+
</id>
9+
<many-to-one fetch="join" name="Bank" property-ref="Code"/>
10+
</class>
11+
12+
<class name="Bank">
13+
<id name="Id">
14+
<generator class="guid"/>
15+
</id>
16+
<property name="Code" unique="true"/>
17+
</class>
18+
</hibernate-mapping>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.GH1226
4+
{
5+
public class Account
6+
{
7+
public virtual Guid Id { get; set; }
8+
public virtual Bank Bank { get; set; }
9+
}
10+
11+
public class Bank
12+
{
13+
public virtual Guid Id { get; set; }
14+
public virtual string Code { get; set; }
15+
}
16+
}

0 commit comments

Comments
 (0)