Nested Projections do not work with One-To-One Mapping
See original GitHub issueNested projections are returning null
Spring-Boot: 2.4.3
Spring-Data-JPA: 2.4.5
Hibernate-Core: 5.4.28
PostgreSQL: 12
I have the following Schema
schema.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE product (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" varchar(100) NOT NULL,
description text NULL,
featured_media varchar(200) NOT NULL,
status varchar(10) NOT NULL DEFAULT 'DRAFT'::character varying,
tags _varchar NULL,
created_date timestamptz NOT NULL,
price numeric NULL DEFAULT 0,
last_modified_date timestamptz NOT NULL,
created_by varchar(100) NOT NULL,
last_modified_by varchar(100) NOT NULL,
CONSTRAINT product_id_pk PRIMARY KEY (id),
CONSTRAINT product_name_unq UNIQUE (name)
);
CREATE TABLE product_details (
id uuid NOT NULL,
cost_price numeric NULL,
CONSTRAINT product_detials_id_pk PRIMARY KEY (id)
);
ALTER TABLE product_details ADD CONSTRAINT product_details_id_fk FOREIGN KEY (id) REFERENCES product(id) ON UPDATE CASCADE ON DELETE CASCADE
data.sql
INSERT INTO product(id, name, featured_media, created_date, last_modified_date, created_by, last_modified_by, status) VALUES
('1fb9e691033d4092b32699088d401ec9', 'Jordans', '/files/products/jordans.jpeg', '2020-12-21 22:25:00+01:00', '2020-12-21 22:26:00+01:00', 'juliuskrah', 'juliuskrah', 'ACTIVE'),
('050976729f414a51ac6c3c673644cdc0', 'Hoodie', '/files/products/hoodie.jpeg', '2020-12-21 22:28:00+01:00', '2020-12-21 22:28:50+01:00', 'juliuskrah', 'juliuskrah', 'DRAFT');
INSERT INTO product_details(id, cost_price) VALUES
('1fb9e691033d4092b32699088d401ec9', 350.00),
('050976729f414a51ac6c3c673644cdc0', 250.00)
And the following classes
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditEntity implements Auditable<String, UUID, OffsetDateTime>, Serializable {
private static final long serialVersionUID = 1111111L;
@Id
@GeneratedValue
private UUID id;
private OffsetDateTime createdDate;
private OffsetDateTime lastModifiedDate;
private String createdBy;
private String lastModifiedBy;
@Transient
private boolean isNew = true;
@Override
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
@Override
public Optional<String> getCreatedBy() {
return Optional.of(this.createdBy);
}
@Override
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
@Override
public Optional<String> getLastModifiedBy() {
return Optional.of(this.lastModifiedBy);
}
@Override
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
@Override
public Optional<OffsetDateTime> getCreatedDate() {
return Optional.of(this.createdDate);
}
@Override
public void setCreatedDate(OffsetDateTime createdDate) {
this.createdDate = createdDate;
}
@Override
public Optional<OffsetDateTime> getLastModifiedDate() {
return Optional.of(this.lastModifiedDate);
}
@Override
public void setLastModifiedDate(OffsetDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
}
public enum ProductStatus {
ACTIVE,
DRAFT
}
@Data
@Entity
@EqualsAndHashCode(callSuper = false)
@org.hibernate.annotations.TypeDef(name = "list-array", typeClass = com.vladmihalcea.hibernate.type.array.ListArrayType.class)
public class Product extends AbstractAuditEntity {
private static final long serialVersionUID = 222222L;
private String name;
@Column(columnDefinition = "text")
private String description;
private URI featuredMedia;
private Double price;
@Enumerated(EnumType.STRING)
private ProductStatus status = ProductStatus.DRAFT;
@org.hibernate.annotations.Type(type = "list-array")
@Column(columnDefinition = "varchar[]")
private List<String> tags;
}
@Data
@Entity
public class ProductDetails implements Serializable {
private static final long serialVersionUID = 3333L;
@Id
private UUID id;
@MapsId
@JoinColumn(name = "id")
@OneToOne
private Product product;
private Double costPrice;
}
@Converter(autoApply = true)
public class UriAttributeConverter implements AttributeConverter<URI, String> {
@Override
public String convertToDatabaseColumn(URI entityValue) {
return (entityValue == null) ? null : entityValue.toString();
}
@Override
public URI convertToEntityAttribute(String databaseValue) {
return (org.springframework.util.StringUtils.hasLength(databaseValue) ? URI.create(databaseValue.trim()) : null);
}
}
public interface ProductDetailsRepository extends CrudRepository<ProductDetails, UUID> {
<T> Optional<T> findById(UUID id, Class<T> clazz);
}
public interface StoreAdminProduct {
UUID getId();
Double getCostPrice();
ProductView getProduct();
static interface ProductView {
String getName();
URI getFeaturedMedia();
}
}
// Tests
public class ProductRepositoryTest {
// Omitting boostrap code and test containers
@Autowired
private ProductDetailsRepository detailsRepository;
@Test
void fetchProjectedProductWithDetailsTest() {
var storeFrontProduct = detailsRepository.findById(
UUID.fromString("1fb9e691-033d-4092-b326-99088d401ec9"), StoreAdminProduct.class);
assertThat(storeFrontProduct).isPresent()
.get(InstanceOfAssertFactories.type(StoreAdminProduct.class))
.hasFieldOrPropertyWithValue("costPrice", 350.0)
.extracting(StoreAdminProduct::getProduct).isNotNull() // Fails here
.hasFieldOrPropertyWithValue("featuredMedia", URI.create("/files/products/jordans.jpeg"));
}
}
Below is the query generated
select
productdet0_.id as col_0_0_,
productdet0_.cost_price as col_1_0_,
product1_.id as col_2_0_,
product1_.id as id1_9_,
product1_.created_by as created_2_9_,
product1_.created_date as created_3_9_,
product1_.last_modified_by as last_mod4_9_,
product1_.last_modified_date as last_mod5_9_,
product1_.description as descript6_9_,
product1_.featured_media as featured7_9_,
product1_.name as name9_9_,
product1_.status as status10_9_,
product1_.tags as tags11_9_,
from
product_details productdet0_
left outer join
product product1_
on productdet0_.id=product1_.id
where
productdet0_.id=?
As you can see, the query is clearly selecting more columns than I specified in my closed projection
Issue Analytics
- State:
- Created 2 years ago
- Comments:8 (6 by maintainers)
Top Results From Across the Web
How to make Spring Projections work with @OneToOne ...
You can use Dto in JPQL query. Right below a simple example. @Entity @Data public class User { @Id private Long id; private...
Read more >Spring Data JPA Projections - Baeldung
A quick and practical overview of Spring Data JPA Projections.
Read more >How to fetch a one-to-many DTO projection with JPA and ...
Introduction. In this article, I'm going to show you how you can fetch a one-to-many relationship as a DTO projection when using JPA...
Read more >Why, When and How to Use DTO Projections with JPA and ...
DTO projections are the most efficient ones for read operations. Let me show you how to use them in JPQL, Criteria and native...
Read more >Nested Projections with Hibernate and Spring Data – BeToneful
However, things to watch out is N+1 fetch problem and even Open vs Closed projections when defining the attributes subset. Still, this all...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
I created an issue with the Hibernate team: https://hibernate.atlassian.net/browse/HHH-14556
Thanks for the reproducer. I poked around and currently I’m thinking this might be a Hibernate issue.
The following test recreates basically the criteria query created by Spring Data and demonstrates, that it’s result has
null
values where aProduct
instance should be.It’s getting late here. If on Monday I still think this is a Hibernate issue I’ll open a ticket with them.