Skip to content

Support to join not associated entities in Criteria (aka Entity Join) #1545

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 8 commits into from
Jan 30, 2018

Conversation

bahusoid
Copy link
Member

@bahusoid bahusoid commented Jan 20, 2018

With this PR it's possible to join entities with not mapped association in QueryOver via JoinEntityQueryOver/JoinEntityAlias methods:

Entity root = null;
NotRelatedEntity notRelatedEntity = null;

root = session.QueryOver(() => root)
//Join on not mapped entity
.JoinEntityQueryOver(() => notRelatedEntity, () => notRelatedEntity.Id == root.NotRelatedEntityId)
//Also preload child for NotRelatedEntity
.JoinQueryOver(nre => nre.SomeChild)
.Take(1)
.SingleOrDefault();

//Obtain from session already preloaded notRelatedEntity along with notRelatedEntity.SomeChild
notRelatedEntity = session.Get<NotRelatedEntity>(root.NotRelatedEntityId);

See other options in tests

"Entity Join" name comes from Hibernate (where it's implemented for hql only):
http://in.relation.to/2016/02/10/hibernate-orm-510-final-release/

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.

This is just a quick review for now, I should do a more thorough one later.

@@ -280,7 +284,8 @@ IQueryOver<TRoot> IQueryOver<TRoot>.ReadOnly()
/// Implementation of the <see cref="IQueryOver&lt;TRoot, TSubType&gt;"/> interface
/// </summary>
[Serializable]
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>,
ISupportEntityJoinQueryOver<TRoot>
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 not seen the reason for implementing this interface on this class rather than on QueryOver<TRoot>. Why this choice?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe because that is the way it looks to be done for other similar cases. But then, ISupportEntityJoinQueryOver<TRoot> "6.0 todo" should be "merge in IQueryOver<TRoot, TSubType>.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I've just put it next to existing JoinAlias/JoinQueryOver methods. Looks more natural to have them in the same place.

namespace NHibernate.Criterion
{
//TODO: Make interface more flexible for changes (maybe it should take only 2 params alias + EntityJoinConfig)
public interface ISupportEntityJoinQueryOver
Copy link
Member

Choose a reason for hiding this comment

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

I would add a " 6.0 TODO: merge with IQueryOver". Is there a reason for not aiming at this? The same apply to below interface, but of course with IQueryOver<TRoot>.


namespace NHibernate.Impl
{
public interface ISupportEntityJoinCriteria
Copy link
Member

Choose a reason for hiding this comment

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

I would add a " 6.0 TODO: merge with ICriteria". Is there a reason for not aiming at this?

public static class EntityJoinExtensions
{
// 6.0 TODO: merge into 'IQueryOver<TType, TSubType>
public static TThis JoinEntityAlias<TThis, TAlias>(this TThis queryOver, Expression<Func<TAlias>> alias, JoinType joinType, ICriterion withClause, string entityName = null) where TThis : IQueryOver
Copy link
Member

Choose a reason for hiding this comment

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

So that is somewhat the todo I am asking for on interface.

{
last = entityJoinInfo.Persister;
lastEntity = (IPropertyMapping) last;
++i;
Copy link
Member

Choose a reason for hiding this comment

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

i++; is more common place, and we do not need pre-increment here.

Copy link
Member

Choose a reason for hiding this comment

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

Just to add to this: if preferring ++i; other i++; for "performance", this is an old performance "trick", obsolete for C, potentially still relevant for C++ when applied to an object but well, we are using C#. See here. Optimizing such trivial things is not a developer job but a compiler job. I am on the side on optimizing the developer work by letting compiler responsible of such optimizations, and letting developer write most readable code possible. This includes using most common places syntaxes unless there is a real and significant impact for the application.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, of course It's not about optimization :) it's about habit - I just use ++i; since C++ and it's more common for me.
I have no objections to change it but curious why do you think that i++ is more common for C#? Is it because VS/Resharper expands for with i++?

Copy link
Member Author

Choose a reason for hiding this comment

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

P.S. I don't have much time this week but I will try to address all the questions/concerns next week.

Copy link
Member

Choose a reason for hiding this comment

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

Is it because VS/Resharper expands for with i++?

No. Just because I have far more seen post-increments than pre-increments in C#, and I do not think I am alone. Alexander had made the same demand, switching away from ++i to i++ on a previous PR. The opener argued for performance as far a I remember, and it was discarded as not relevant in C#.

Copy link
Member

Choose a reason for hiding this comment

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

This was here.

@@ -71,7 +72,7 @@ public CriteriaImpl(string entityOrClassName, string alias, ISessionImplementor
rootAlias = alias;
subcriteriaByAlias[alias] = this;
}

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

@@ -280,7 +284,8 @@ IQueryOver<TRoot> IQueryOver<TRoot>.ReadOnly()
/// Implementation of the <see cref="IQueryOver&lt;TRoot, TSubType&gt;"/> interface
/// </summary>
[Serializable]
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>,
ISupportEntityJoinQueryOver<TRoot>
Copy link
Member

Choose a reason for hiding this comment

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

Maybe because that is the way it looks to be done for other similar cases. But then, ISupportEntityJoinQueryOver<TRoot> "6.0 todo" should be "merge in IQueryOver<TRoot, TSubType>.

@@ -668,6 +675,16 @@ internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string al
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, JoinType joinType)
: this(root, parent, path, null, joinType) { }

/// <summary>
/// Entity name for "Entity Join" - join for enitity with not mapped association
Copy link
Member

Choose a reason for hiding this comment

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

Typo, enitity.

var tableAlias = translator.GetSQLAlias(entityJoinInfo.Criteria);
var criteriaPath = entityJoinInfo.Criteria.Alias; //path for entity join is equal to alias
var persister
= entityJoinInfo.Persister as IOuterJoinLoadable;
Copy link
Member

Choose a reason for hiding this comment

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

A single line here should be enough. (And it is not an usual formatting to have the affectation operator on the next line instead of at the end of line.)

{
last = entityJoinInfo.Persister;
lastEntity = (IPropertyMapping) last;
++i;
Copy link
Member

Choose a reason for hiding this comment

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

Just to add to this: if preferring ++i; other i++; for "performance", this is an old performance "trick", obsolete for C, potentially still relevant for C++ when applied to an object but well, we are using C#. See here. Optimizing such trivial things is not a developer job but a compiler job. I am on the side on optimizing the developer work by letting compiler responsible of such optimizations, and letting developer write most readable code possible. This includes using most common places syntaxes unless there is a real and significant impact for the application.

{
on = on.Count == 0
? on.Append(withClause)
: on.Append(" and ( ").Append(withClause).Append(" )");
Copy link
Member

Choose a reason for hiding this comment

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

For me, that is an undue change. It looks to me the way it is generated, on is either empty or starts with " and ". Testing on Count here changes behavior. Better not do that and keep previous code.

if (fkColumns.Length == 0)
{
_fromFragment.Add(on);
return;
Copy link
Member

Choose a reason for hiding this comment

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

Here, an additional adjustment should be done. on is usually generated with a leading " and ", which should be removed before adding to _fromFragment.

@@ -62,7 +62,7 @@ protected bool AddCondition(SqlStringBuilder buffer, SqlString on)
{
if (SqlStringHelper.IsNotEmpty(on))
{
if (!on.StartsWithCaseInsensitive(" and"))
if (buffer.Count > 0 && !on.StartsWithCaseInsensitive(" and"))
Copy link
Member

Choose a reason for hiding this comment

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

I doubt this change is legit. Was it causing a failure not having this change done? It looks to me code expects condition clauses to always potentially starts with an " and", removing it if needed.

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 intention here is the same as in ANSIJoinFragment.AddJoin - avoid bare withClause to be added with " and" prefix to SQL. But yeah - maybe I should handle it in each dialect explicitly.

Copy link
Member Author

@bahusoid bahusoid Jan 25, 2018

Choose a reason for hiding this comment

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

You are right - I've removed this change. I've struggled with this " and" prefix and thought this change is required for Oracle (but I was wrong)

@bahusoid
Copy link
Member Author

bahusoid commented Jan 25, 2018

@fredericDelaporte It seems I've addressed all your requests.

I also checked how to make Fetch work with entity joins. And I think I found a bug. Interface for Fetch is defined as:

IQueryOverFetchBuilder<TRoot,TSubType> Fetch(Expression<Func<TRoot, object>> path);

Expression<Func<TRoot, object>> path - type for root criteria is provided (which is TRoot) in expression parameter . But in implementation fetch mode is applied to UnderlyingCriteria(which is TSubType based) instead of RootCriteria:

this.root.UnderlyingCriteria.SetFetchMode(path, FetchMode.Eager);

So basically fetch doesn't work after JoinQueryOver call as property name from root entity is provided to current query over entity.

I think it's actually an "interface bug" - as this TRoot restriction is artificial and not really necessary. So it really should be:

IQueryOverFetchBuilder<TRoot,TSubType> Fetch(Expression<Func<**TSubType**, object>> path);

Any ideas how to resolve it properly? Maybe in 5.x we should made a more straightforward fix - use RootCriteria in fetch builder and in 6.x change interface to use Expression<Func<TSubType, object>> path.

@fredericDelaporte
Copy link
Member

I also checked how to make Fetch work with entity joins. And I think I found a bug.

I think it will need to be addressed as another PR, that is why I approve this one as is.

Any ideas how to resolve it properly? Maybe in 5.x we should made a more straightforward fix - use RootCriteria in fetch builder and in 6.x change interface to use Expression<Func<TSubType, object>> path.

Well, as usual, for a minor, we cannot change anything in an interface. Does it work to define a suitable extension, that would handle only NHibernate QueryOver internal implementation, while obsoleting the interface method? That should be the preferred way, with some "6.0 TODO" for merging it back into the interface.

@fredericDelaporte fredericDelaporte self-assigned this Jan 27, 2018
@fredericDelaporte fredericDelaporte added this to the 5.1 milestone Jan 27, 2018
@hazzik
Copy link
Member

hazzik commented Jan 27, 2018

It seems this PR has fixed another issue btw.

@bahusoid
Copy link
Member Author

I've made last small adjustments:

  1. Fixed exception message. Don't know how nameof(T) got there and why it's even allowed :)
  2. Added one more extension to Criteria. Just to make things even for CreateEntityCriteria/CreateEntityAlias

@fredericDelaporte
Copy link
Member

It seems this PR has fixed another issue btw.

Which is?

@fredericDelaporte fredericDelaporte removed their assignment Jan 28, 2018
@hazzik
Copy link
Member

hazzik commented Jan 28, 2018

I was not near the PC when wrote this. It should be something along NH-3506 (#1366) and NH-2029

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Jan 29, 2018

I have cherry-picked tests for #1366 (NH-3506) from #268, and tested them on this PR and on its base commit: no changes, fails the same way, with the same resulting queries (catched inside SQL-Profiler). So it does not look like this PR is fixing anything additional. (NH-2029 was already fixed in 5.0.)

@fredericDelaporte fredericDelaporte self-assigned this Jan 29, 2018
@fredericDelaporte
Copy link
Member

So I intend to merge this soon.

@fredericDelaporte fredericDelaporte merged commit 909fac2 into nhibernate:master Jan 30, 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.

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