-
Notifications
You must be signed in to change notification settings - Fork 933
Join-fetching with property-ref results in select n+1 #1492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
00b36c0
912d210
c628196
2d610b1
7b74b9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
//------------------------------------------------------------------------------ | ||
// <auto-generated> | ||
// This code was generated by AsyncGenerator. | ||
// | ||
// Changes to this file may cause incorrect behavior and will be lost if | ||
// the code is regenerated. | ||
// </auto-generated> | ||
//------------------------------------------------------------------------------ | ||
|
||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1226 | ||
{ | ||
using System.Threading.Tasks; | ||
[TestFixture] | ||
public class FixtureAsync : BugTestCase | ||
{ | ||
protected override void OnSetUp() | ||
{ | ||
base.OnSetUp(); | ||
|
||
using (var session = OpenSession()) | ||
{ | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var bank = new Bank { Code = "01234" }; | ||
session.Save(bank); | ||
|
||
var account = new Account { Bank = bank }; | ||
session.Save(account); | ||
|
||
var account2 = new Account { Bank = bank }; | ||
session.Save(account2); | ||
|
||
tx.Commit(); | ||
} | ||
} | ||
} | ||
|
||
[Test] | ||
public async Task BankShouldBeJoinFetchedAsync() | ||
{ | ||
using (var session = OpenSession()) | ||
{ | ||
var wasStatisticsEnabled = session.SessionFactory.Statistics.IsStatisticsEnabled; | ||
session.SessionFactory.Statistics.IsStatisticsEnabled = true; | ||
|
||
long statementCount; | ||
|
||
using (var tx = session.BeginTransaction()) | ||
{ | ||
// Bug only occurs if the Banks are already in the session cache. | ||
var preloadedBanks = await (session.CreateQuery("from Bank").ListAsync<Bank>()); | ||
|
||
var countBeforeQuery = session.SessionFactory.Statistics.PrepareStatementCount; | ||
|
||
Console.WriteLine("Query: -------------------------------------------------------"); | ||
|
||
var accounts = await (session.CreateQuery("from Account a left join fetch a.Bank").ListAsync<Account>()); | ||
IList<Bank> associatedBanks = accounts.Select(x => x.Bank).ToList(); | ||
|
||
var countAfterQuery = session.SessionFactory.Statistics.PrepareStatementCount; | ||
statementCount = countAfterQuery - countBeforeQuery; | ||
|
||
Console.WriteLine("End ----------------------------------------------------------"); | ||
|
||
await (tx.CommitAsync()); | ||
} | ||
|
||
session.SessionFactory.Statistics.IsStatisticsEnabled = wasStatisticsEnabled; | ||
|
||
Assert.That(statementCount, Is.EqualTo(1)); | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
base.OnTearDown(); | ||
|
||
using (var session = OpenSession()) | ||
{ | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
session.Delete("from Account"); | ||
session.Delete("from Bank"); | ||
tx.Commit(); | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1226 | ||
{ | ||
[TestFixture] | ||
public class Fixture : BugTestCase | ||
{ | ||
protected override void OnSetUp() | ||
{ | ||
base.OnSetUp(); | ||
|
||
using (var session = OpenSession()) | ||
{ | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var bank = new Bank { Code = "01234" }; | ||
session.Save(bank); | ||
|
||
var account = new Account { Bank = bank }; | ||
session.Save(account); | ||
|
||
var account2 = new Account { Bank = bank }; | ||
session.Save(account2); | ||
|
||
tx.Commit(); | ||
} | ||
} | ||
} | ||
|
||
[Test] | ||
public void BankShouldBeJoinFetched() | ||
{ | ||
using (var session = OpenSession()) | ||
{ | ||
var wasStatisticsEnabled = session.SessionFactory.Statistics.IsStatisticsEnabled; | ||
session.SessionFactory.Statistics.IsStatisticsEnabled = true; | ||
|
||
long statementCount; | ||
|
||
using (var tx = session.BeginTransaction()) | ||
{ | ||
// Bug only occurs if the Banks are already in the session cache. | ||
var preloadedBanks = session.CreateQuery("from Bank").List<Bank>(); | ||
|
||
var countBeforeQuery = session.SessionFactory.Statistics.PrepareStatementCount; | ||
|
||
Console.WriteLine("Query: -------------------------------------------------------"); | ||
|
||
var accounts = session.CreateQuery("from Account a left join fetch a.Bank").List<Account>(); | ||
IList<Bank> associatedBanks = accounts.Select(x => x.Bank).ToList(); | ||
|
||
var countAfterQuery = session.SessionFactory.Statistics.PrepareStatementCount; | ||
statementCount = countAfterQuery - countBeforeQuery; | ||
|
||
Console.WriteLine("End ----------------------------------------------------------"); | ||
|
||
tx.Commit(); | ||
} | ||
|
||
session.SessionFactory.Statistics.IsStatisticsEnabled = wasStatisticsEnabled; | ||
|
||
Assert.That(statementCount, Is.EqualTo(1)); | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
base.OnTearDown(); | ||
|
||
using (var session = OpenSession()) | ||
{ | ||
using (var tx = session.BeginTransaction()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please align nested usings:
|
||
{ | ||
session.Delete("from Account"); | ||
session.Delete("from Bank"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
tx.Commit(); | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" | ||
assembly="NHibernate.Test" | ||
namespace="NHibernate.Test.NHSpecificTest.GH1226"> | ||
<class name="Account"> | ||
<id name="Id"> | ||
<generator class="guid"/> | ||
</id> | ||
<many-to-one fetch="join" name="Bank" property-ref="Code"/> | ||
</class> | ||
|
||
<class name="Bank"> | ||
<id name="Id"> | ||
<generator class="guid"/> | ||
</id> | ||
<property name="Code" unique="true"/> | ||
</class> | ||
</hibernate-mapping> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1226 | ||
{ | ||
public class Account | ||
{ | ||
public virtual Guid Id { get; set; } | ||
public virtual Bank Bank { get; set; } | ||
} | ||
|
||
public class Bank | ||
{ | ||
public virtual Guid Id { get; set; } | ||
public virtual string Code { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -972,6 +972,34 @@ private void InstanceAlreadyLoaded(DbDataReader rs, int i, IEntityPersister pers | |
entry.LockMode = lockMode; | ||
} | ||
} | ||
|
||
CacheByUniqueKey(i, persister, obj, session); | ||
} | ||
|
||
private void CacheByUniqueKey(int i, IEntityPersister persister, object obj, ISessionImplementor session) | ||
{ | ||
// #1226: If it is already loaded and can be loaded from an association with a property ref, make | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this please be extracted into an own method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
// sure it is also cached by its unique key. | ||
var ukName = OwnerAssociationTypes?[i]?.RHSUniqueKeyPropertyName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we unfold There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just null propagation all the way. Original code was: IAssociationType[] ownerAssociationTypes = OwnerAssociationTypes;
if (ownerAssociationTypes != null && ownerAssociationTypes[i] != null)
{
string ukName = ownerAssociationTypes[i].RHSUniqueKeyPropertyName; Would you rather have: var ownerAssociationTypes = OwnerAssociationTypes;
if (ownerAssociationTypes == null)
return;
var ukName = ownerAssociationTypes[i]?.RHSUniqueKeyPropertyName; |
||
if (ukName == null) | ||
return; | ||
var index = ((IUniqueKeyLoadable)persister).GetPropertyIndex(ukName); | ||
var ukValue = persister.GetPropertyValue(obj, index); | ||
// ukValue can be null for two reasons: | ||
// - Entity currently loading and not yet fully hydrated. In such case, it has already been handled by | ||
// InstanceNotYetLoaded on a previous row, there is nothing more to do. This case could also be | ||
// detected with "session.PersistenceContext.GetEntry(obj).Status == Status.Loading", but since there | ||
// is a second case, just test for ukValue null. | ||
// - Entity association is unset in session but not yet persisted, autoflush disabled: ignore. We are | ||
// already in an error case: querying entities changed in session without flushing them before querying. | ||
// So here it gets loaded as if it were still associated, but we do not have the key anymore in session: | ||
// we cannot cache it, so long for the additionnal round-trip this will cause. (Do not fallback on | ||
// reading the key in rs, this is stale data in regard to the session state.) | ||
if (ukValue == null) | ||
return; | ||
var type = persister.PropertyTypes[index]; | ||
var euk = new EntityUniqueKey(persister.EntityName, ukName, ukValue, type, session.Factory); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LoadFromRow's version caches by rootPersister.EntityName. Please add a test where the referenced entity is a hierarchical object. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok |
||
session.PersistenceContext.AddEntity(euk, obj); | ||
} | ||
|
||
/// <summary> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same