Multi-Column Join in Hibernate/JPA Annotations

121,735

Solution 1

This worked for me . In my case 2 tables foo and boo have to be joined based on 3 different columns.Please note in my case ,in boo the 3 common columns are not primary key

i.e., one to one mapping based on 3 different columns

@Entity
@Table(name = "foo")
public class foo implements Serializable
{
    @Column(name="foocol1")
    private String foocol1;
    //add getter setter
    @Column(name="foocol2")
    private String foocol2;
    //add getter setter
    @Column(name="foocol3")
    private String foocol3;
    //add getter setter
    private Boo boo;
    private int id;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "brsitem_id", updatable = false)
    public int getId()
    {
        return this.id;
    }
    public void setId(int id)
    {
        this.id = id;
    }
    @OneToOne
    @JoinColumns(
    {
        @JoinColumn(updatable=false,insertable=false, name="foocol1", referencedColumnName="boocol1"),
        @JoinColumn(updatable=false,insertable=false, name="foocol2", referencedColumnName="boocol2"),
        @JoinColumn(updatable=false,insertable=false, name="foocol3", referencedColumnName="boocol3")
    }
    )
    public Boo getBoo()
    {
        return boo;
    }
    public void setBoo(Boo boo)
    {
        this.boo = boo;
    }
}





@Entity
@Table(name = "boo")
public class Boo implements Serializable
{
    private int id;
    @Column(name="boocol1")
    private String boocol1;
    //add getter setter
    @Column(name="boocol2")
    private String boocol2;
    //add getter setter
    @Column(name="boocol3")
    private String boocol3;
    //add getter setter
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id", updatable = false)
    public int getId()
    {
        return id;
    }
    public void setId(int id)
    {
        this.id = id;
    }
}

Solution 2

If this doesn't work I'm out of ideas. This way you get the 4 columns in both tables (as Bar owns them and Foo uses them to reference Bar) and the generated IDs in both entities. The set of 4 columns has to be unique in Bar so the many-to-one relation doesn't become a many-to-many.

@Embeddable
public class AnEmbeddedObject
{
    @Column(name = "column_1")
    private Long column1;
    @Column(name = "column_2")
    private Long column2;
    @Column(name = "column_3")
    private Long column3;
    @Column(name = "column_4")
    private Long column4;
}

@Entity
public class Foo
{
    @Id
    @Column(name = "id")
    @GeneratedValue(generator = "seqGen")
    @SequenceGenerator(name = "seqGen", sequenceName = "FOO_ID_SEQ", allocationSize = 1)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "column_1", referencedColumnName = "column_1"),
        @JoinColumn(name = "column_2", referencedColumnName = "column_2"),
        @JoinColumn(name = "column_3", referencedColumnName = "column_3"),
        @JoinColumn(name = "column_4", referencedColumnName = "column_4")
    })
    private Bar bar;
}

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {
    "column_1",
    "column_2",
    "column_3",
    "column_4"
}))
public class Bar
{
    @Id
    @Column(name = "id")
    @GeneratedValue(generator = "seqGen")
    @SequenceGenerator(name = "seqGen", sequenceName = "BAR_ID_SEQ", allocationSize = 1)
    private Long id;
    @Embedded
    private AnEmbeddedObject anEmbeddedObject;
}

Solution 3

Hibernate is not going to make it easy for you to do what you are trying to do. From the Hibernate documentation:

Note that when using referencedColumnName to a non primary key column, the associated class has to be Serializable. Also note that the referencedColumnName to a non primary key column has to be mapped to a property having a single column (other cases might not work). (emphasis added)

So if you are unwilling to make AnEmbeddableObject the Identifier for Bar then Hibernate is not going to lazily, automatically retrieve Bar for you. You can, of course, still use HQL to write queries that join on AnEmbeddableObject, but you lose automatic fetching and life cycle maintenance if you insist on using a multi-column non-primary key for Bar.

Share:
121,735
Tadraes
Author by

Tadraes

Updated on February 17, 2020

Comments

  • Tadraes
    Tadraes over 4 years

    I have two entities which I would like to join through multiple columns. These columns are shared by an @Embeddable object that is shared by both entities. In the example below, Foo can have only one Bar but Bar can have multiple Foos (where AnEmbeddableObject is a unique key for Bar). Here is an example:

    @Entity
    @Table(name = "foo")
    public class Foo {
        @Id
        @Column(name = "id")
        @GeneratedValue(generator = "seqGen")
        @SequenceGenerator(name = "seqGen", sequenceName = "FOO_ID_SEQ", allocationSize = 1)
        private Long id;
        @Embedded
        private AnEmbeddableObject anEmbeddableObject;
        @ManyToOne(targetEntity = Bar.class, fetch = FetchType.LAZY)
        @JoinColumns( {
            @JoinColumn(name = "column_1", referencedColumnName = "column_1"),
            @JoinColumn(name = "column_2", referencedColumnName = "column_2"),
            @JoinColumn(name = "column_3", referencedColumnName = "column_3"),
            @JoinColumn(name = "column_4", referencedColumnName = "column_4")
        })
        private Bar bar;
    
        // ... rest of class
    }
    

    And the Bar class:

    @Entity
    @Table(name = "bar")
    public class Bar {
        @Id
        @Column(name = "id")
        @GeneratedValue(generator = "seqGen")
        @SequenceGenerator(name = "seqGen", sequenceName = "BAR_ID_SEQ", allocationSize = 1)
        private Long id;
        @Embedded
        private AnEmbeddableObject anEmbeddableObject;
    
        // ... rest of class
    }
    

    Finally the AnEmbeddedObject class:

    @Embeddable
    public class AnEmbeddedObject {
        @Column(name = "column_1")
        private Long column1;
        @Column(name = "column_2")
        private Long column2;
        @Column(name = "column_3")
        private Long column3;
        @Column(name = "column_4")
        private Long column4;
    
        // ... rest of class
    }
    

    Obviously the schema is poorly normalised, it is a restriction that AnEmbeddedObject's fields are repeated in each table.

    The problem I have is that I receive this error when I try to start up Hibernate:

    org.hibernate.AnnotationException: referencedColumnNames(column_1, column_2, column_3, column_4) of Foo.bar referencing Bar not mapped to a single property
    

    I have tried marking the JoinColumns are not insertable and updatable, but with no luck. Is there a way to express this with Hibernate/JPA annotations?

  • Tadraes
    Tadraes almost 12 years
    That's the rub, the "AnEmbeddedObject" must exist in both objects. The generated id is preferable for performance reasons.
  • siebz0r
    siebz0r almost 12 years
    So the 4 columns plus the generated key map to bar? That is very very bad. If your solution is to generate a surrogate key for Bar and don't make the columns in Foo foreign key, you have to set the columns in Foo yourself. If I'm right I can adjust my answer. ;-)
  • siebz0r
    siebz0r almost 12 years
    @bowsie I modified my answer so Foo and Bar both have the 4 columns and Bar is referenced in Foo with its ID.
  • Tadraes
    Tadraes almost 12 years
    Cheers. But this is still not quite what I'm after. For performance reasons I do not want to look up Bar's primary id, and I expect that hibernate should allow me to join on whatever I want - and if I do reference columns twice then setting insertable and updatable to false should do the trick - like with single-column joins.
  • siebz0r
    siebz0r almost 12 years
    @bowsie I've adjusted my answer again, maybe this is what you are looking for?
  • siebz0r
    siebz0r almost 12 years
    I'd say it's worth a shot, in theory using multiple foreign keys which refer to one unique constraint should be possible. If it's not the OP's out of luck. +1 for this is possibly the answer.
  • Old Pro
    Old Pro almost 12 years
    @siebz0r, the OP has taken the shot and gotten almost exactly the error message I would expect for this error condition: "referencedColumnNames... not mapped to a single property". It should say "not mapped to a property having a single column," but it's about the same thing.
  • siebz0r
    siebz0r almost 12 years
    I guess it's game over then ;-)
  • Tadraes
    Tadraes almost 12 years
    Thanks for the info. I have been using HQL in the meantime - but good to understand the limitations. +1
  • user666
    user666 about 5 years
    In my case i want to join on one column but the deleted field should be null in order to make the join (i dont want to retrieve softly deleted items) how to implement this?
  • Stunner
    Stunner over 3 years
    Looking at the answer in 2020 , the code looks horrible. Can someone please explain why should we use JPA if we have multiple and complex joins. Why not Spring jdbc or any other ORM ? Just want to understand. Also let's say I have 10 joins in a query and multiple such queries , the whole class will be filled with annotations.