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.