Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.
Bug 356296 - If QueryHints.BATCH is set, all join conditions on the path to the expression for which the hint is set are changed to inner joins
Summary: If QueryHints.BATCH is set, all join conditions on the path to the expression...
Status: RESOLVED FIXED
Alias: None
Product: z_Archived
Classification: Eclipse Foundation
Component: Eclipselink (show other bugs)
Version: unspecified   Edit
Hardware: PC Windows XP
: P2 major with 1 vote (vote)
Target Milestone: ---   Edit
Assignee: James Sutherland CLA
QA Contact:
URL:
Whiteboard: submitted_patch
Keywords:
Depends on:
Blocks:
 
Reported: 2011-08-31 05:29 EDT by Martin Marinschek CLA
Modified: 2022-06-09 10:10 EDT (History)
4 users (show)

See Also:


Attachments
patch (25.54 KB, patch)
2013-08-27 14:46 EDT, James Sutherland CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Marinschek CLA 2011-08-31 05:29:50 EDT
Build Identifier: 2.2.1

First let me say that I love the QueryHints system - it is really useful. However, in applying QueryHints.BATCH, I am running into a problem.

See QueryHintsHandler.BatchHint#applyToDatabaseQuery: an expression in the path is resolved by calling: expression = expression.get(token);

This get is named problematically, as it has side-effects (a sidenote - it is my strong believe that method prefixed with a get should never have side-effects): it changes the expression to be an inner-join, independent of what has been set before.

See the following code in ObjectExpression:

    public Expression get(String attributeName, Vector arguments) {
        Expression operatorExpression = super.get(attributeName, arguments);
        if (operatorExpression != null) {
            return operatorExpression;
        }

        QueryKeyExpression result = derivedExpressionNamed(attributeName);
        result.doNotUseOuterJoin();
        return result;

    }

IMHO, adding a batch hint should not change the query in any other way, especially not changing outer joins (for which I also explicitly set a QueryHint) to inner joins.

An example where this was used:

query string:

"select wl from WorklistEntry wl left join fetch wl.owner left join fetch wl.modifierEmployee left join wl.approval app left join app.approver apprv left join app.roleRequest rr left join app.functionGroupRequest fgr"

and the hints:

        query.setHint(QueryHints.BATCH_TYPE, "IN");
        query.setHint(QueryHints.LEFT_FETCH, "wl.owner");
        query.setHint(QueryHints.LEFT_FETCH, "wl.modifierEmployee");
        query.setHint(QueryHints.LEFT_FETCH, "wl.approval");
        query.setHint(QueryHints.LEFT_FETCH, "wl.approval.roleRequest");
        query.setHint(QueryHints.LEFT_FETCH, "wl.approval.functionGroupRequest");
        query.setHint(QueryHints.LEFT_FETCH, "wl.approval.approver");
        query.setHint(QueryHints.BATCH, "wl.approval.roleRequest.countries");

best regards,

Martin

Reproducible: Always
Comment 1 Martin Marinschek CLA 2011-08-31 05:37:24 EDT
It might be that the issue is really the code in ObjectExpression - there are two methods there:

    public Expression get(String attributeName, Vector arguments) {
        Expression operatorExpression = super.get(attributeName, arguments);
        if (operatorExpression != null) {
            return operatorExpression;
        }

        QueryKeyExpression result = derivedExpressionNamed(attributeName);
        result.doNotUseOuterJoin();
        return result;

    }

    public Expression getAllowingNull(String attributeName, Vector arguments) {
        ObjectExpression exp = existingDerivedExpressionNamed(attributeName);

        // The same (aliased) table cannot participate in a normal join and an outer join.
        // To help enforce this, if the node already exists 
        if (exp != null) {
            return exp;
        }
        exp = derivedExpressionNamed(attributeName);
        exp.doUseOuterJoin();
        return exp;

    }

Where getAllowingNull() retrieves the expression and only if it is not existing sets the outer join use, but get() retrieves the expression in the first line (which is always null as super.get() returns null), and then always sets exp.doNotUseOuterJoin();.

My suggested patch would hence be to change:

        Expression operatorExpression = super.get(attributeName, arguments);

to:

        ObjectExpression exp = existingDerivedExpressionNamed(attributeName);

best regards,

Martin
Comment 2 Tom Ware CLA 2011-09-22 11:36:18 EDT
Setting target and priority.  See the following page for the meanings of these fields:

http://wiki.eclipse.org/EclipseLink/Development/Bugs/Guidelines

Community: Please vote for this bug if it is important to you.  Votes are one of the main criteria we use to determine which bugs to fix next.
Comment 3 Tom Ware CLA 2011-09-28 15:28:54 EDT
reviewing the suggestion
Comment 4 Tom Ware CLA 2011-10-03 10:13:44 EDT
When does the problem occur?  'At the first run of the query, or when the batch is actually triggered?

What happens if you do something like this:

        query.setHint(QueryHints.BATCH, "rr.countries");
Comment 5 Tom Ware CLA 2011-10-03 10:35:17 EDT
'looks like the following probably will not work:

query.setHint(QueryHints.BATCH, "rr.countries");

I think the solution to this issue is to provide a facility like the one suggested above. (i.e. a way to indicate that the batch should use an outer join)

Changing the expression code as suggested above will change the fundamentals of how expressions work and make it so certain expression no longer function as they should.
Comment 6 Martin Marinschek CLA 2011-10-06 03:20:07 EDT
@rr.countries: no, that would not work, Eclipselink wouldn't permit it.

@your argument about expressions: this might change the way expressions work, but my hunch would be they are working wrongly in the moment. I have already asked you guys to explain to me why this code is there, I just don't understand what the explanation for this is.

In my humble opinion, you are arguing in a circular way: this code can't be wrong because it was always there (and would fundamentally change the way things worked). Maybe things were fundamentally wrong? 

If you are worried about backwards compatibility, just think about adding a parameter which will change this behaviour.

best regards,

Martin
Comment 7 Tom Ware CLA 2011-10-06 08:24:41 EDT
Can you please answer this question from the above and perhaps give some pseudocode and output as an example:

"When does the problem occur?  'At the first run of the query, or when the batch
is actually triggered?"

The test of whether the expression framework needs to be fixed or the parsing framework is whether it is possbile to build a query based on the Expression framework that acheives what you want.
Comment 8 Martin Marinschek CLA 2011-10-06 09:18:43 EDT
Hi Tom,

I hope I have understood your question right: this happens in the base query - not in the query which executes the batch fetching. I see the inner joins in the SQL which is printed by the session log.

best regards,

Martin
Comment 9 Tom Ware CLA 2011-10-07 10:07:46 EDT
Can you please help me reproduce this.  I have the following:

A OneToMany -> B
B OneToOne -> C

I am running the following query:

        query = em.createQuery("select a from A a");
        query.setHint(QueryHints.LEFT_FETCH, "a.bs");
        query.setHint(QueryHints.LEFT_FETCH, "a.bs.c");
        query.setHint(QueryHints.BATCH_TYPE, "IN");
        query.setHint(QueryHints.BATCH, "a.bs.c");

When I run that query, all my joins are outer joins.

What should I change/add?
Comment 10 Tom Ware CLA 2011-10-07 10:49:23 EDT
I am still not quite sure if I reproduced the problem but the following:

        query = em.createQuery("select a from A a left join a.bs b");
        query.setHint(QueryHints.LEFT_FETCH, "a.bs");
        query.setHint(QueryHints.LEFT_FETCH, "a.bs.c");
        query.setHint(QueryHints.BATCH_TYPE, "IN");
        query.setHint(QueryHints.BATCH, "a.bs.c");

Yields a query that in addition to the left joins contains an inner join from a to b.

If that this the problem, the following Expression-built query does not show the same problem:

        ReadAllQuery raq = new ReadAllQuery(Customer.class);
        ExpressionBuilder custBuilder = raq.getExpressionBuilder();
        Expression ordersExp = custBuilder.anyOfAllowingNone("orders");
        Expression itemExp = ordersExp.get("item");
        raq.addJoinedAttribute(ordersExp);
        raq.addJoinedAttribute(itemExp);
        raq.addBatchReadAttribute(itemExp);

If the above is a demonstration of the bug, then having EclipseLink build similar query to the ReadAllQuery here for that query would solve the problem.

There are two ways we could do that.

1. allowing the use of the alias "b" from the initial query in the batch and fetch hints

2. allowing some kind of outer join syntax in the hints
Comment 11 Tom Ware CLA 2011-10-07 10:53:16 EDT
Here is the query adjusted for my A-B-C example

        ReadAllQuery raq = new ReadAllQuery(A.class);
        ExpressionBuilder aBuilder = raq.getExpressionBuilder();
        Expression bsExp = aBuilder.anyOfAllowingNone("bs");
        Expression cExp = bsExp.get("c");
        raq.addJoinedAttribute(bsExp);
        raq.addJoinedAttribute(cExp);
        raq.addBatchReadAttribute(cExp);
Comment 12 Martin Marinschek CLA 2011-10-07 22:48:38 EDT
Hi Tom,

glad that you could reproduce some of the issue, I would have invested some time  to give you a testcase soon, if not. 

From the setup: we only have many-to-one's and one-to-one's in the base query (we need to keep the SQL count intact, to make paging and sorting in the database available). For the one-to-many's (and those can be in a level arbitrarily deep and still need to be fetched), we want to use the batch query hint facility.

I agree your options are possible solutions, where I am still not sure that option 1) won't run into the same problem (you also need to retrieve the expression than somehow, even if you use the alias, right?). But if you make this available without having to call get() on ObjectExpression, everything is fine.

You will in any case always run into this problem if the get() Method on ObjectExpression is called - see the sourcecode I posted in my first comment.

But - could you please explain to me why this line of code (doNotUseOuterJoin()) is valid ;) ? I still see no reason (from an SQL perspective) why this should be there. I promise I will immediately shut up bugging you with this if I have understood the reason ;)

best regards,

Martin
Comment 13 Tom Ware CLA 2011-10-11 09:57:34 EDT
One key thing here is that there is a translation lair between the JPQL string and the expression code.  One thing that translation lair does is keep an index of the various aliased expressions and reuses those expressions when the aliases are used.

So.. for instance in:

select e from Employee e left join e.projects p

In the translation lair, there will be a map that contains:

e, (Expression Builder for Employee)
p, (Expression Builder for Employee).getAllowingNull("projects")

Any reference to p will use that map to get the proper expression for p.

Note: the code for this is in our JPQL parsing framework, not in our expression framework (i.e. it is not in the get() or getAllowingNull() method - those look-ups are used in a different context)  The org.eclipse.persistence.internal.parsing.jpql package contains the JPQL parsing code.

The get() method has a call doNotUseOuterJoin() because the callers of the get method specifically have chosen not to do an outer join.  They would have called getAllowingNull() if they want an outer join.
Comment 14 James Sutherland CLA 2013-08-27 14:46:31 EDT
Created attachment 234820 [details]
patch
Comment 15 James Sutherland CLA 2013-08-27 14:52:26 EDT
Git main push: bug#356296 batch fetch forces inner joins

Fixes batch fetch hint to not convert outer joins to inner joins.

Changes:
- Added Query hint "eclipselink.query-results-cache.invalidate-on-change" to allow disable of query cache invalidation.
- Expression: added get() API to allow not forcing inner join.
- IdentityMapManager: Fixed query cache invalidation to avoid memory leak in queryResultsInvalidationsByClass by using Set instead of List.
- ForeignReferenceMapping: Changed join fetch to overwrite batch fetch two avoid both being done.
Comment 16 James Sutherland CLA 2013-08-29 11:53:32 EDT
fixed in 2.6
Comment 17 Eclipse Webmaster CLA 2022-06-09 10:05:53 EDT
The Eclipselink project has moved to Github: https://github.com/eclipse-ee4j/eclipselink
Comment 18 Eclipse Webmaster CLA 2022-06-09 10:10:03 EDT
The Eclipselink project has moved to Github: https://github.com/eclipse-ee4j/eclipselink