简体   繁体   English

JPA通过缺少零大小的集合项而与Group左外部联接

[英]JPA Left outer join with Group by missing collection item with zero size

I'm trying to perform a simple left outer join on 2 related entities. 我正在尝试对2个相关实体执行简单的左外部联接。

Following are the entities (Omitted getter/setters) 以下是实体(省略的getter / setter)

@Entity
public class TestPart {
    @Id
    @GeneratedValue
    private int partId;

    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn(name="f_categoryId", nullable=false)
    private TestCategory category;
}

@Entity
public class TestCategory {
    @Id
    @GeneratedValue
    private int categoryId;

    @OneToMany(mappedBy="category", cascade={CascadeType.ALL})
    private Set<TestPart> parts = new HashSet<>();
}

TestPart is the owning side of the relationship. TestPart是关系的拥有方。

Now I need to get the count of TestPart per TestCategory. 现在,我需要获取每个TestCategory的TestPart计数。 So I use the following JPQL query. 因此,我使用以下JPQL查询。

select distinct c, size(c.parts) from TestCategory c left join c.parts group by c

I expect that the categories for which there're no entries in TestPart would return with count 0, but that does not happen. 我希望在TestPart中没有条目的类别将返回计数0,但这不会发生。 Above query returns count only for the categories which have at least one entry in the TestPart. 以上查询仅返回在TestPart中至少具有一个条目的类别的计数。

I'm using following configuration. 我正在使用以下配置。

 1. spring-boot
 2. spring-data
 3. hibernate (as loaded by spring-data)
 4. Postgres 9.5

Following is the source for testing. 以下是测试的来源。

pom.xml 的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>TestLeftOuterJoin</groupId>
    <artifactId>TestLeftOuterJoin</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.2.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties application.properties

logging.level.org.springframework.web: DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss.SSS zzz

spring.jpa.database=POSTGRESQL
spring.datasource.platform=postgres
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=create-drop
spring.datasource.driver=org.postgresql.Driver
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url = jdbc:postgresql://localhost:5432/test?sslmode=disable
spring.datasource.username = postgres
spring.datasource.password = test


#spring.jpa.database=H2
#spring.datasource.platform=H2
#spring.jpa.show-sql=true
#spring.jpa.hibernate.ddl-auto=update
#spring.datasource.driver=org.h2.Driver
#hibernate.dialect=org.hibernate.dialect.H2Dialect
#spring.datasource.url = jdbc:h2:mem:testdb
#spring.datasource.username = sa
#spring.datasource.password =

TestPart TestPart

package test.entity;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

@Entity
public class TestPart {
    @Id
    @GeneratedValue
    private int partId;

    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn(name="f_categoryId", nullable=false)
    private TestCategory category;

    public int getPartId() {
        return partId;
    }

    public void setPartId(int partId) {
        this.partId = partId;
    }

    public TestCategory getCategory() {
        return category;
    }

    public void setCategory(TestCategory category) {
        this.category = category;
    }
}

TestCategory TestCategory

package test.entity;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class TestCategory {

    @Id
    @GeneratedValue
    private int categoryId;

    @OneToMany(mappedBy="category", cascade={CascadeType.ALL})
    /*@ElementCollection(fetch=FetchType.EAGER)*/
    private Set<TestPart> parts = new HashSet<>();

    public int getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(int categoryId) {
        this.categoryId = categoryId;
    }

    public Set<TestPart> getParts() {
        return parts;
    }

    public void setParts(Set<TestPart> parts) {
        this.parts = parts;
    }
}

PartRepository PartRepository

package test.entity;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PartRepository extends PagingAndSortingRepository<TestPart, Long>{

}

CategoryRepository CategoryRepository

package test.entity;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryRepository extends PagingAndSortingRepository<TestCategory, Long>{

}

Application 应用

package test;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@EnableAutoConfiguration
public class ApplicationConfig {

}

JunitTest JunitTest

package test;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;

import test.entity.CategoryRepository;
import test.entity.PartRepository;
import test.entity.TestCategory;
import test.entity.TestPart;
import test.queryresult.TestQueryResult;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ApplicationConfig.class)
public class JunitTest {

    @PersistenceContext
    EntityManager entityManager;

    @Autowired
    private CategoryRepository categoryRepo;

    @Autowired
    private PartRepository partRepositry;

    @Before
    public void init() {
        /*
         * adding 2 categories, category1 and category2.
         * adding 3 parts part1, part2 and part3
         * all parts are associated with category2
         */
        TestCategory category1 = new TestCategory();
        categoryRepo.save(category1);

        TestCategory category2 = new TestCategory();

        TestPart part1 = new TestPart();
        part1.setCategory(category2);

        TestPart part2 = new TestPart();
        part2.setCategory(category2);

        TestPart part3 = new TestPart();
        part3.setCategory(category2);

        Set<TestPart> partSet = new HashSet<>();
        partSet.addAll(Arrays.asList(part1, part2, part3));

        partRepositry.save(partSet);

    }

    @Test
    public void test() {
        System.out.println("##################### started " + TestQueryResult.class.getName());

        String query = "select distinct c, size(c.parts) from TestCategory c left join c.parts group by c";
        List list = entityManager.createQuery(query).getResultList();
        System.out.println("################# size " + list.size());

        Assert.isTrue(list.size() == 2, "list size must be 2");
    }
}

Edit 编辑

Adding the query produced by JPQL, 添加由JPQL生成的查询,

SELECT DISTINCT testcatego0_.category_id     AS col_0_0_, 
                Count(parts2_.f_category_id) AS col_1_0_, 
                testcatego0_.category_id     AS category1_0_ 
FROM   test_category testcatego0_ 
       LEFT OUTER JOIN test_part parts1_ 
                    ON testcatego0_.category_id = parts1_.f_category_id, 
       test_part parts2_ 
WHERE  testcatego0_.category_id = parts2_.f_category_id 
GROUP  BY testcatego0_.category_id

As it can be seen that JPQL is producing an unnecessary where clause testcatego0_.category_id = parts2_.f_category_id which is causing the issue. 可以看出,JPQL正在产生不必要的where子句testcatego0_.category_id = parts2_.f_category_id ,这引起了问题。

If I run the native query without this where clause it returns correct result. 如果运行不带where子句的本机查询,它将返回正确的结果。

Your query has 2 distinct joins in it over the parts relationship: 您的查询在部件关系中具有2个不同的联接:

"select distinct c, size(c.parts) from TestCategory c left join c.parts group by c"

The first is within the select, "size(c.parts)" forces JPA to traverse the relationship, and I'm guessing this accounts for the inner join in the resulting SQL though this might be a provider bug as I don't see how you would get a size of 0 as required by the spec - it should be using some sort of subquery to get the value rather than just a count. 第一个是在select中,“ size(c.parts)”强制JPA遍历关系,我猜想这可能是导致生成的SQL中内部联接的原因,尽管这可能是提供程序错误,因为我没有看到您将如何获得规格要求的0大小-应该使用某种子查询来获取值,而不只是计数。

The second is the explicit left join in the from clause. 第二个是from子句中的显式左连接。 Even though it isn't used anywhere, your query is required to include it in the SQL. 即使未在任何地方使用它,您也需要查询才能将其包括在SQL中。

What you instead may want is: 相反,您可能想要的是:

"select distinct c, size(c.parts) from TestCategory c"

Which should work according to the spec, but if not, try 哪个应该按照规范工作,否则,请尝试

"select distinct c, count(part.id) from TestCategory c left join c.parts part group by c"

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM