skip to Main Content

I’m encountering a problem in a Spring Boot application using Hibernate, where OneToOne relationships involving entities with composite keys are not mapped correctly in a PostgreSQL database. Specifically, Hibernate maps the same entity instance to two properties in a parent entity, despite different expected results based on the underlying database entries.

Here are the simplified entity structures to illustrate the problem:

Entities:

@Entity
public class Foo {
    @EmbeddedId
    private FooId id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "alphaId", referencedColumnName = "userId"),
        @JoinColumn(name = "betaId", referencedColumnName = "barId")
    })
    private Bar alphaBar;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "betaId", referencedColumnName = "userId"),
        @JoinColumn(name = "alphaId", referencedColumnName = "barId")
    })
    private Bar betaBar;
}

@Entity
public class Bar {
    @EmbeddedId
    private BarId id;
}

@Embeddable
public class FooId {
    private String alphaId;
    private String betaId;
}

@Embeddable
public class BarId {
    private String userId;
    private String barId;
}

Problem:

In the application, each Foo instance involves two distinct Bar instances, representing different relationships (alphaBar and betaBar). However, when retrieving Foo, both alphaBar and betaBar are populated with the same Bar instance, even though the underlying data in the PostgreSQL database for each Bar is distinct and correct.

Requirements:

Each Foo should map to two distinct Bar instances, reflecting the unique userId and barId combinations.
Correct mapping is crucial for the integrity of the application’s data handling and processing.

What I’ve Tried:

Verified database integrity to ensure distinct and accurate data for Bar entries.
Checked the generated SQL through Hibernate’s logging, which seems correct but does not align with the application’s entity mapping results.

Questions:

Is there an error in how I’ve configured the @JoinColumns for mapping the relationships using composite keys?
Could Hibernate’s caching or session management be affecting how these entities are uniquely identified and fetched in a PostgreSQL setting?
I would appreciate any insights or suggestions on how to resolve this issue.

Update:

Further investigation into the issue has revealed that the problem might be linked to the use of the IN clause in the SQL generated by Hibernate. Despite configuring @JoinColumns for my OneToOne relationships and expecting Hibernate to use an inner join to fetch each associated Bar instance distinctly, the framework still utilizes an IN clause which seems to be causing incorrect entity mapping.

I am seeking advice on how to force Hibernate to utilize an explicit inner join without resorting to an IN clause, or any configurations that might prevent this behavior. Insights into how Hibernate handles such complex entity relationships and any potential misconfigurations in my setup would be highly appreciated.

2

Answers


  1. The way you set it up, Foo‘s composite primary key is made of alphaId and betaId columns. Those will be the only two columns in Foo‘s table on the db.

    @Entity
    public class Foo {
        @EmbeddedId
        private FooId id;
        //...
    }
    @Embeddable
    public class FooId {
        private String alphaId;
        private String betaId;
    }
    

    Depending on your PhysicalNamingStrategy (dictating how hibernate will map your field names in Java to column names in PostgreSQL), you then either instruct hibernate to use those same two columns (alphaId and betaId) to hold foreign keys pointing at the first Bar as its alphaBar, or introduce two very similar ones (if the FooId ended up translated differently, e.g. as alpha_id and beta_id, avoiding the overlap):

        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn( name = "alphaId" //this is a Foo table column
                        //and it was already used as the 1st half of its primary key
                        ,referencedColumnName = "userId"),
            @JoinColumn(name = "betaId", referencedColumnName = "barId")
        })
        private Bar alphaBar;
    

    But then you’re telling it to once again write foreign keys pointing at yet another Bar, as betaBar, to those same exact two columns as alphaBar (and possibly FooId).

        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn(  name = "betaId"//the same columns as before
                          //it's already supposed to also hold `alphaBar` keys
                        , referencedColumnName = "userId"),
            @JoinColumn(name = "alphaId", referencedColumnName = "barId")
        })
        private Bar betaBar;
    

    As a result (the first two depending on field name to column name translation)

    1. Once persisted on the db, each Foo is only uniquely identified by which Bar it’s linked to.
    2. Attempting to link a given Foo to a different Bar changes its identity. If something was referencing Foo, changing its alphaBar or betaBar would break that reference.
    3. From the db’s perspective, there’s no difference between alphaBar and betaBar, because they’re encoded by the same two columns in Foo‘s table on the db.
    4. It’s all fine from the Java perspective: these are all valid references to different objects. The decorators instructing Hibernate how to map them to a relational structure are to blame, resulting in the collision.

    Assuming that you want each Foo to be related to two (possibly different) Bars and avoid tying Foo‘s identity to anything it’s currently linked to, you need to

    1. Let Foo‘s composite primary key FooId use its own two columns,
    2. Let the alphaBar foreign key to the first Bar use its own two columns,
    3. Give another two to be used for betaBar.
    @Entity
    public class Foo {
        @EmbeddedId
        private FooId id;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn(name = "alphaBarUserId", referencedColumnName = "userId"),
            @JoinColumn(name = "alphaBarBarId", referencedColumnName = "barId")
        })
        private Bar alphaBar;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn(name = "betaBarUserId", referencedColumnName = "userId"),
            @JoinColumn(name = "betaBarBarId", referencedColumnName = "barId")
        })
        private Bar betaBar;
    }
    
    @Embeddable
    public class FooId {
        private String alphaId;
        private String betaId;
    }
    
    @Entity
    public class Bar {
        @EmbeddedId
        private BarId id;
    }
    
    @Embeddable
    public class BarId {
        private String userId;
        private String barId;
    }
    
    Login or Signup to reply.
  2. Another way to solve this issue is to use 2 classes Bar1 and Bar2.

    @Entity
    public class Foo {
        @EmbeddedId
        private FooId id;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn(name = "alphaId", referencedColumnName = "userId"),
            @JoinColumn(name = "betaId", referencedColumnName = "barId")
        })
        private Bar1 alphaBar;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumns({
            @JoinColumn(name = "alphaId", referencedColumnName = "barId"),
            @JoinColumn(name = "betaId", referencedColumnName = "userId")
        })
        private Bar2 betaBar;
    }
    
    @Entity
    public class Bar1 {
        @EmbeddedId
        private BarId1 id;
    }
    
    @Entity
    public class Bar2 {
        @EmbeddedId
        private BarId2 id;
    }
    
    @Embeddable
    public class FooId {
        private String alphaId;
        private String betaId;
    }
    
    @Embeddable
    public class BarId1 {
        private String userId;
        private String barId;
    }
    
    @Embeddable
    public class BarId2 {
        private String barId;
        private String userId;
        
    }
    

    Update : I add BarId1 and BarId2 to be more consistent.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search