Skip to content

Wrong column value bound for transitive self-referencing parent #3850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
onacit opened this issue Apr 21, 2025 · 1 comment
Open

Wrong column value bound for transitive self-referencing parent #3850

onacit opened this issue Apr 21, 2025 · 1 comment
Labels
status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged

Comments

@onacit
Copy link

onacit commented Apr 21, 2025

Sorry for the vague title.

I have the following tables

[Hibernate] 
    create table h2_other (
        id bigint generated by default as identity,
        some_id bigint not null unique,
        primary key (id)
    )
[Hibernate] 
    create table h2_other_history (
        id bigint generated by default as identity,
        some_id bigint not null,
        primary key (id)
    )
[Hibernate] 
    create table h2_some (
        id bigint generated by default as identity,
        parent_id bigint,
        name varchar(255) not null,
        primary key (id)
    )

Java classes look like this. No reproducible classes/project required these are all I recreated for reproducing the issue.

import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

import java.util.Optional;

@Entity
@Table(name = "h2_other")
@Setter
@Getter
public class H2Other {

    public H2Some getSome() {
        return some;
    }

    public void setSome(H2Some some) {
        this.some = some;
        setSomeId(
                Optional.ofNullable(this.some).map(H2Some::getId).orElse(null)
        );
    }

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "id", nullable = false, insertable = true, updatable = false)
    private Long id;

    @NotNull
    @Basic(optional = false)
    @Column(name = "some_id", nullable = false, insertable = true, updatable = false, unique = true)
    private Long someId;

    @Valid
    @NotNull
    @OneToOne(optional = false)
    @JoinColumn(name = "some_id", nullable = false, insertable = false, updatable = false, unique = true)
    private H2Some some;
}

import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = "h2_some")
@Setter
@Getter
public class H2Some {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "id", nullable = false, insertable = true, updatable = false)
    private Long id;

    @NotNull
    @Basic(optional = false)
    @Column(name = "name", nullable = false, insertable = true, updatable = true)
    private String name = "whatever";

    @ManyToOne(optional = true)
    @JoinColumn(name = "parent_id", nullable = true, insertable = true, updatable = true)
    private H2Some parent;
}

import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;

import java.util.Optional;

@Entity
@Table(name = "h2_other_history")
@Setter
@Getter
public class H2OtherHistory {

    public H2Some getSome() {
        return some;
    }

    public void setSome(H2Some some) {
        this.some = some;
        setSomeId(
                Optional.ofNullable(this.some).map(H2Some::getId).orElse(null)
        );
    }

    public H2Other getOther() {
        return other;
    }

    public void setOther(H2Other other) {
        this.other = other;
        setSome(
                Optional.ofNullable(this.other).map(H2Other::getSome).orElse(null)
        );
    }

    // -----------------------------------------------------------------------------------------------------------------
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "id", nullable = false, insertable = true, updatable = false)
    private Long id;

    // -----------------------------------------------------------------------------------------------------------------
    @Valid
    @NotNull
    @Basic(optional = false)
    @Column(name = "some_id", nullable = false, insertable = true, updatable = false)
    private Long someId;

    @Valid
    @NotNull
    @OneToOne(optional = false)
    @JoinColumn(name = "some_id", nullable = false, insertable = false, updatable = false)
    private H2Some some;

    // -----------------------------------------------------------------------------------------------------------------
    @NotFound(action = NotFoundAction.IGNORE) // -> EAGER!!!
    @ManyToOne(optional = false,
               fetch = FetchType.LAZY // WON'T WORK!!!!
               )
    @JoinColumn(name = "some_id", referencedColumnName = "some_id", nullable = false, insertable = false,
                updatable = false)
    private H2Other other;
}

With the following repository interface,

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface H2OtherHistoryRepository extends JpaRepository<H2OtherHistory, Long> {

    Page<H2OtherHistory> findAllBySomeId(Long someId, Pageable pageable);

    Page<H2OtherHistory> findAllByOther(H2Other other, Pageable pageable);
}

I tried the following tests.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.data.domain.Pageable;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class H2OtherHistoryRepositoryDataJpaTest {

    @Nested
    class FindBySomeTest {

    }

    @Nested
    class FindAllByOtherTest {

        @RepeatedTest(10)
//        @Test
        void __() {
            // ---------------------------------------------------------------------------------------------------------
            final var some = testEntityManager.persist(new H2Some());


            // toggle!!!
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            some.setParent(testEntityManager.persist(new H2Some()));
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

            // ---------------------------------------------------------------------------------------------------------
            final var other = new H2Other();
            other.setSome(some);
            testEntityManager.persist(other);
            // ---------------------------------------------------------------------------------------------------------
            final var otherHistory = new H2OtherHistory();
            otherHistory.setOther(other);
            testEntityManager.persist(otherHistory);
            // ---------------------------------------------------------------------------------------------------------
            final var found = otherHistoryRepository.findAllByOther(other, Pageable.unpaged());
            assertThat(found).contains(otherHistory);
        }
    }

    @Autowired
    private TestEntityManager testEntityManager;

    @Autowired
    private H2OtherHistoryRepository otherHistoryRepository;
}

Now it 100 % fails while 100 % passes without the,

            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            some.setParent(testEntityManager.persist(new H2Some()));
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 21, 2025
@mp911de
Copy link
Member

mp911de commented Apr 22, 2025

Have you tried replacing otherHistoryRepository.findAllByOther(…) with a query to EntityManager.createQuery(…)? This issue looks like a pure Hibernate issue.

@mp911de mp911de added the status: waiting-for-feedback We need additional information before we can continue label Apr 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

No branches or pull requests

3 participants