question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Nested Projections do not work with One-To-One Mapping

See original GitHub issue

Nested 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:closed
  • Created 2 years ago
  • Comments:8 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
schaudercommented, Apr 12, 2021

I created an issue with the Hibernate team: https://hibernate.atlassian.net/browse/HHH-14556

1reaction
schaudercommented, Apr 9, 2021

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 a Product instance should be.

CriteriaQuery<Object> query = em.getCriteriaBuilder().createQuery(Object.class);
Root<ProductDetails> root = query.from(ProductDetails.class);
query = query.multiselect(root.get("id"), root.get("costPrice"), root.get("product"));

List<Object> resultList = em.createQuery(query).getResultList();

SoftAssertions.assertSoftly(softly -> resultList.forEach(o -> {
	assertThat(o).isInstanceOf(Object[].class);
	softly.assertThat(((Object[]) o)[2]).isNotNull();
}));

It’s getting late here. If on Monday I still think this is a Hibernate issue I’ll open a ticket with them.

Read more comments on GitHub >

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

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found