Skip to content

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

Merged
merged 5 commits into from
Dec 22, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/NHibernate.Test/Async/NHSpecificTest/GH1226/Fixture.cs
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

{
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();
}
}
}
}
}
84 changes: 84 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH1226/Fixture.cs
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())
Copy link
Member

@hazzik hazzik Dec 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please align nested usings:

using(...)
using(...)
{
}

{
session.Delete("from Account");
session.Delete("from Bank");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateQuery("delete ...").ExecuteUpdate()?

tx.Commit();
}
}
}
}
}
18 changes: 18 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH1226/Mappings.hbm.xml
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>
16 changes: 16 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH1226/Model.cs
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; }
}
}
2 changes: 2 additions & 0 deletions src/NHibernate/Async/Loader/Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,8 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer
entry.LockMode = lockMode;
}
}

CacheByUniqueKey(i, persister, obj, session);
}

/// <summary>
Expand Down
28 changes: 28 additions & 0 deletions src/NHibernate/Loader/Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this please be extracted into an own method?

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we unfold ? operator here? It looks unreadable.

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here persister is rootPersister. Non-root persister is a thing existing only inside LoadFromResultSet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

session.PersistenceContext.AddEntity(euk, obj);
}

/// <summary>
Expand Down