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

Conversation

fredericDelaporte
Copy link
Member

This is a rebase of #1460, with async regen, squashed, as it was asked here.

@@ -972,6 +972,29 @@ private void CheckVersion(int i, IEntityPersister persister, object id, object e
entry.LockMode = lockMode;
}
}

// #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.

@hazzik
Copy link
Member

hazzik commented Dec 16, 2017

@fredericDelaporte why not do it in the original PR? It seems to have "changes from contributors" enabled.

@fredericDelaporte
Copy link
Member Author

fredericDelaporte commented Dec 16, 2017

It seems to have "changes from contributors" enabled.

Yes but I have seen on some other PR that some people have difficulties handling forced pushes. So unless the contributor has already shown by its action on his PR that it is not a trouble for him, I now avoid doing forced pushes on their PR.

@gliljas
Copy link
Member

gliljas commented Dec 17, 2017

@fredericDelaporte Yes, force pushing on someone else's PR should only be done after communicating with the owner about it.

@hazzik
Copy link
Member

hazzik commented Dec 20, 2017

@fredericDelaporte, @kfedorchenko can the CacheByUniqueKey logic be unifed, eg. called this way:

//If the object is already loaded, return the loaded one
obj = session.GetEntityUsingInterceptor(key);
if (obj != null)
{
	//its already loaded so dont need to hydrate it
	InstanceAlreadyLoaded(rs, i, persisters[i], key, obj, lockModes[i], session);
}
else
{
	obj = InstanceNotYetLoaded(rs, i, persisters[i], key, lockModes[i], descriptors[i].RowIdAlias, optionalObjectKey, optionalObject, hydratedObjects, session);
}
CacheByUniqueKey(i, persisters[i], obj, session);

And remove this logic from LoadFromResultSet?

(I don't know if this is possible or not, it just a suggestion)

@fredericDelaporte
Copy link
Member Author

can the CacheByUniqueKey logic be unifed

No, there is a trap here: in the InstanceNotYetLoaded case, obj is not actually hydrated but just put in a "to be hydrated" list, with its values array already put in persistence context. So getting its unique key value from its properties as do CacheByUniqueKey will fail. We may try to get values back from the persistence context in such case, but this would cause a more complex code and also more redundant unique key caching when the entity appears in many rows.

@hazzik
Copy link
Member

hazzik commented Dec 20, 2017

I've discovered this "trap".

this code works:

obj = session.GetEntityUsingInterceptor(key);
var persister = persisters[i];
object[] values;
if (obj != null)
{
	//its already loaded so dont need to hydrate it
	InstanceAlreadyLoaded(rs, i, persister, key, obj, lockModes[i], session);
	values = persister.GetPropertyValues(obj);
	CacheByUniqueKey(i, persister, obj, session, values);
}
else
{
	obj = InstanceNotYetLoaded(rs, i, persister, key, lockModes[i], descriptors[i].RowIdAlias, optionalObjectKey, optionalObject, hydratedObjects, session);
	var entry = session.PersistenceContext.GetEntry(obj);
	values = entry.LoadedState;
}
CacheByUniqueKey(i, persister, obj, session, values);

Copy link
Member

@hazzik hazzik left a comment

Choose a reason for hiding this comment

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

Minor code issues, otherwise looks good.

{
// #1226: If it is already loaded and can be loaded from an association with a property ref, make
// 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;

using (var tx = session.BeginTransaction())
{
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()?


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(...)
{
}


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

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

@fredericDelaporte
Copy link
Member Author

this code works:

But it causes, in the already loaded case, to get all the entity properties from its tuplizer even if there is no unique key, which is quite heavier than getting only one property when there is an unique key. I would rather pass null as values for this case, and fallback on the single property.
Well I consider instead passing an alreadyLoaded boolean and handle that inside the method.

using (var session = OpenSession())
using (var tx = session.BeginTransaction())
{
// Bug only occurs if the Banks are already in the session cache.
Copy link
Member

Choose a reason for hiding this comment

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

Since the old code has changed, could this case please be checked as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean, checking eager fetching works for Bank not already in session too? It is likely this is already tested somewhere else, but why not.

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.

@fredericDelaporte fredericDelaporte merged commit ba8fb89 into nhibernate:master Dec 22, 2017
@fredericDelaporte fredericDelaporte deleted the 1226 branch December 22, 2017 11:13
fredericDelaporte added a commit to fredericDelaporte/nhibernate-core that referenced this pull request Apr 9, 2018
Fix key type mismatch in EntityUniqueKey
EntityUniqueKey was assuming the value to be always semi-resolved (to be
the identifier instead of the entity for an entity type). It was always
computing the value hashcode with the semi-resolved type, which is the
identifier type for an entity.
But this has changed with nhibernate#1492, which caches unique keys also for
already fully loaded entities. The value hashcode must then be computed
with the entity type, which accounts for proxies.
Otherwise the proxy loading may get triggered while the code is already
loading the proxy, which results in a stack overflow.

Add test case adapted from Michael Estermann repro

Fix nhibernate#1645

Co-authored-by: Michael Estermann <michael.estermann@bbv.ch>
fredericDelaporte added a commit that referenced this pull request Apr 10, 2018
Fix key type mismatch in EntityUniqueKey
EntityUniqueKey was assuming the value to be always semi-resolved (to be
the identifier instead of the entity for an entity type). It was always
computing the value hashcode with the semi-resolved type, which is the
identifier type for an entity.
But this has changed with #1492, which caches unique keys also for
already fully loaded entities. The value hashcode must then be computed
with the entity type, which accounts for proxies.
Otherwise the proxy loading may get triggered while the code is already
loading the proxy, which results in a stack overflow.

Add test case adapted from Michael Estermann repro

Fix #1645

Co-authored-by: Michael Estermann <michael.estermann@bbv.ch>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants