Skip to content

Conversation

leonschenk
Copy link
Contributor

@leonschenk leonschenk commented Nov 16, 2024

I have come to the conclusion that this must be a bug, because the same query is executed but the effect is different. Here is a testcase that will succeed when fixed.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


https://hibernate.atlassian.net/browse/HHH-18855

@leonschenk leonschenk changed the title HHH-18855: Subselect fetches not generat HHH-18855: Subselect fetches not always generated in the same situation Nov 16, 2024
@hibernate-github-bot
Copy link

hibernate-github-bot bot commented Nov 16, 2024

Thanks for your pull request!

This pull request appears to follow the contribution rules.

› This message was automatically generated.

@leonschenk leonschenk marked this pull request as ready for review November 16, 2024 21:18
Copy link
Member

@mbellade mbellade left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! This is a great first pass, left a few comments.

this.restrictivePart = restrictivePart;
this.lockOptions = lockOptions.makeCopy();
this.jdbcParameters = jdbcParameters;
this.sqlAst = sqlAst;
Copy link
Member

Choose a reason for hiding this comment

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

Storing the whole SQL AST tree here means having to keep a complex structure around for every single-id load plan, which means for every mapped entity type. It would be best to avoid this, especially seems the SubselectFetch#createRegistrationHandler method apparently doesn't use it anymore apart from a specific case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see what you mean here. Each SingleIdLoadPlan is stored in some sort of cache, it doesn't make much sense to keep around the queryast as well.

Comment on lines +135 to +140
final SubselectFetch.RegistrationHandler subselectRegistrationHandler = SubselectFetch.createRegistrationHandler(
session.getPersistenceContextInternal().getBatchFetchQueue(),
sqlAst,
jdbcParameters,
jdbcParameterBindings
);
Copy link
Member

Choose a reason for hiding this comment

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

It would be great to avoid having to pass the sqlAst here. If you look at the SubselectFetch.createRegistrationHandler implementation, you can see it's used for 2 things:

  1. checking whether the query has at least 1 root
  2. passing the first TableGroup to the StandardRegistrationHandler constructor

Regarding point 2., the constructor parameter is never actually used - so we can deprecate this constructor and create one without the TableGroup parameter.

As for point 1., in SingleIdLoadPlan we always know the generated query has at least one root (we can be extra sure by adding an assert !sqlAst.getQuerySpec().getFromClause().getRoots().isEmpty() in the constructor) - so we don't really need to pass it for this check. I suggest creating a third signature of the createRegistrationHandler with the same parameters but no TableGroup, and deprecate the one with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mbladel You might have overlooked something, because as far as I can see the QuerySpec (from sqlAst) is used for the generation of the SubselectFetch query. The original query is copied into the subselect query 'in (... original query ... )'.

It might be possible to generate de subselection query by other means (ie recreate the original queryspec while subselecting)? Otherwise I do not think we can do without at least the original queryspec. What is your opinion on this?

Copy link
Member

Choose a reason for hiding this comment

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

You're right, and e.g. EntityBatchLoaderArrayParam already stores the SQL AST to use in subselect registration handlers, so we might do the same here.

Comment on lines 36 to 40
@Jpa(annotatedClasses = {
GenerateSubselectsOnJoinedAssociationTest.LazySelectRoot.class,
GenerateSubselectsOnJoinedAssociationTest.LazySelectNode.class
})
Copy link
Member

Choose a reason for hiding this comment

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

Small note, but if you want you can use @DomainModel instead of @Jpa, together with the @SessionFactory annotation they will allow your test methods to accept a SessionFactoryScope so you won't need to call unwrap.

Comment on lines 89 to 93
for ( final LazySelectNode node : root.nodes ) {
assertThat( Hibernate.isInitialized( ( (LazySelectNode) Hibernate.unproxy( node ) ).children ),
is( false )
);
}
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to also test if the registered subselect fetches actually work when trying to initialize the children collections, and make some assertions on their contents as well.

@leonschenk
Copy link
Contributor Author

@mbladel Thank you for your review. I will do the required work on the points that you mentioned.

@mbellade
Copy link
Member

mbellade commented Dec 2, 2024

@leonschenk could you please also test if FetchMode.SUBSELECT with FetchType.EAGER associations also works as expected for find (same as with queries)? Thanks.

@leonschenk
Copy link
Contributor Author

@leonschenk could you please also test if FetchMode.SUBSELECT with FetchType.EAGER associations also works as expected for find (same as with queries)? Thanks.

There is indeed a problem here. I used the following setup to enable lazy vs eager fetching:
@FetchProfileOverride(profile = "lazysubselect", fetch = FetchType.LAZY, mode = FetchMode.SUBSELECT)
@FetchProfileOverride(profile = "eagersubselect", fetch = FetchType.EAGER, mode = FetchMode.SUBSELECT)

However, the sqlAst is generated at setup-time where no fetchprofile was active. This means lazy fetchtype was selected, bacause it is the default for manyToMany. The 'cached' sqlAst is used because isLoadPlanReusable returns true (fetchprofile is not registered on the EntityPersister, but it should be because it affects the loadplan actually). Though the described problem has not much to do with the original problem, and may be a succesive ticket.

@mbellade
Copy link
Member

mbellade commented Dec 3, 2024

I believe the "problem" you mention is not really an issue, it's more of an improvement: if FetchMode.SUBSELECT was never considered for find, then this changes the default behavior - and while for FetchType.LAZY I agree it would make sense, it would be quite different for eager-fetching. Also, I believe if we want to go ahead with this, eager and lazy fetching behaviors should be aligned for consistency - just as they already are for queries.

Could you please try just mapping the association as FetchType.EAGER instead of using @FetchProfileOverride? Maybe it doesn't make a difference, just to fully understand this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants