简体   繁体   中英

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.

Following are the entities (Omitted getter/setters)

@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.

Now I need to get the count of TestPart per TestCategory. So I use the following JPQL query.

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. Above query returns count only for the categories which have at least one entry in the 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

<?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

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

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

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

package test.entity;

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

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

}

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

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,

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.

If I run the native query without this where clause it returns correct result.

Your query has 2 distinct joins in it over the parts relationship:

"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.

The second is the explicit left join in the from clause. Even though it isn't used anywhere, your query is required to include it in the 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"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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