Skip to content

Ability to select entities in Criteria projections #1471

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 14 commits into from
Dec 23, 2017

Conversation

bahusoid
Copy link
Member

@bahusoid bahusoid commented Dec 3, 2017

Another attempt to implement #948 (initially I re-based #302 in #1458 but it turns out only lazy entities are supported by this PR).

So with new implementatione entities are initialized by default (without lazy properties). Can be configured in fluent syntax:

Projections.Entity(()=> entityAlias)
.SetLazy(lazy)
.SetFetchLazyProperties(lazyProps)

See other examples in tests.

@bahusoid bahusoid changed the title #948(NH-3435) - Ability to select entities in Criteria projections #948(NH-3435) - Ability to select entities in Criteria projections (new version) Dec 3, 2017
@fredericDelaporte
Copy link
Member

Compilation warnings are turned into errors in the release build. The test has introduced some warnings, they need to be fixed.

public void RootEntityProjectionFullyInitializedAndWithUnfetchedLazyPropertiesByDefault()
{
using(var sqlLog = new SqlLogSpy())
using (var session = OpenSession())
Copy link
Member

Choose a reason for hiding this comment

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

Code formatting is not consistent, better issue a global reformat on new files (CTRL+K then CTRL+D in VS). (But it has to be avoided on already existing files, in order to avoid unrelated changes on them.)

There are some other cases of formatting glitches to fix in this file, I am not pointing them all with comments.

using (var session = OpenSession())
{
Sfi.Statistics.Clear();
EntityComplex entitypRoot = session
Copy link
Member

Choose a reason for hiding this comment

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

pRoot? I guess the p is a typo. Otherwise for what does it stand for?


namespace NHibernate.Criterion
{
using TThis = EntityProjection;
Copy link
Member

Choose a reason for hiding this comment

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

This is not a pattern used in NHibernate. I am not favorable to introducing it. I would rather have methods yielding their class do it under its actual name without using this alias.

using NHibernate.Transform;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH948
Copy link
Member

Choose a reason for hiding this comment

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

For having a more sound default ordering, please pad with 0 the number in order to have a four digits number. So the folder and the namespace should be renamed to GH0948.

Copy link
Member

Choose a reason for hiding this comment

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

In fact we favor putting tests in their functional area, so it would be even better to move them to Criteria/ProjectionsTest.cs.

Copy link
Member

Choose a reason for hiding this comment

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

I didn't do it because I have my set of entities specifically for my needs and I don't like hbm configuration (and it's not clear to me how to mix them).

Better add another test file in such case, like Criteria/EntityProjectionsTest.cs. Mixing mapping methods is not an option.

It doesn't look to me as clean solution to mix all sort of various tests depending on different data in one file and do set up/clean up for all of them for each test method. I would really prefer keep my tests in separate class

I do not really get that point since that is the fate of most test fixtures, even your isolated current one: it already sets up a composite entity for the last tests, unused by the other tests.
Of course we should try to reuse other test entities if possible by default. But when there is a good reason, not re-using them is ok too. Anyway when it is impractical to add to the existing fixture, you can add just another one in the same folder as I propose just above.

Maybe it's better to introduce Criteria/Projections folder? And put files there (for now two files - ProjectionsTest.cs and new EntityProjectionTest.cs)

They do not need their own folder. There are not so many fixtures in Criteria.


public TThis SetAllPropertyFetch(bool fetchLazyProperties = true)
{
FetchLazyProperties = fetchLazyProperties;
Copy link
Member

Choose a reason for hiding this comment

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

Naming inconsistency. I would rather have it named SetLazyPropertiesFetch.

using TThis = EntityProjection;

[Serializable]
public class EntityProjection : IProjection
Copy link
Member

Choose a reason for hiding this comment

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

This new class is exposed publicly by the criteria API, it should have xml comments on all its public members.


PropertyColumnAliases = Lazy
? new string[][] { }
: Enumerable.Range(0, Persister.PropertyNames.Length).Select(i => Persister.GetPropertyAliases(_columnAliasSuffix, i)).ToArray();
Copy link
Member

Choose a reason for hiding this comment

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

Haven't we an inconsistency here compared to ToSqlString implementation? If there is a lazy property, it looks to me it will be included here in the property aliases whatever the value of FetchLazyProperties, while it will not be in the emitted SQL if FetchLazyProperties is false. But I have not checked if this could create an issue.

Copy link
Member Author

@bahusoid bahusoid Dec 4, 2017

Choose a reason for hiding this comment

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

Indexes for PropertyColumnAliases must be the same as in Persister.PropertyNames (it's used somewhere deep inside for property mapping - don't remember exact place)

So I guess I could only add some micro-optimization to avoid alias calculation for lazy properties and return empty string collection for it. If you think it's worth it.

Copy link
Member

@fredericDelaporte fredericDelaporte Dec 4, 2017

Choose a reason for hiding this comment

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

So I guess I could only add some micro-optimization to avoid alias calculation for lazy properties and return empty string collection for it. If you think it's worth it.

No, how this part of the PR is currently is ok for me if it is already a constraint of current code base to always have lazy properties listed here.

using NHibernate.Transform;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH948
Copy link
Member

Choose a reason for hiding this comment

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

In fact we favor putting tests in their functional area, so it would be even better to move them to Criteria/ProjectionsTest.cs.

_entityAlias = criteria.Alias;
}

Persister = (IQueryable) criteriaQuery.Factory.GetEntityPersister(RootEntity.FullName);
Copy link
Member

@fredericDelaporte fredericDelaporte Dec 4, 2017

Choose a reason for hiding this comment

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

This cast will fail if a custom persister is used. The code should handle more gracefully this case, maybe with something like:

Persister = criteriaQuery.Factory.GetEntityPersister(RootEntity.FullName) as IQueryable ??
    throw new InvalidOperationException($"Projecting to entities requires a NHibernate.Persister.Entity.IQueryable persister, {RootEntity.FullName} does not have one.");

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok.

For some reasons I can't comment under your previous comment. So answering here:

In fact we favor putting tests in their functional area, so it would be even better to move them to Criteria/ProjectionsTest.cs.

I didn't do it because I have my set of entities specifically for my needs and I don't like hbm configuration (and it's not clear to me how to mix them). It doesn't look to me as clean solution to mix all sort of various tests depending on different data in one file and do set up/clean up for all of them for each test method. I would really prefer keep my tests in separate class
Maybe it's better to introduce Criteria/Projections folder? And put files there (for now two files - ProjectionsTest.cs and new EntityProjectionTest.cs)

Copy link
Member

Choose a reason for hiding this comment

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

I have slightly simplified my proposed code in previous comment.

For some reasons I can't comment under your previous comment.

I have seen this happening on the conversation page, where comments appears somewhat regrouped by their respective review, and may appears in subsequent ones too. You have to find the "right" group of comments. Or switch to files tab and answer from there.


PropertyColumnAliases = Lazy
? new string[][] { }
: Enumerable.Range(0, Persister.PropertyNames.Length).Select(i => Persister.GetPropertyAliases(_columnAliasSuffix, i)).ToArray();
Copy link
Member

@fredericDelaporte fredericDelaporte Dec 4, 2017

Choose a reason for hiding this comment

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

So I guess I could only add some micro-optimization to avoid alias calculation for lazy properties and return empty string collection for it. If you think it's worth it.

No, how this part of the PR is currently is ok for me if it is already a constraint of current code base to always have lazy properties listed here.

return entity;
}

private static object CreateInitializedEntity(DbDataReader rs, ISessionImplementor session, IQueryable persister, object identifier, string[][] propertyAliases, LockMode lockMode, bool fetchLazyProperties, bool readOnly)
Copy link
Member

Choose a reason for hiding this comment

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

I guess all that has been deducted from the loader logic. I am uncomfortable with having it re-coded here. I wonder how eager loading works for other cases like HQL or Linq. We should try to re-use its implementations for those other cases.
(From how is implemented ManyToOneType and EntityType, I tend to think that from the query results something hydrates and put eagerly fetched entities into the session cache before the ManyToOneType is hydrated, allowing it to not query the db.)

Copy link
Member Author

Choose a reason for hiding this comment

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

The thing is - I didn't find anything reusable.

I tend to think that from the query results something hydrates and put eagerly fetched entities into the session cache before the ManyToOneType is hydrated, allowing it to not query the db

I'm not sure I'm following. How this logic is related here - we are in the state when query is already executed. And we need to materialize our results to entities.
My current understanding ManyToOneType doesn't support loading of initialized entities. It only reads identifier from data reader. If entity needs to be fully initialized - separate query is executed (that's exact behavior of #302)

Copy link
Member Author

@bahusoid bahusoid Dec 4, 2017

Choose a reason for hiding this comment

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

I'm not sure I'm following. How this logic is related here

Ok - I got it now ) And got me thinking how my code would behave if some entities are already in session.

Copy link
Member

Choose a reason for hiding this comment

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

And got me thinking how my code would behave if some entities are already in session.

Good point, things would likely not be coherent in such case with current state of the PR. Normally NHibernate yields the entity already in the session, with the state it has already, disregarding the data from the query. So this is another reason to check how eager loading is implemented for other cases. (I have never checked that, I am not more knowledgeable than you on this subject.)

@bahusoid
Copy link
Member Author

bahusoid commented Dec 8, 2017

@fredericDelaporte I tried to address all the requested changes.
Also now "materializing entities" logic is reused from Loader (Now this change is more intrusive - as I needed to change Loader code to make it reusable).

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Dec 11, 2017

I am sorry, this is not what I was asking for.
You should not have to call the loader logic from the entity type. The new entity type should not even be needed.

I think the work should be done by adapting the CriteriaLoader for it to handle your new projection semantic (projection yielding entities, which should likely be typed with ManyToOne). Currently, when it sees a projection, the logic for handling entities in the result set is by-passed, as if it were assuming a projection can not load entities. See CriteriaJoinWalker constructor.

I think your feature requires changes in CriteriaLoader/CriteriaJoinWalker for enabling the entity loading for projections. Then the loader will do its work, loading the entities by the way, without additional changes in it. Loading entities is the responsibility of the loader, it should not need to be "externalized" in an additional NHibernate type as this PR do.

This entity loading should happen in Loader.GetRow, but currently it cannot because the query "tells" the loader there are no entities to be loaded (persisters and keys array parameters are empty).

@bahusoid
Copy link
Member Author

@fredericDelaporte Ok. Thanks for pointers. Will look into it...

@fredericDelaporte
Copy link
Member

I have got hinted at these pointers by reviewing #1460. From the loader I have searched what should be changed for causing it to load the entities.

@bahusoid bahusoid force-pushed the GH948New branch 2 times, most recently from 28639ee to 1c61763 Compare December 17, 2017 02:57
@bahusoid
Copy link
Member Author

@fredericDelaporte I've made yet another attempt to implement it. Could you please review my latest commit and share your thoughts. The challenge with your suggested approach - how to inject info about "entities projections" as CriteriaJoinWalker has access only for single IProjection that wraps any other projections in to ProjectionList or some other projections aggregator.

I've extended CriteriaQueryTranslator (that's supplied to projection as ICriteriaQuery parameter) implementation with ISupportEntityProjectionCriteriaQuery.RegisterEntityProjection interface (ICriteriaQuery can be extended directly instead). That allows me to register entities in translator right from projection:
https://github.com/bahusoid/nhibernate-core/blob/1c6176316bc0e1b73e9155c6174b109a05b6ad8d/src/NHibernate/Criterion/EntityProjection.cs#L160-L169

Copy link
Member

@fredericDelaporte fredericDelaporte left a comment

Choose a reason for hiding this comment

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

I have not yet checked all the details but it looks good.
I will do another review later.

ICriteriaQuery can be extended directly instead

This could be a "6.0 Todo". Adding methods to an interface is a breaking change and so cannot happen in a minor version. Add a comment like this above ISupportEntityProjectionCriteriaQuery:

// 6.0 Todo: merge into `ICriteriaQuery`.

/// </summary>
public static T AsEntity<T>(this T alias) where T:class
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
Copy link
Member

Choose a reason for hiding this comment

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

Do not throw bare Exception. For such case we tend to throw an InvalidOperationException (as in MappedAs Linq function).

Copy link
Member Author

Choose a reason for hiding this comment

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

I just copy-pasted it from some method above. Actually all methods in this ProjectionsExtensions class throw this Exception("Not to be used directly.... So I guess it's better be uniform and throw the same kind of exception for all cases. And if change it - it should be done for all cases at once and not in scope of this PR. Don't you agree?

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I was thinking the code was already purged from throwing bare Exception, but in fact that is only ApplicationException which were removed (#602, its title applies to the cases where it served as base class).

So yes, keep it that way.

Copy link
Member

Choose a reason for hiding this comment

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

Need to be aligned with #1498

@fredericDelaporte fredericDelaporte self-requested a review December 19, 2017 14:46
Copy link
Member

@fredericDelaporte fredericDelaporte left a comment

Choose a reason for hiding this comment

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

There is some clean-up to do and a breaking change to fix, but otherwise it still looks good.

Now since I am not using Criteria or QueryOver much, it would be better if someone else review this too.

/// <summary>
/// Entity alias
/// </summary>
public string[] Aliases
Copy link
Member

Choose a reason for hiding this comment

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

All IProjection members are explicitly implemented excepted this one, but I do not see for which reason.

By the way we tend to use expressions for such properties, when coding some new one: bool IProjection.IsAggregate => false; by example.


SqlString IProjection.ToSqlString(ICriteria criteria, int position, ICriteriaQuery criteriaQuery)
{
//return new SqlString(string.Empty);
Copy link
Member

Choose a reason for hiding this comment

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

Forgotten trash?


SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery)
{
throw new NotImplementedException();
Copy link
Member

Choose a reason for hiding this comment

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

For this method, all existing throwing cases are throwing throw new InvalidOperationException("not a grouping projection"); instead.


TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
{
throw new NotImplementedException();
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be return Array.Empty<TypedValue>();? There are no SQL parameters associated with this projection, so just return empty. That is already done so for SimpleProjection by example.

/// Create an alias for a projection
/// </summary>
/// <param name="projection">the projection instance</param>
/// <param name="alias">LambdaExpression returning an alias</param>
Copy link
Member

Choose a reason for hiding this comment

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

It looks like a string here. And this method seems to be a somewhat unrelated small feature addition.

Copy link
Member Author

Choose a reason for hiding this comment

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

@fredericDelaporte Yep. It's accidentally leaked here. But can I keep it? It's convenient extension for using with nameof (which I think should be preferred over lambda expression):
Projections.RootEntity().WithAlias(nameof(r.Root))
vs
Projections.RootEntity().WithAlias(() => r.Root)

Though it's easily can be placed directly in user code - I'm not going to push it :) .

Copy link
Member

Choose a reason for hiding this comment

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

I think it can stay.

About lambda vs nameof, I agree. Almost all QueryOver API is somewhat obsoleted by nameof, since it allows using the Criteria API without magic strings.


InitPersisters(allAssociations, lockMode);
InitStatementString(whereString, orderByString, lockMode);
}

protected void InitProjection(SqlString projectionString, SqlString whereString, SqlString orderByString, SqlString groupByString, SqlString havingString, IDictionary<string, IFilter> enabledFilters, LockMode lockMode)
protected void InitProjection(SqlString projectionString, SqlString whereString, SqlString orderByString, SqlString groupByString, SqlString havingString, IDictionary<string, IFilter> enabledFilters, LockMode lockMode, IList<EntityProjection> entityProjections = null)
Copy link
Member

Choose a reason for hiding this comment

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

This is a breaking change. Better obsolete the previous signature, telling to call the new one, and call the new one from the old one. (The default value for the new parameter will not avoid this to be a binary compatibility issue. Drop that default value by the way.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok. Will do. But are you sure regarding binary compatibility? I was under impression that default value is just a syntactic sugar - 2 methods should be generated in IL. So it should not be a breaking change (I didn't make actual tests - will do it later too)

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately it will generate one 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.

Yep. Just checked with Resharper IL Viewer window.. Good to know..

InitStatementString(projectionString, whereString, orderByString, groupByString, havingString, lockMode);

Copy link
Member

Choose a reason for hiding this comment

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

Undue whitespace, better remove this tabs line.

@@ -26,6 +26,7 @@ public class CriteriaQueryTranslator : ICriteriaQuery
private readonly string rootEntityName;
private readonly string rootSQLAlias;
private int indexForAlias = 0;
private IList<EntityProjection> _entityProjections = new List<EntityProjection>();
Copy link
Member

Choose a reason for hiding this comment

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

This can be readonly.

Copy link
Member

Choose a reason for hiding this comment

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

Also, please make field type of List instead of IList to avoid unnecessary boxing.

Copy link
Member Author

Choose a reason for hiding this comment

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

@hazzik Ok. But could you please clarify what boxing do you expect to see? I've just checked with Resharper IL viewer and don't see any difference in initializing both IList<EntityProjection> or List<EntityProjection>. IList<EntityProjection> is directly implemented by List<EntityProjection> - no (co/contr)-variant conversion needed.

And from what I see I can directly assign IEnumerable<object> objs = new List<EntityProjection>(); without any penalties with implicit hidden conversions.

Copy link
Member

Choose a reason for hiding this comment

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

The enumerator of List is declared as struct and exposed as bare type. Both IEnumerable<>.GeEnumerator and IEnumerable.GetEnumerator implemented as explicit and there will be boxing of enumerator when it is accessed from IEnumerable.GetEnumerator

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

@hazzik Interesting! Thanks. So it's about the price of single boxing per each enumeration via foreach. But that means that I also need to expose direct List<> class in GetEntityProjections() method and InitProjection method should also expect direct List<> class:

protected void InitProjection(..., *List<EntityProjection> entityProjections*)

Did I get it right? Are you sure you want me to go this way (except field type changing - it's done already)? Considering that there is no enumeration via foreach/LINQ in current implementation.

Also it seems it helps to avoid boxing only if foreach is used on list. It's still boxed for any LINQ calls (which is logical but I was hoping to see some CLR magic :) ):
https://dotnetfiddle.net/voVECl

Copy link
Member

Choose a reason for hiding this comment

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

No, just field.

@@ -45,6 +46,7 @@ public class CriteriaQueryTranslator : ICriteriaQuery
private readonly ICollection<IParameterSpecification> collectedParameterSpecifications;
private readonly ICollection<NamedParameter> namedParameters;
private readonly ISet<string> subQuerySpaces = new HashSet<string>();
public IList<EntityProjection> entityProjections = new List<EntityProjection>();
Copy link
Member

Choose a reason for hiding this comment

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

Forgotten trash? Public field and not used.

Copy link
Member

@fredericDelaporte fredericDelaporte left a comment

Choose a reason for hiding this comment

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

The SetField logic looks a bit brittle to me, I am going to push a commit with my proposed changes.


IType[] IProjection.GetTypes(string alias, ICriteria criteria, ICriteriaQuery criteriaQuery)
{
return new[] { criteriaQuery.GetType(criteria, alias) };
Copy link
Member

Choose a reason for hiding this comment

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

I am only seeing this now although it is here from the beginning (and maybe it was here in previous PR too): when looking at how this function is implemented and used, it does not look to me as an expected implementation.
Inferring from its usage in CriteriaQueryTranslator.GetTypeUsingProjection and its implementation in other classes like AliasedProjection, I would expect more something like:

SetFields(criteria, criteriaQuery);
return _entityAlias == alias
	? _types
	: null;

Copy link
Member Author

Choose a reason for hiding this comment

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

@fredericDelaporte Yes. It's something that was copy-pasted from initial implementation. Actually after looking into it I think it should always return null. We reuse alias that's already present in query and it's already associated with an entity - I think we shouldn't interfere here and let query to resolve alias and handle it.
I was trying to come up with some examples where your implementation could be useful and failed.

P.S. I also realized that GetColumnAliases(string alias,.. is also wrong - it's simply provides entity projection aliases for any supplied propertyPath. I think it also should always return null.

Copy link
Member

Choose a reason for hiding this comment

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

Actually after looking into it I think it should always return null.

Agreed.

I also realized that GetColumnAliases(string alias,.. is also wrong

Good catch. It should be aligned with GetTypes(string alias, ... implementation.

private void SetFields(ICriteria criteria, ICriteriaQuery criteriaQuery)
{
//Persister is required, so let's use it as "initialized marker"
if (Persister != null)
Copy link
Member

Choose a reason for hiding this comment

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

This lazy SetFields may cause issues, because it depends on receiving the root criteria on its first call for doing properly its job, while it could receive a sub-criteria instead. criteria.Alias will then not be the correct alias for initializing _entityAlias. (Other usages have their implementation delegated to the root criteria.)

I think it should rely on criteriaQuery only, by adding another member to ISupportEntityProjectionCriteriaQuery for getting the root criteria.

Copy link
Member Author

Choose a reason for hiding this comment

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

@fredericDelaporte Are you sure? From what I see Subcriteria.SetProjection use rootCriteria. Or did you mean some other scenario?

Copy link
Member

Choose a reason for hiding this comment

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

Many methods of EntityProjection are calling SetFields, and these many methods can each be called with something else than the root criteria. Reading from the code, I am unable to ascertain the first SetFields call would forcibly be done with the root criteria.
Can you tell me if you are sure its first call can only be done with the root criteria, and why are you sure of this?

I do not get by the way why you refer to Subcriteria.SetProjection. Yes it delegates its implementation to the root criteria. But here I am pointing to criteria.Alias, which does 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.

My understanding is the following: from one side projections are assigned to some criterias. And from the other side projections methods should be called in context of assigned criteria (what the point of calling it for anything else?).
So it's more logical to me to use exact provided criteria for fields initialization.

while it could receive a sub-criteria instead
I do not get by the way why you refer to Subcriteria.SetProjection

And so if subcriteria projections are delegated to root criteria - I expect projections methods to be called in context of root criteria.

All 3 methods (ToSqlString, GetTypes, GetColumnAliases) entry points where SetFields is currently called are executed for root criteria:

public SqlString GetSelect()
{
return rootCriteria.Projection.ToSqlString(rootCriteria.ProjectionCriteria, 0, this);
}
internal IType ResultType(ICriteria criteria)
{
return TypeFactory.ManyToOne(GetEntityName(criteria));
//return Factory.getTypeResolver().getTypeFactory().manyToOne(getEntityName(criteria));
}
public IType[] ProjectedTypes
{
get { return rootCriteria.Projection.GetTypes(rootCriteria, this); }
}
public string[] ProjectedColumnAliases
{
get { return rootCriteria.Projection.GetColumnAliases(0, rootCriteria, this); }
}

Maybe I'm wrong but it looks like unnecessary complication with additional interface extension.

Copy link
Member

Choose a reason for hiding this comment

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

All this entry points are public and may be called by code outside of NHibernate, so even if it were true they were always called with rootCriteria, there is still no guarantee about it. So if we agree alias must be taken from the root criteria when initializing a projection for root entity (for consistency with GetRootEntityTypeIfAvailable by the way, which always takes it from root criteria), then we should ensure that we will forcibly take it from the root criteria, even if something else than the root criteria is used to call one of these entry points.

Moreover, in current NHibernate code base, IProjection.GetTypes is also called from InExpression.ToSqlString (which is from ICriterion, not from IProjection), which can be called with criteria that do not look to be the root one. Same for IProjection.ToSqlString, called from IdentifierEqExpression.ToSqlString. Granted, it is unlikely a valid query could be done we those expressions calling an entity projection. But still, better be safe in my opinion.

The rootCriteria.ProjectionCriteria sent in IProjection.ToSqlString adds also to the risk. Currently it seems always null or being equal to rootCriteria, being affected only by this. (I guess it will not be null if that call occurs by the way, otherwise we are in troubles too). But some code (the Clone) assumes it could be something else too. Maybe that is an obsolete thing, or maybe that is here for being future proof.

The contract of those entry points does not stipulate they must be called with the root criteria, so relying on this is not future proof.

}

[Test]
public void EntityProjectionLockMode()
Copy link
Member

@fredericDelaporte fredericDelaporte Dec 20, 2017

Choose a reason for hiding this comment

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

This one fails for Oracle. It generates the following query:

select id1_1_0_,name2_1_0_
    from (
        SELECT child1x1_.Id as id1_1_0_, child1x1_.Name as name2_1_0_
            FROM EntityComplex this_
            inner join EntitySimpleChild child1x1_
                on this_.Child1Id=child1x1_.Id )
    where rownum <=:p0
    for update of child1x1_.Id

It fails with the error:

ORA-00904: "CHILD1X1_"."ID" : invalid identifier

The for update clause is bad. I think this is an unrelated bug, indeed #1352.
So better add something like

if (Dialect is Oracle8iDialect)
	Assert.Ignore("Oracle is not supported due to #1352 bug (NH-3902)");

@fredericDelaporte
Copy link
Member

I have adjusted some assert messages, which were not much helpful due to being redundant with asserts in the same test.

@fredericDelaporte fredericDelaporte added this to the 5.1 milestone Dec 21, 2017
@fredericDelaporte fredericDelaporte self-assigned this Dec 21, 2017
@bahusoid
Copy link
Member Author

@fredericDelaporte Ok. So it seems all pending questions are resolved - I agree with your interface extension logic.


if (!(criteriaQuery is ISupportEntityProjectionCriteriaQuery entityProjectionQuery))
{
throw new HibernateException($"Projecting to entities requires a '{criteriaQuery.GetType().FullName}' type to implement {nameof(ISupportEntityProjectionCriteriaQuery)} interface.");
Copy link
Member

@hazzik hazzik Dec 23, 2017

Choose a reason for hiding this comment

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

Maybe it shall be ArgumentException?

hazzik
hazzik previously approved these changes Dec 23, 2017
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.

Some minor questions to exceptions, otherwise looks good.

@fredericDelaporte fredericDelaporte dismissed stale reviews from hazzik and themself via fe3aa9d December 23, 2017 12:12
Copy link
Member

@fredericDelaporte fredericDelaporte left a comment

Choose a reason for hiding this comment

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

Minor exceptions changes done.

@hazzik hazzik changed the title #948(NH-3435) - Ability to select entities in Criteria projections (new version) Ability to select entities in Criteria projections Dec 23, 2017
@hazzik hazzik merged commit f2969d0 into nhibernate:master Dec 23, 2017
@bahusoid bahusoid mentioned this pull request Mar 11, 2018
@fredericDelaporte
Copy link
Member

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.

(Yeah, copy/paste from #1545, but here it is a different new criteria/queryover feature.)

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.

3 participants