Skip to content
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

Support binding embeddables and associations using compound or foreign keys in CTE’s #571

Closed
jwgmeligmeyling opened this issue May 17, 2018 · 5 comments · Fixed by #655
Assignees
Milestone

Comments

@jwgmeligmeyling
Copy link
Collaborator

Description

I am facing a couple of rarities with CTE's that have a @ManyToOne relationship. First of all, when I do something like this: bind("ref").select("entity.someMapping"), all columns of someMapping are bound in the CTE, even though they are unused. This can be worked around with by mapping to the id instead, like so: bind("ref").select("entity.someMapping.id"), which works, even without changing the type of the field, but has some other quirks: in the final select query, the reference will always be lazy loaded (i.e. only the id column). Specifically adding the fetch join to the criteria builder is ignored. The effective query remains the same.

Expected behavior

The expected behaviour is that the CTE only selects the required columns, and that it is possible to optionally fetch join these fields in the final result set.

Actual behavior

Either the fields have to be "fetch joined" in the CTE part already (leading to an unnecessarily complex CTE query), or it is not possible to fetch join these queries at all (adding fetch joins to a CTE root seems to be ignored).

Steps to reproduce

Workaround

A workaround is to map the id and referenced entity separately, but to a different column. Then in the CTE you bind the column through the id field, and in the final query you can fetch join the entity field.

Environment

Version:
JPA-Provider:
DBMS:
Application Server:

@beikov beikov added this to the 1.3.0 milestone Aug 15, 2018
@beikov beikov self-assigned this Aug 15, 2018
@beikov
Copy link
Member

beikov commented Aug 27, 2018

I'm not sure what this is about, but I think com.blazebit.persistence.testsuite.CTETest#testCTEAdvanced represents exactly what you are looking for. Maybe this was a problem on an older version?
Could you please elaborate or provide more details about the issue?

@jwgmeligmeyling
Copy link
Collaborator Author

I'll add a test case tomorrow, perhaps its fixed in the mean time. I saw the issue with a recursive CTE binding an ManyToOne field to a value selected from an entity using JOINED inheritance.

@jwgmeligmeyling
Copy link
Collaborator Author

jwgmeligmeyling commented Aug 27, 2018

For example:

package com.blazebit.persistence.testsuite;

import com.blazebit.persistence.CTE;
import com.blazebit.persistence.testsuite.tx.TxVoidWork;
import org.junit.Before;
import org.junit.Test;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.util.List;

public class Issue571Test extends AbstractCoreTest {


    @Override
    protected Class<?>[] getEntityClasses() {
        return new Class<?>[] { Cte.class, MyEntity.class };
    }

    @Before
    public void setUp() throws Exception {
        transactional(new TxVoidWork() {
            @Override
            public void work(EntityManager em) {
                MyEntity myEntity = new MyEntity();
                myEntity.setAttribute("attr");
                em.persist(myEntity);
            }
        });
    }


    @Test
    public void issue571Test() {
        transactional(new TxVoidWork() {
            @Override
            public void work(EntityManager em) {
                List<MyEntity> resultList = cbf.create(em, MyEntity.class)
                        .withRecursive(Cte.class)
                        .from(MyEntity.class, "a")
                        .bind("myEntity").select("a")
                        .bind("id").select("1")
                        .unionAll()
                        .from(MyEntity.class, "a")
                        .where("a.attribute").notIn().from(Cte.class, "b").select("b.myEntity.attribute").end()
                        .bind("myEntity").select("a")
                        .bind("id").select("1")
                        .end()
                        .from(Cte.class)
                        .select("myEntity")
                        .getResultList();

                System.out.println(resultList);
            }
        });
    }

    @CTE
    @Entity
    @IdClass(Cte.CteIdClass.class)
    public static class Cte {

        @Id private Long id;
        @Id @ManyToOne private MyEntity myEntity;

        public MyEntity getMyEntity() {
            return myEntity;
        }

        public void setMyEntity(MyEntity myEntity) {
            this.myEntity = myEntity;
        }

        public static class CteIdClass implements Serializable {

            Long id;
            Long myEntity;
        }

    }

    @Entity
    public static class MyEntity {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;

        private String attribute;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getAttribute() {
            return attribute;
        }

        public void setAttribute(String attribute) {
            this.attribute = attribute;
        }

    }

}

Throws:

java.lang.IllegalArgumentException: The join path [b.myEntity.attribute] has a non joinable part [myEntity.attribute]

	at com.blazebit.persistence.impl.JoinManager.getPathType(JoinManager.java:1837)
	at com.blazebit.persistence.impl.JoinManager.implicitJoin(JoinManager.java:1667)
	at com.blazebit.persistence.impl.JoinManager.implicitJoin(JoinManager.java:1365)
	at com.blazebit.persistence.impl.JoinVisitor.visit(JoinVisitor.java:88)
	at com.blazebit.persistence.impl.JoinVisitor.visit(JoinVisitor.java:80)
	at com.blazebit.persistence.parser.expression.PathExpression.accept(PathExpression.java:74)
	at com.blazebit.persistence.impl.JoinVisitor.visit(JoinVisitor.java:347)
	at com.blazebit.persistence.impl.SelectInfo.accept(SelectInfo.java:59)
	at com.blazebit.persistence.impl.SelectManager.acceptVisitor(SelectManager.java:182)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.applyImplicitJoins(AbstractCommonQueryBuilder.java:1508)
	at com.blazebit.persistence.impl.JoinVisitor.visit(JoinVisitor.java:137)
	at com.blazebit.persistence.parser.expression.SubqueryExpression.accept(SubqueryExpression.java:42)
	at com.blazebit.persistence.impl.JoinVisitor.visit(JoinVisitor.java:173)
	at com.blazebit.persistence.parser.predicate.InPredicate.accept(InPredicate.java:82)
	at com.blazebit.persistence.parser.expression.VisitorAdapter.visit(VisitorAdapter.java:194)
	at com.blazebit.persistence.parser.predicate.CompoundPredicate.accept(CompoundPredicate.java:76)
	at com.blazebit.persistence.impl.PredicateManager.acceptVisitor(PredicateManager.java:202)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.applyImplicitJoins(AbstractCommonQueryBuilder.java:1548)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.prepareAndCheck(AbstractCommonQueryBuilder.java:2178)
	at com.blazebit.persistence.impl.AbstractCTECriteriaBuilder.prepareAndCheck(AbstractCTECriteriaBuilder.java:177)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.getBaseQueryStringWithCheck(AbstractCommonQueryBuilder.java:1628)
	at com.blazebit.persistence.impl.AbstractCTECriteriaBuilder.getQuery(AbstractCTECriteriaBuilder.java:91)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.getQuery(AbstractCommonQueryBuilder.java:2066)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.getCteNodes(AbstractCommonQueryBuilder.java:1976)
	at com.blazebit.persistence.impl.AbstractCommonQueryBuilder.getTypedQuery(AbstractCommonQueryBuilder.java:1670)
	at com.blazebit.persistence.impl.AbstractQueryBuilder.getQuery(AbstractQueryBuilder.java:52)
	at com.blazebit.persistence.impl.AbstractQueryBuilder.getResultList(AbstractQueryBuilder.java:57)
	at com.blazebit.persistence.testsuite.Issue571Test$2.work(Issue571Test.java:58)
	at com.blazebit.persistence.testsuite.tx.TxSupport.transactional(TxSupport.java:33)
	at com.blazebit.persistence.testsuite.AbstractCoreTest.transactional(AbstractCoreTest.java:388)
	at com.blazebit.persistence.testsuite.Issue571Test.issue571Test(Issue571Test.java:42)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalArgumentException: Can't access property 'attribute' on basic type 'java.lang.Long'. Did you forget to add the embeddable type to your persistence.xml?
	at com.blazebit.persistence.parser.PathTargetResolvingExpressionVisitor.visit(PathTargetResolvingExpressionVisitor.java:203)
	at com.blazebit.persistence.parser.expression.PropertyExpression.accept(PropertyExpression.java:41)
	at com.blazebit.persistence.parser.PathTargetResolvingExpressionVisitor.visit(PathTargetResolvingExpressionVisitor.java:300)
	at com.blazebit.persistence.parser.expression.PathExpression.accept(PathExpression.java:74)
	at com.blazebit.persistence.impl.JpaUtils.getAttributeForJoining(JpaUtils.java:66)
	at com.blazebit.persistence.impl.JoinManager.getPathType(JoinManager.java:1835)
	... 55 more

And:

package com.blazebit.persistence.testsuite;

import com.blazebit.persistence.CTE;
import com.blazebit.persistence.testsuite.tx.TxVoidWork;
import org.junit.Before;
import org.junit.Test;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.util.List;

public class Issue571Test extends AbstractCoreTest {


    @Override
    protected Class<?>[] getEntityClasses() {
        return new Class<?>[] { Cte.class, MyEntity.class };
    }

    @Before
    public void setUp() throws Exception {
        transactional(new TxVoidWork() {
            @Override
            public void work(EntityManager em) {
                MyEntity myEntity = new MyEntity();
                myEntity.setAttribute("attr");
                em.persist(myEntity);
            }
        });
    }


    @Test
    public void issue571Test() {
        transactional(new TxVoidWork() {
            @Override
            public void work(EntityManager em) {
                List<MyEntity> resultList = cbf.create(em, MyEntity.class)
                        .withRecursive(Cte.class)
                        .from(MyEntity.class, "a")
                        .bind("myEntity").select("a")
                        .bind("id").select("1")
                        .unionAll()
                        .from(MyEntity.class, "a")
                        .where("a.attribute").eq("bogus")
                        .bind("myEntity").select("a")
                        .bind("id").select("1")
                        .end()
                        .from(Cte.class)
                        .select("myEntity")
                        .getResultList();

                System.out.println(resultList);
            }
        });
    }

    @CTE
    @Entity
    @IdClass(Cte.CteIdClass.class)
    public static class Cte {

        @Id private Long id;
        @Id @ManyToOne private MyEntity myEntity;

        public MyEntity getMyEntity() {
            return myEntity;
        }

        public void setMyEntity(MyEntity myEntity) {
            this.myEntity = myEntity;
        }

        public static class CteIdClass implements Serializable {

            Long id;
            Long myEntity;
        }

    }

    @Entity
    public static class MyEntity {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;

        private String attribute;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getAttribute() {
            return attribute;
        }

        public void setAttribute(String attribute) {
            this.attribute = attribute;
        }

    }

}

Produces:

    with recursive Issue571Test$Cte(myEntity_id, id) AS( select
        issue571te0_.id as col_0_0_,
        1 as col_1_0_,
        issue571te0_.id as id1_1_,
        issue571te0_.attribute as attribut2_1_ 
    from
        Issue571Test$MyEntity issue571te0_ 
    UNION
    ALL select
        issue571te0_.id as col_0_0_,
        1 as col_1_0_,
        issue571te0_.id as id1_1_,
        issue571te0_.attribute as attribut2_1_ 
    from
        Issue571Test$MyEntity issue571te0_ 
    where
        issue571te0_.attribute=? ) select
        issue571te1_.id as id1_1_,
        issue571te1_.attribute as attribut2_1_ 
    from
        Issue571Test$Cte issue571te0_ 
    inner join
        Issue571Test$MyEntity issue571te1_ 
            on issue571te0_.myEntity_id=issue571te1_.id

Where there are more fields projected than actually exist in the CTE.

@jwgmeligmeyling
Copy link
Collaborator Author

jwgmeligmeyling commented Aug 27, 2018

Also note the strange behaviour that occurs when you bind to the id value instead. For the latter example:

    public void issue571Test() {
        transactional(new TxVoidWork() {
            @Override
            public void work(EntityManager em) {
                List<MyEntity> resultList = cbf.create(em, MyEntity.class)
                        .withRecursive(Cte.class)
                        .from(MyEntity.class, "a")
                        .bind("myEntity").select("a.id")
                        .bind("id").select("1")
                        .unionAll()
                        .from(MyEntity.class, "a")
                        .where("a.attribute").eq("bogus")
                        .bind("myEntity").select("a.id")
                        .bind("id").select("1")
                        .end()
                        .from(Cte.class)
                        .select("myEntity")
                        .getResultList();

                System.out.println(resultList);
            }
        });
    }

produces:

    with recursive Issue571Test$Cte(myEntity_id, id) AS( select
        issue571te0_.id as col_0_0_,
        1 as col_1_0_ 
    from
        Issue571Test$MyEntity issue571te0_ 
    UNION
    ALL select
        issue571te0_.id as col_0_0_,
        1 as col_1_0_ 
    from
        Issue571Test$MyEntity issue571te0_ 
    where
        issue571te0_.attribute=? ) select
        issue571te1_.id as id1_1_,
        issue571te1_.attribute as attribut2_1_ 
    from
        Issue571Test$Cte issue571te0_ 
    inner join
        Issue571Test$MyEntity issue571te1_ 
            on issue571te0_.myEntity_id=issue571te1_.id

(I believe the original issue was that in this case, the inner join wouldn't be added as well, requiring an additional select to initialize the entity, but I cant reproduce that. The other part of the issue is however obvious from the above example).

@jwgmeligmeyling
Copy link
Collaborator Author

The issue occurs because the AbstractCTECriteriaBuilder will produce SELECT a, 1 FROM Issue571Test$MyEntity as JPQL for the base query of the CTE, which in the query translator eventually gets adjusted to SELECT a.id, 1, a.id, a.attribute FROM a (with some custom aliasing). I'm a little surprised the id field is projected twice, but this seems to be a Hibernate thing.

I think the easiest way to solve this is by providing different JPQL for the base query. I.e. translate .bind("association").to("entity") to .bind("association.uniqueKeyPropertyName").to("entity.uniqueKeyPropertyName").

jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Aug 28, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Aug 30, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Aug 30, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 3, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 4, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 4, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 4, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 4, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 4, 2018
@jwgmeligmeyling jwgmeligmeyling changed the title Some quirks with CTE (fetch) joins Support binding embeddables and associations using compound or foreign keys in CTE’s Sep 13, 2018
jwgmeligmeyling added a commit to jwgmeligmeyling/blaze-persistence that referenced this issue Sep 13, 2018
beikov pushed a commit that referenced this issue Sep 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment