jdaugherty commented on code in PR #16573:
URL: https://github.com/apache/grails-core/pull/16573#discussion_r4413143669
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -306,20 +305,11 @@ private Predicate handleAssociationCriteria(
JpaQueryContext fromsByProvider,
DetachedAssociationCriteria<?> associationCriteria) {
String associationName = associationCriteria.getAssociationPath();
- From<?, ?> associationRoot = fromsByProvider.getFrom(associationName);
- if (associationRoot == null) {
- // Check if we already have it in our parent or alias map
- Expression<?> expr =
fromsByProvider.getFullyQualifiedExpression(associationName);
- if (expr instanceof From<?, ?> from) {
- associationRoot = from;
- } else {
- associationRoot =
fromsByProvider.getRoot().join(associationName);
- fromsByProvider.addFrom(associationName, associationRoot);
- }
- }
+ From<?, ?> associationRoot =
fromsByProvider.resolveAssociation(associationName);
// Create a nested context for this association
- JpaQueryContext nestedContext = new JpaQueryContext(fromsByProvider,
null, associationRoot);
+ JpaQueryContext nestedContext = new
QueryAliasRegistrar(associationCriteria, null)
+ .createContext(fromsByProvider, associationRoot);
Review Comment:
`createAlias` inside a junction makes the junction always true. GORM core's
`createAlias` adds the alias's `DetachedAssociationCriteria` through `add()`,
so inside `or { }` it lands in the disjunction (the `HibernateAlias` criterion
goes to the top-level list). That association criteria has no criteria of its
own, so the `return` two lines below yields `criteriaBuilder.and()` with no
predicates, a `true` that short-circuits the `or`.
With `Team { String name; Like favorite }` (`favorite` nullable), five teams
of which three have a favorite:
```groovy
Team.withCriteria {
or {
createAlias('favorite', 'f', 1)
isNull('f.id')
eq('f.label', 'likeB')
}
}
```
| | result |
|---|---|
| Hibernate 5 (7.0.x) | the four expected teams: two without a favorite, two
whose favorite is `likeB` |
| 8.0.x (87fdc9f3f8) | three teams: the two with `likeB` and the one with
`likeA`; the two without a favorite are dropped |
| this head | all five teams |
Returning `null` from this method when `criteriaList` is empty, after
`createContext` so that an unused nested alias still joins, gives the Hibernate
5 result; with that change the full module suite still passes (4396 tests, 0
failures, 18 skipped). An `or { }` holding only a `likes { }` block keeps its
join either way, since `resolveAssociation` already ran above. Please add that
and a test for the `or { createAlias(..., 1); isNull(...); eq(...) }` shape,
with `count()` and `totalCount` if you like, since the reference page's own
example is this `or` block with the alias one line outside it.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java:
##########
@@ -308,49 +284,14 @@ private void assignOrderBy(CriteriaQuery<?> cq,
JpaQueryContext context) {
private boolean isCountOnly() {
List<Query.Projection> projectionList =
projections.getProjectionList();
return !projectionList.isEmpty() && projectionList.stream()
- .allMatch(projection -> projection instanceof
Query.CountProjection ||
- projection instanceof Query.CountDistinctProjection);
- }
-
- private void discoverAliases(List<Query.Criterion> criteria,
JpaQueryContext context) {
- if (criteria == null) return;
- for (Query.Criterion criterion : criteria) {
- if (criterion instanceof HibernateAlias ha) {
- // If the alias is already defined in parent and materialized,
just link it
- if (!context.hasAlias(ha.alias())) {
- context.registerAlias(ha.alias(), ha);
- }
- } else if (criterion instanceof DetachedAssociationCriteria<?>
dac) {
- if (dac.getAlias() != null) {
- context.registerAlias(dac.getAlias(), new
HibernateAlias(dac.getAssociationPath(), dac.getAlias()));
- }
- discoverAliases(dac.getCriteria(), context);
- } else if (criterion instanceof Query.PropertyNameCriterion pnc) {
- String propertyName = pnc.getProperty();
- if (propertyName.contains(".")) {
- String alias = propertyName.substring(0,
propertyName.indexOf("."));
- // Only register if not already known in this or parent
context
- if (!context.hasAlias(alias)) {
- context.registerAlias(alias, new HibernateAlias(alias,
alias));
- }
- }
- } else if (criterion instanceof Query.Junction junction) {
- discoverAliases(junction.getCriteria(), context);
- }
- }
- }
-
- private void registerDetachedJoins(JpaQueryContext context) {
- detachedCriteria.getJoinTypes().forEach((path, joinType) ->
- context.registerAlias(path, new HibernateAlias(path, path,
joinType))
- );
+ .allMatch(projection -> projection instanceof
Query.CountProjection || projection instanceof Query.CountDistinctProjection);
Review Comment:
Still an unrelated hunk: 8.0.x has this lambda on two lines (`... instanceof
Query.CountProjection ||` and `projection instanceof
Query.CountDistinctProjection);` on the next), and this joins them into one
140-character line. Restoring the two lines leaves `isCountOnly()` untouched by
the PR, which is what matrei and sbglasius asked for.
##########
grails-doc/src/en/ref/Domain Classes/createCriteria.adoc:
##########
@@ -75,10 +75,42 @@ The `sort` and `order` arguments accepted alongside the
pagination values are ch
=== Description
+Criteria queries are a type-safe, advanced way to query that uses a Groovy
builder to construct potentially complex queries. It is a much better
alternative to using a StringBuilder to dynamically construct an HQL query.
Refer to the criteria sections in the GORM for
link:{hibernate7GuideFromRef}index.html#criteria[Hibernate 7] or
link:{hibernate5GuideFromRef}index.html#criteria[Hibernate 5] guide for usage
instructions.
-Criteria queries are a type-safe, advanced way to query that uses a Groovy
builder to construct potentially complex queries. It is a much better
alternative to using a StringBuilder to dynamically construct an HQL query.
Refer to the user guide section on
link:{hibernate5GuideFromRef}index.html#criteria[Criteria] for usage
instructions.
+==== Association aliases
-Method reference:
+With Hibernate, `createAlias('association', 'alias')` creates an inner join
and lets you refer to the association as `alias.property`.
+
+In both Hibernate 5 and Hibernate 7, the three-argument overload accepts `0`
for an inner join, `1` for a left outer join, or `2` for a right outer join.
Hibernate 5 also supports `4` for a full join. Hibernate 7 does not support
full joins through this overload: any value other than `1` or `2`, including
`4`, produces an inner join.
+
+For example, to include books with no author as well as books whose author has
a name:
+
+[source,groovy]
+----
+def books = Book.createCriteria().list {
+ createAlias('author', 'a', 1) // left outer join
+ or {
+ isNull('author')
+ isNotNull('a.name')
+ }
+}
+----
+
+The alias uses the requested join type. A left join alone does not guarantee
that unmatched rows remain in the result: the restrictions must also allow the
missing association, as the `or` block does above.
+
+In Hibernate 7, every explicitly requested alias of an entity association
creates a join even when no restriction, projection, or order references it.
Declaring a left alias and an inner alias for the same association therefore
excludes rows with no associated entity, even if the inner alias is otherwise
unused.
+
+An alias declared inside an association block is relative to that association,
not the outer domain class. In Hibernate 7, aliases declared in a `list { ...
}` closure do not carry over to a subsequent `list { ... }` call on the same
criteria builder.
Review Comment:
This sentence has no qualifier while the paragraph above it has one, so it
reads as a rule for both Hibernate versions, and on Hibernate 5 it is the other
way round: the path is resolved from the domain class being queried. With the
domain above plus `Tag tag` on both `Team` and `Like`:
| query | Hibernate 5 (7.0.x) | this head |
|---|---|---|
| `likes { createAlias('tag', 't') }; eq('t.name', 'a')` | the teams whose
own `tag` is `a` and that have a like | `ConfigurationException: Cannot use
comparison criteria on non-existent property [t.name]` |
| `likes { createAlias('tag', 't'); eq('t.name', 'a') }` | `QueryException:
could not resolve property: likes_alias1` | the teams with a like tagged `a` |
So the first shape is the one thing in this area that worked on 7.x and now
fails, with a clear error rather than a different result, which I think is
fine. Please start the sentence with "In Hibernate 7," and add that Hibernate 5
resolves the alias path from the domain class being queried, so a reader
upgrading knows which query to move the `createAlias` out of the block.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]