-
Notifications
You must be signed in to change notification settings - Fork 933
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
Conversation
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()) |
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.
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 |
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.
pRoot
? I guess the p
is a typo. Otherwise for what does it stand for?
|
||
namespace NHibernate.Criterion | ||
{ | ||
using TThis = EntityProjection; |
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 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 |
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.
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
.
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.
In fact we favor putting tests in their functional area, so it would be even better to move them to Criteria/ProjectionsTest.cs.
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 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; |
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.
Naming inconsistency. I would rather have it named SetLazyPropertiesFetch
.
using TThis = EntityProjection; | ||
|
||
[Serializable] | ||
public class EntityProjection : IProjection |
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 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(); |
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.
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.
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.
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.
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.
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 |
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.
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); |
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 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.");
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.
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)
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 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(); |
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.
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) |
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 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.)
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.
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)
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'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.
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.
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.)
@fredericDelaporte I tried to address all the requested changes. |
I am sorry, this is not what I was asking for. I think the work should be done by adapting the I think your feature requires changes in This entity loading should happen in |
@fredericDelaporte Ok. Thanks for pointers. Will look into it... |
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. |
28639ee
to
1c61763
Compare
@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 |
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 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"); |
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.
Do not throw bare Exception
. For such case we tend to throw an InvalidOperationException
(as in MappedAs
Linq function).
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 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?
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.
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.
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.
Need to be aligned with #1498
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.
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 |
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.
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); |
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.
Forgotten trash?
|
||
SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery) | ||
{ | ||
throw new NotImplementedException(); |
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.
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(); |
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 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> |
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 looks like a string here. And this method seems to be a somewhat unrelated small feature addition.
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.
@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 :) .
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 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) |
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 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.)
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.
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)
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.
Unfortunately it will generate one method.
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.
Yep. Just checked with Resharper IL Viewer window.. Good to know..
InitStatementString(projectionString, whereString, orderByString, groupByString, havingString, lockMode); | ||
|
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.
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>(); |
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 can be readonly
.
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.
Also, please make field type of List
instead of IList
to avoid unnecessary boxing.
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.
@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.
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.
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
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.
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.
@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
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.
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>(); |
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.
Forgotten trash? Public field and not used.
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.
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) }; |
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 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;
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.
@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.
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.
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) |
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 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.
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.
@fredericDelaporte Are you sure? From what I see Subcriteria.SetProjection
use rootCriteria
. Or did you mean some other scenario?
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.
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.
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.
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:
nhibernate-core/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs
Lines 167 to 187 in c020a40
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.
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.
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() |
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 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)");
I have adjusted some assert messages, which were not much helpful due to being redundant with asserts in the same test. |
@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."); |
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.
Maybe it shall be ArgumentException
?
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.
Some minor questions to exceptions, otherwise looks good.
fe3aa9d
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.
Minor exceptions changes done.
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:
See other examples in tests.