0

I have two tables in my database that are mapped together via composite primary key/foreign keys, and I'm having a hell of a time getting Hibernate to work with them. My database looks like this:

Simple ERD

TABLE1 has a composite primary key of foreign keys, mapping to TABLE_A and TABLE_B. TABLE2 also has a composite primary key of foreign keys, mapping to TABLE_A, TABLE_B, and TABLE_D. In the database, TABLE2 maps back to TABLE1 using just the first two foreign keys. No problems there. It's translating this to Hibernate that is killing me.

Because TABLE2 requires an embedded id with three columns, I cannot use the @OneToMany annotation's mappedBy parameter. I get the expected error of the number of foreign keys not matching the primary key columns. So, I utilized @JoinColumns instead. And that worked perfectly fine for saving new entities. However, when I attempt to delete some mappings from TABLE2, I run into an issue where Hibernate is attempting to update TABLE2 before deleting, setting FK_TABLE_A to null, which is obviously not allowed. The best I've been able to find is that the use of inverse="true" in a mapping xml might resolve the problem, ensuring that Hibernate knows that despite the use of @JoinColumn, the TABLE1 entity should be the owner of the relationship. But I'm not using the XML, and I cannot figure out what the equivalent might be via annotations.

Here's what I have so far:

@Entity
@AssociationOverrides({
    @AssociationOverride(name = "pk.tableA",
            joinColumns = @JoinColumn(name = "FK_TABLE_A")),
    @AssociationOverride(name = "pk.tableB",
            joinColumns = @JoinColumn(name = "FK_TABLE_B")) })
@Table(name="TABLE1")
public class Table1 extends BaseObject implements Serializable
{
private static final long serialVersionUID = 1L;

private Table1Id pk = new Table1Id();

@EmbeddedId
public Table1Id getPk() {
    return pk;
}
public void setPk(Table1Id pk) {
    this.pk = pk;
}

private TableC tableC;
@ManyToOne
@JoinColumn(name = "FK_TABLE_C", referencedColumnName = "ID", nullable = true)
public TableC getTableC () {
    return this.tableC;
}
public void setTableC(TableC tableC) {
    this.tableC= tableC;
}

private List<Table2> table2s;
@OneToMany(cascade = {CascadeType.ALL}, orphanRemoval=true, fetch=FetchType.EAGER)
@JoinColumns({
    @JoinColumn(name="FK_TABLE_A", referencedColumnName="FK_TABLE_A"),
    @JoinColumn(name="FK_TABLE_B", referencedColumnName="FK_TABLE_B")
})
public List<Table2> getTable2s() {
    return table2s;
}
public void setTable2s(List<Table2> table2s) {
    this.table2s= table2s;
}

@Override
public boolean equals(Object o) {
    ...
}

@Override
public int hashCode() {
    ...
}

@Override
public String toString() {
    ...
}
}

@Embeddable
public class Table1Id extends BaseObject implements Serializable 
{
    private static final long serialVersionUID = 1L;

    private TableA tableA;
    private TableB tableB;

    @ManyToOne
    public TableA getTableA() {
        return tableA;
    }
    public void setTableA(TableA tableA) {
        this.tableA = tableA;
    }

    @ManyToOne
    public TableB getTableB() {
        return tableB;
    }
    public void setTableB(TableB tableB) {
        this.tableB= tableB;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}


@Entity
@AssociationOverrides({
        @AssociationOverride(name = "pk.tableA",
                joinColumns = @JoinColumn(name = "FK_TABLE_A")),
        @AssociationOverride(name = "pk.tableB",
                joinColumns = @JoinColumn(name = "FK_TABLE_B")),
        @AssociationOverride(name = "pk.tableD",
                joinColumns = @JoinColumn(name = "FK_TABLE_D")) })
@Table(name="TABLE2")
public class Table2 extends BaseObject implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Table2Id pk = new Table2Id ();

    @EmbeddedId
    public Table2Id getPk() {
        return pk;
    }
    public void setPk(Table2Id pk) {
        this.pk = pk;
    }

    private Double value;
    @Column(name = "VALUE", nullable = false, insertable = true, updatable = true, precision = 2)
    @Basic
    public Double getValue() {
        return this.value;
    }
    public void setValue(Double value) {
        this.goal = goal;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

@Embeddable
public class Table2Id extends BaseObject implements Serializable 
{
    private static final long serialVersionUID = 1L;

    private TableA tableA;
    @ManyToOne
    public TableA getTableA() {
        return tableA;
    }
    public void setTableA(TableA tableA) {
        this.tableA= tableA;
    }

    private TableB tableB;
    @ManyToOne
    public TableB getTableB() {
        return tableB;
    }
    public void setTableB(TableB tableB) {
        this.tableB= tableB;
    }

    private TableD tableD;
    @ManyToOne
    public TableD getTableD() {
        return this.tableD;
    }
    public void setTableD(TableD tableD) {
        this.tableD= tableD;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

Normally for relationships like this, I just use the mappedBy value of the @OneToMany annotation, and everything works fine - updates, inserts, and deletes execute as expected and desired. But given the admittedly odd way the underlying tables are constructed, I cannot do that. Mapping to only a single record in the Table2Id (mappedBy="pk.tableA" or mappedBy="pk.tableB") would result in completely incorrect data. I require both fields to have an appropriate match, but near as I can tell I cannot have multiple columns listed in mappedBy. mappedBy="pk.tableA, pk.tableB" fails.

I know that I can resolve this easily by just modifying the database and adding a single ID primary key to TABLE1, and a single FK_TABLE1 primary key to TABLE2. Then I could just use my standard approach of @OneToMany(mappedBy="table1"...). But I was really hoping to avoid that, if for no other reason that I obviously don't need to do that on the database level. I'm hoping there's a way to tell Hibernate that Table1 is the owner, and that all changes to Table2 are dependent on it.

Curtis Snowden
  • 407
  • 1
  • 4
  • 20

1 Answers1

0

God, this was a nightmare. I finally have it figured out and in retrospect, it's something I really should have thought of sooner. Here's what worked for me in case anyone else has a similar problem in the future.

The problem is that the Table2 embedded Id was mapping directly to the same entities as the Table1 embedded Id. That's what I want with the database, but not what I want with Hibernate. Instead, the two fields for TableA and TableB should be represented by Table1 itself, and the association overrides written to match. They need to include insertable=false and updatable=false so that Table2 can't make any changes to Table1. In my case, I want just a unidirectional relationship. Table1 can then use the mappedBy parameter of the @OneToMany annotation to map directly to itself. This allows Table1 to control the relationship. So, the code should be:

@Entity
@AssociationOverrides({
        @AssociationOverride(name = "pk.tableA",
                joinColumns = @JoinColumn(name = "FK_TABLE_A", nullable=false)),
        @AssociationOverride(name = "pk.tableB",
                joinColumns = @JoinColumn(name = "FK_TABLE_B", nullable=false)) })
@Table(name="TABLE1")
public class Table1 extends BaseObject implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Table1Id pk = new Table1Id ();

    @EmbeddedId
    public Table1Id getPk() {
        return pk;
    }
    public void setPk(Table1Id pk) {
        this.pk = pk;
    }

    private TableC tableC;
    @ManyToOne
    @JoinColumn(name = "FK_TABLE_C", referencedColumnName = "ID", nullable = true)
    public TableC getTableC() {
        return this.tableC;
    }
    public void setTableC(TableC tableC) {
        this.tableC = tableC;
    }

    private List<Table2> table2s;
    @OneToMany(mappedBy="pk.table1", cascade = {CascadeType.ALL}, orphanRemoval=true, fetch=FetchType.EAGER)
    public List<Table2> getTable2s() {
        return table2s;
    }
    public void setTable2s(List<Table2> table2s) {
        this.table2s= table2s;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

@Entity
@AssociationOverrides({
        @AssociationOverride(name = "pk.table1",
                joinColumns = {
                        @JoinColumn(name = "FK_TABLE_A", nullable=false, insertable=false, updatable=false),
                        @JoinColumn(name = "FK_TABLE_B", nullable=false, insertable=false, updatable=false)
                        }),
        @AssociationOverride(name = "pk.tableD",
                joinColumns = @JoinColumn(name = "FK_TABLE_D", nullable=false)) })
@Table(name="TABLE2")
public class Table2 extends BaseObject implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Table2Id pk = new Table2Id();

    @EmbeddedId
    public Table2Id getPk() {
        return pk;
    }
    public void setPk(Table2Id pk) {
        this.pk = pk;
    }

    private Double value;
    @Column(name = "VALUE", nullable = false, insertable = true, updatable = true, precision = 2)
    @Basic
    public Double getValue() {
        return this.value;
    }
    public void setValue(Double value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

@Embeddable
public class Table2Id extends BaseObject implements Serializable 
{
    private static final long serialVersionUID = 1L;

    private Table1 table1;
    @ManyToOne
    @JoinColumn(nullable=false)
    public Table1 getTable1() {
        return this.table1;
    }
    public void setTable1(Table1 table1) {
        this.table1 = table1;
    }

    private TableD tableD;
    @ManyToOne
    @JoinColumn(nullable=false)
    public TableD getTableD() {
        return this.tableD;
    }
    public void setTableD(TableD tableD) {
        this.tableD = tableD;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}
Curtis Snowden
  • 407
  • 1
  • 4
  • 20