-
Notifications
You must be signed in to change notification settings - Fork 934
Full control of entities fetching in Criteria #1599
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
Conversation
/// <param name="selectMode"></param> | ||
/// <param name="associationPath">Criteria association path. If empty root entity for given criteria is used.</param> | ||
/// <param name="alias">Criteria alias. If empty current <see cref="ICriteria"/> criteria is used.</param> | ||
void SetSelectMode(SelectMode selectMode, string associationPath, string alias); |
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.
Shouldn't it return ICriteria
?
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.
Yeah. It should. Fixed.
There is something forgotten: documenting the new feature in the reference documentation. I think this new feature deserve a mention into criteria and queryover chapters. (Ok you have been notified of this already twice on two other already merged PR. If updating the doc here, do not do it for the other PR at the same time, because the other PRs should be documented in a 5.1.1 PR if possible, while this PR here would be merged in 5.2.) |
Why the decision has been made to put this as a separate method? For me it's counterintuitive. Could it be just an additional parameter on existing methods (JoinQueryOver, etc.)? |
Because functionality is not bounded to (Join...) or any other existing methods. SelectMode for instance can be applied for root query (or any other current criteria):
But it also can be applied only to particular list of aliases (so select mode can be set in the end after query is written).
It's more like existing |
Approved in principle. However I'm still not convinced with the method name: I think that this feature would be hard to discover.
Can you show some examples of what you mean? |
If you really need it - you will find it :) But if you have in mind a better name - just let me know. @hazzik query
.Fetch(p => p.Child1).Eager
.Fetch(p => p.Child2).Eager can be rewritten as query
.With(SelectMode.Fetch, p => p.Child1, p => p.Child2) Plus it supports fetching by aliases (not supported by Fetch and criteria SetFetchMode methods); NotAssociatedEntity notAssociatedAlias = null;
ChildEntity aliasForList = null;
query.JoinEntityAlias(() => notAssociatedAlias, ... )
.JoinAlias(p => p.ChildrenList, () => childList);
.With(SelectMode.Fetch, () => notAssociatedAlias.Child, () => childList) So SelecMode currently is the only option for fetching inner joined collections and entity joins children. Plus all other SelectMode options explained in pull request. And instead of query
.Fetch(SelectMode.Fetch, p => p.Child1, p => p.Child2) and query.JoinEntityAlias(() => notAssociatedAlias, ... )
.JoinAlias(p => p.ChildrenList, () => childList);
.Fetch(SelectMode.Fetch, () => notAssociatedAlias.Child, () => childList) But if you agree |
rebased + fixed CriteriaImpl.Clone |
src/NHibernate/Loader/SelectMode.cs
Outdated
/// Use it for fetching child objects for already loaded entities. | ||
/// Note: Missing entities in session will be returned as null | ||
/// </summary> | ||
ChildFetch, |
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.
Note: Missing entities in session will be returned as null
What does that mean? That using this mode could result in persisted entities to be returned as null if they were not already loaded in the session? If yes, this is a very unusual behavior for NHibernate. It should yield a proxy instead. (Or throw an error, or trigger a load, whatever, but guarantee no inconsistent data is returned.)
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.
If yes, this is a very unusual behavior for NHibernate
You should be ready for null if you are doing right join for instance. This option is useful strictly for "fetch only" queries - you should not really use result of query with SelectMode.ChildFetch.
You must use result of parent query instead. So I don't think of it as inconsistency - it's just the implementation details: you just can't load parent for one ID and try to fetch child for another ID and expect it to work. So I think nulls are quite appropriate results for the following case:
var parentResultsToUse = s.QueryOver<Parent>.Where(p => p.Id =1).List();
// Correct usage - just load query to initialize children; ignore returning results
s.QueryOver<Parent>()
.With(SelectMode.ChildFetch, p => p)
.With(SelectMode.Fetch, p => p.Child1, p => p.Child2)
.Where(p => p.Id = 1)
.List();
//Incorrect Usage - child fetch for different ID; results are saved for further usage
var resultWithNulls = s.QueryOver<Parent>()
.With(SelectMode.ChildFetch, p => p)
.With(SelectMode.Fetch, p => p.Child1, p => p.Child2)
.Where(p => p.Id = 2)
.List();
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.
You should be ready for null if you are doing right join for instance.
Yes, but that does not look to me as a right join case here.
There are not any cases in NHibernate where a query result correctness depends on having already loaded these entities in the session. Yielding null
in the query results instead of an entity (or proxy of an entity) because this entity was not already loaded in the session is not acceptable in my opinion.
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.
We are talking about Loader logic. We are processing an open data reader with only ID columns present for ChildFetch entities. It's not the right place to load any entities here. And returning a proxy is also a bad idea - object might not be proxified. To me null here is just logical and easiest solution - results of those queries should not be processed as is anyway.
I would agree with you if I've changed existing behavior. But that's completely new feature - why can't it have specific documented behavior. Those nulls are not going anywhere in session except resulting list returned by query execution.
I don't see how it's any different with current collections fetches - by default root entities are duplicated in results. Yep it's not nulls but you can't just process results as is - you need to know this behavior and use appropriate result transformer.
You suggestion will considerable complicate Loader code with no added benefits (I'm not even sure how to proceed - you can check how Loader.IsChildFetchEntity is used now).
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.
It adds a notable benefit: it avoids a case of query yielding incorrect results. I maintain my point: adding a query case which can yield null instead of the expected entity does not seem acceptable. This can only end up reported as a bug.
I do not think adding a feature with this kind of special semantic is a good move.
returning a proxy is also a bad idea - object might not be proxified
A load that does not actually load the returned entities calls for proxy availability. The feature should just throw an exception when used on entities for which proxies are disabled. (Or default to fully load the entity in a dedicated query, as the Loader is already capable of, like illustrated by this case.)
The collection fetching case is a legacy users have to cope with. (Due to this kind of troubles with NHibernate fetching, my personal recommendation with NHibernate is to use batching of lazy loads instead. It solves the n+1 loads trouble while keeping things simple, without needs to split queries or de-duplicate results or give up paging, ...)
You suggestion will considerable complicate Loader code with no added benefits (I'm not even sure how to proceed - you can check how Loader.IsChildFetchEntity is used now).
This case demonstrates the Loader is already able of loading non proxified entities when required with an additional dedicated query. Of course this may then end up with potentially a n+1 loads when a user somewhat incorrectly uses this ChildFetch
feature on non-proxifiable entities which are not already loaded in the session. But that looks to me still better than yielding null instead of the queried entities. As of whether this capability is usable in the case of the ChildFetch
or not, I have not yet studied it.
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.
It appears to me that yielding null
will moreover wreck child collection loading, defeating the purpose of the feature. See Loader.ReadCollectionElement
, which is called on the row array. It may end up calling EntityType.ResolveIdentifier
which has an early exit bypassing instantiating the element if the owner is null.
@@ -1646,8 +1646,27 @@ public object NotFoundObject | |||
|
|||
public abstract SqlString WhereJoinFragment(string alias, bool innerJoin, bool includeSubclasses); | |||
|
|||
// 6.0 TODO: Remove (Replace with ISupportSelectModeJoinable.SelectFragment) | |||
public abstract string SelectFragment(IJoinable rhs, string rhsAlias, string lhsAlias, string currentEntitySuffix, |
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.
Things that are to be removed have to be obsoleted. This eases the migration path.
src/NHibernate/Type/TypeHelper.cs
Outdated
@@ -14,6 +14,12 @@ public static partial class TypeHelper | |||
{ | |||
public static readonly IType[] EmptyTypeArray = Array.Empty<IType>(); | |||
|
|||
internal static T CastOrThrow<T>(object obj, string supportMessage) where T : class |
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.
TypeHelper
is about NHibernate types (IType
). Better put this in ReflectHelper
.
src/NHibernate/Loader/SelectMode.cs
Outdated
/// Use it for fetching child objects for already loaded entities. | ||
/// Note: Missing entities in session will be returned as null | ||
/// </summary> | ||
ChildFetch, |
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.
It appears to me that yielding null
will moreover wreck child collection loading, defeating the purpose of the feature. See Loader.ReadCollectionElement
, which is called on the row array. It may end up calling EntityType.ResolveIdentifier
which has an early exit bypassing instantiating the element if the owner is null.
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.
About the naming, I am for changing With
to Fetch
, but also obsolete the previous Fetch
, since the new one covers all its use cases.
💯% agree. I would also request to change parameter positions: Fetch(path, SelectMode) If this is possible |
@fredericDelaporte Wow. Thanks for you changes. If it works it would really be better approach. For some reasons I thought that another entity loading in loader will lead to multiple open data readers that might not be supported by database. I've added test to ensure that this scenario works. |
Indeed, but this is already handled by NHibernate. See |
Regarding How about |
if (!alreadyLoaded && mustLoadMissingEntity) | ||
{ | ||
// Missing in session while its data has not been selected: fallback on immediate load | ||
obj = session.ImmediateLoad(key.EntityName, key.Identifier); |
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.
I have first attempted to do an InternalLoad
here instead, without accounting for returnProxies
/mustLoadMissingEntity
, and without changing anything to GetRow
caller. But this was (as expected) potentially yielding a proxy, which GetRow
caller was not supporting: it was forcing the proxy initialization with itself as implementation, resulting in stack overflows when attempting to access the entity state.
For this reason, when proxy are supported, better yield null
here and let GetRow
caller handle returning a proxy.
obj = session.ImmediateLoad(key.EntityName, key.Identifier); | ||
} | ||
rowResults[i] = obj; | ||
continue; |
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.
Now completely skipping InstanceAlreadyLoaded
and CacheByUniqueKey
for child-fetch cases.
InstanceAlreadyLoaded
handle the locking: should we lock a thing for which we only have selected its identifier, and which the query was designed to not fetch?
Likewise, handling caching for an entity which the query is not meant to actually load should not be done.
if (entity != proxy) | ||
{ | ||
// Force the proxy to resolve itself | ||
((INHibernateProxy) proxy).HibernateLazyInitializer.SetImplementation(entity); |
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.
I was tempted to remove that line, because ProxyFor
implementation already handles it. But well, that is not the subject of this PR.
I am for keeping the |
Actually I've realized And I'm not sure that we should drop/obsolete existing So
|
Yes but that is, in the QueryOver case, an internal implementation detail already quite hidden away from user. And the select mode does also operate the In the criteria case, I think |
src/NHibernate/Impl/CriteriaImpl.cs
Outdated
@@ -15,12 +16,13 @@ namespace NHibernate.Impl | |||
/// Implementation of the <see cref="ICriteria"/> interface | |||
/// </summary> | |||
[Serializable] | |||
public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria | |||
public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria, ISupportSelectModeCriteria | |||
{ | |||
private readonly System.Type persistentClass; | |||
private readonly List<CriterionEntry> criteria = new List<CriterionEntry>(); | |||
private readonly List<OrderEntry> orderEntries = new List<OrderEntry>(10); | |||
private readonly Dictionary<string, FetchMode> fetchModes = new Dictionary<string, FetchMode>(); |
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.
This should likely be removed, in favor of storing only select modes, with the obsoleted Fetch
being translated to the new one for keeping it working.
/// <param name="associationPath">The criteria association path. If empty, the root entity for the given | ||
/// criteria is used.</param> | ||
/// <param name="alias">The criteria alias. If empty, the current <see cref="ICriteria"/> criteria is used.</param> | ||
ICriteria SetSelectMode(SelectMode selectMode, string associationPath, string alias); |
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.
Either rename to Fetch
too or rename the ICriteria
extension to SetSelectMode
. Otherwise, once merged, the ICriteria
API would expose two ways of doing exactly the same thing, which would be confusing for the user.
So it seems I've adressed all requested changes. Some notes: s.QueryOver<Entity>().JoinQueryOver(e => e.Child).Fetch(SelectMode.Lazy, child => child) I would expect inner join present in query - I just don't want it to be eagerly fetched. But in fact the way I've added shortcuts for SelectMode.Fetch criteria.Fetch("Child");
queryOver.Fetch(c => c.Child); But QueryOver version is hidden by obsolete queryOver.Fetch(c => c.Child, c => c.Child2 ); |
Do additional minor cleanup and comments adjustments Update documentation
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.
With
SkipJoin
name I think it's more clear.
So SelectMode.SkipJoin
is for completely ignoring the relation (not selected, not joined) while SelectMode.Skip
is for only not selecting it (but still joined, for filtering).
Maybe both should be renamed, like SelectMode.JoinOnly
instead of Skip
, then SkipJoin
could be renamed just Skip
.
I've added shortcuts for SelectMode.Fetch
Along with SelectMode.Default
current description:
Default behavior (as if no select mode is specified).
This is a bit confusing, since the shortcut resolves to SelectMode.Fetch
. The description of Default
should be changed to the one used for FetchMode.Default
.
The documentation also needs some updates.
|
||
[Test] | ||
public void LazyRootEntityIsNotSupported() | ||
{using (var sqlLog = new SqlLogSpy()) |
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.
This looks like a forgotten line of code.
Assert.Throws<NotSupportedException>(() => query.SingleOrDefault()); | ||
} | ||
} | ||
|
||
//6.0 TODO: Remove tests wrapped in pragma below | ||
#pragma warning disable 618 |
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.
Just mark tests for obsolete things as obsolete themselves.
@@ -482,10 +482,12 @@ protected internal QueryOver(CriteriaImpl rootImpl, ICriteria criteria) | |||
get { return new QueryOverSubqueryBuilder<TRoot,TSubType>(this); } | |||
} | |||
|
|||
#pragma warning disable CS0612 // Type or member is obsolete |
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.
Mark as obsolete instead.
IQueryOverFetchBuilder<TRoot,TSubType> IQueryOver<TRoot,TSubType>.Fetch(Expression<Func<TRoot, object>> path) | ||
{ return new IQueryOverFetchBuilder<TRoot,TSubType>(this, path); } | ||
#pragma warning restore CS0612 // Type or member is obsolete |
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.
Mark as obsolete instead.
@@ -7,6 +7,8 @@ namespace NHibernate.Criterion | |||
{ | |||
public static class QueryOverBuilderExtensions | |||
{ | |||
#pragma warning disable CS0612 // Type or member is obsoletes | |||
|
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.
Mark them obsolete instead.
src/NHibernate/FetchMode.cs
Outdated
@@ -33,4 +32,4 @@ public enum FetchMode | |||
Lazy = Select, | |||
Eager = Join |
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.
Those two values were making sens only for their usage in criteria fetches. They should be obsoleted too.
src/NHibernate/Impl/CriteriaImpl.cs
Outdated
@@ -379,10 +377,28 @@ public ICriteria AddOrder(Order ordering) | |||
|
|||
public ICriteria SetFetchMode(string associationPath, FetchMode mode) |
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.
This should be obsoleted too.
But this So I would prefer to have shorter name for more useful mode (so keep it as Skip). Do you think that "SelectMode.Skip" name is confusing?
Maybe I should rename SelectMode.Default to SelectMode.Undefined? As indeed Default might be confusing. |
For skipping only the select part and keeping the join, I find that name somewhat confusing. It can be understood that the skip relates to the select clause only due to the name of the enumeration, even if that is not very straightforward. But along with having a
I have already rewritten the description, but finding a better name is a good idea. What about So a choice has to be made between:
Looking at the list causes me to wonder, couldn't "Do not fetch, fetch children instead" and "Do not fetch, just join" be combined in a single |
But why? From implementation standpoint "Do not fetch, just join" is simple change. Why to merge it with much more complex scenario? And you can't skip that way non-proxified objects: //collection elements won't be loaded to session
s.QueryOver<Parent>().JoinQueryOver(p => p.ChildListNonProxified).Fetch(SelectMode.Skip, l => l);
//collection elements will be loaded to session with separate query for each object
s.QueryOver<Parent>().JoinQueryOver(p => p.ChildListNonProxified).Fetch(SelectMode.ChildFetch, l => l); Regarding Regarding s.QueryOver<Parent>().Fetch( SelectMode.AsMapped, p => p.LazyChild, p => p.LazyChild.EagerSubChild); Do you think it's clear that both s.QueryOver<Parent>()
.Fetch( SelectMode.ChildFetch, p => p.LazyChild)
.Fetch( SelectMode.Fetch, p => p.LazyChild.EagerSubChild); |
About merging the two cases that are looking similar, that was just a question. If that creates to much complexity, forget about it. Ok for |
The new error message in IQueryOver.Fetch "Use Fetch(SelectMode mode, Expression<Func<TSubType, object>> path) instead" does not help a lot. A Fetch method with two parameters is not part of the interface IQueryOver so it could not been called. |
It's not part of interface to avoid breaking changes (will be in 6.0) - it's an extension method (in namespace NHibernate). See here |
Ah thanks a lot! |
Does this work on Lazy properties too? Because for mapping like this: |
Individual lazy property fetch implemented in another PR: #2097 (not yet commited) |
I see, that worked. Thanks. |
Old behavior: ManyToOne entities joined via
JoinAlias/JoinQueryOver/Join...
are always fetched.OneToMany collections are only fetched when they are joined via LeftOuterJoin, but element entities are fetched anyway.
And you have no control over it.
With this PR:
You can explicitly control fetching behavior via setting
SelectMode
enum for criteria association paths. So with:1)
SelectMode.Fetch
you can fetch current criteria/alias association path (if persister supports it). So now it's possible to fetch collections with inner or whatever join you want:link to test
2)
SelectMode.JoinOnly
you can skip current criteria/alias association path to be fetched (so it won't be included in SELECT statement):link to test
Note: JoinOnly for root entity is not implemented (It's not trivial task and it's too much additional code change noise, so maybe later with another PR). But you can use
SelectMode.ChildFetch
instead (see below).3)
SelectMode.FetchLazyProperties
you can fetch entities with lazy properties initialized:link to test
4)
SelectMode.Undefined
- Basically no-op for queries:link to test
5)
SelectMode.ChildFetch
- Experimental feature. The main idea is to allow effective multiple collections fetching in one database roundtrip. In this mode only entity Identifier columns are added to SELECT statement. Retrieved identifier is used only to obtain object from session and use it further for child entities fetching. If object is not found in session it's returned as null. Example of usage:link to test
Combining JoinOnly, ChildFetch and Fetch allows to do some crazy multilevel collections fetching in one database roundtrip:
link to test
So here
root.ChildrenList.Children.Children
- 3 level deep collections are fetched in 1 db roundtrip.6) `SelectMode.Skip'- skips join (and so fetching) from query. Useful only to disable eager mapping association.
Functionality is exposed via
Fetch
extension methods onIQueryOver
interface taking current criteria paths or aliased criteria paths as parameters: