I am having some difficulty in mapping an attributed many-to-many relationship with overlapping Primary and Foreign keys. Following the recommended best practices I mapped the relationship using two one-to-many relationships, representing the “join” table as its own entity – which made a lot of sense given the additional attributes it held. However due to the existence of a compound key, components of which are in fact foreign keys I’ve had some difficulty in getting a valid set of mapping annotations. Additionally the objects at either end of the many-to-many relationship are using auto-generated keys, which I think is complicating matters.
Probably best illustrated by an example so here goes:
Hibernate version: 3.2 CR4, HEM/HAN: 3.2 CR2
Mapping documents:
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/ ... ce_1_0.xsd"
version="1.0">
<persistence-unit name="Test" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>entity.TableOne</class>
<class>entity.TableTwo</class>
<class>entity.TableOneTwo</class>
<class>entity.TableOneTwoPK</class>
<exclude-unlisted-classes />
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle9Dialect" />
<property name="hibernate.connection.driver_class"
value="oracle.jdbc.driver.OracleDriver" />
<property name="hibernate.connection.username" value="greg" />
<property name="hibernate.connection.password" value="password" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:orcl" />
<property name="hibernate.cache.provider_class"
value="org.hibernate.cache.HashtableCacheProvider" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.use_sql_comments" value="true" />
</properties>
</persistence-unit>
</persistence>
Full stack trace of any exception that occurs:
javax.persistence.RollbackException: Error while commiting the transaction
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:63)
at test.TableOneTwoTest.testCreateEntities(TableOneTwoTest.java:48)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:164)
at junit.framework.TestCase.runBare(TestCase.java:130)
at junit.framework.TestResult$1.protect(TestResult.java:110)
at junit.framework.TestResult.runProtected(TestResult.java:128)
at junit.framework.TestResult.run(TestResult.java:113)
at junit.framework.TestCase.run(TestCase.java:120)
at junit.framework.TestSuite.runTest(TestSuite.java:228)
at junit.framework.TestSuite.run(TestSuite.java:223)
at org.junit.internal.runners.OldTestClassRunner.run(OldTestClassRunner.java:35)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:38)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: org.hibernate.exception.SQLGrammarException: Could not execute JDBC batch update
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:67)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:249)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:235)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:139)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:297)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:53)
... 20 more
Caused by: java.sql.BatchUpdateException: ORA-00957: duplicate column name
at oracle.jdbc.driver.DatabaseError.throwBatchUpdateException(DatabaseError.java:343)
at oracle.jdbc.driver.OraclePreparedStatement.executeBatch(OraclePreparedStatement.java:10698)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:242)
... 28 more
Name and version of the database you are using: Oracle 10g
The generated SQL (show_sql=true):
/* insert entity.TableOneTwo
*/ insert
into
TABLEONE_TABLETWO
(DATA, TABLEONEID, TABLETWOID, tableOneId, tableTwoId)
values
(?, ?, ?, ?, ?)
DB Schema:
Code:
ALTER TABLE TableOne_TableTwo DROP CONSTRAINT FK_TableOne_TableTwo_1;
ALTER TABLE TableOne_TableTwo DROP CONSTRAINT FK_TableOne_TableTwo_2;
DROP TABLE TableOne;
DROP TABLE TableTwo;
DROP TABLE TableOne_TableTwo;
DROP SEQUENCE hibernate_sequence;
CREATE TABLE TableOne (
TableOneId NUMBER (20) NOT NULL,
data VARCHAR (255) NOT NULL,
CONSTRAINT PK_TableOne PRIMARY KEY (TableOneId)
);
CREATE TABLE TableTwo (
TableTwoId NUMBER (20) NOT NULL,
data VARCHAR (255) NOT NULL,
CONSTRAINT PK_TableTwo PRIMARY KEY (TableTwoId)
);
CREATE TABLE TableOne_TableTwo (
TableOneId NUMBER (20) NOT NULL,
TableTwoId NUMBER (20) NOT NULL,
data VARCHAR (255) NOT NULL,
CONSTRAINT FK_TableOne_TableTwo PRIMARY KEY (TableOneId, TableTwoId)
);
ALTER TABLE TableOne_TableTwo ADD (CONSTRAINT FK_TableOne_TableTwo_1 FOREIGN KEY (TableOneId) REFERENCES TableOne (TableOneId));
ALTER TABLE TableOne_TableTwo ADD (CONSTRAINT FK_TableOne_TableTwo_2 FOREIGN KEY (TableTwoId) REFERENCES TableTwo (TableTwoId));
CREATE SEQUENCE hibernate_sequence NOCYCLE NOORDER CACHE 20 NOMAXVALUE MINVALUE 1 INCREMENT BY 1 START WITH 1000;
Mapping to the following set of classes:
Code:
package entity;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@SuppressWarnings("serial")
@Entity()
@Table(name = "TABLEONE", schema = "GREG")
public class TableOne implements Serializable {
@Id()
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "TABLEONEID", unique = true, nullable = false, precision = 20)
private Long tableOneId;
@Basic()
@Column(name = "DATA", nullable = false, length = 255)
private String data;
@OneToMany(mappedBy = "tableOne", cascade = CascadeType.ALL)
private Set<TableOneTwo> tableOneTwos;
public TableOne() {
tableOneTwos = new HashSet<TableOneTwo>(0);
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Long getTableOneId() {
return tableOneId;
}
public void setTableOneId(Long tableOneId) {
this.tableOneId = tableOneId;
}
public Set<TableOneTwo> getTableOneTwos() {
return tableOneTwos;
}
public void setTableOneTwos(Set<TableOneTwo> tableOneTwos) {
this.tableOneTwos = tableOneTwos;
}
}
package entity;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@SuppressWarnings("serial")
@Entity
@Table(name = "TABLETWO")
public class TableTwo implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "TABLETWOID", unique = true, nullable = false, precision = 20)
private Long tableTwoId;
@Basic
@Column(name = "DATA", nullable = false, length = 255)
private String data;
@OneToMany(mappedBy = "tableTwo", cascade = CascadeType.ALL)
private Set<TableOneTwo> tableOneTwos;
public TableTwo() {
tableOneTwos = new HashSet<TableOneTwo>(0);
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Set<TableOneTwo> getTableOneTwos() {
return tableOneTwos;
}
public void setTableOneTwos(Set<TableOneTwo> tableOneTwos) {
this.tableOneTwos = tableOneTwos;
}
public Long getTableTwoId() {
return tableTwoId;
}
public void setTableTwoId(Long tableTwoId) {
this.tableTwoId = tableTwoId;
}
}
package entity;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity()
@IdClass(TableOneTwoPK.class)
@Table(name = "TABLEONE_TABLETWO")
public class TableOneTwo {
@Id()
private Long tableOneId;
@Id()
private Long tableTwoId;
@Basic()
@Column(name = "DATA")
private String data;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "TABLEONEID", referencedColumnName = "TABLEONEID", nullable = false)
private TableOne tableOne;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "TABLETWOID", referencedColumnName = "TABLETWOID", nullable = false)
private TableTwo tableTwo;
public TableOneTwo() {
}
public Long getTableOneId() {
return tableOneId;
}
public void setTableOneId(Long tableOneId) {
this.tableOneId = tableOneId;
}
public Long getTableTwoId() {
return tableTwoId;
}
public void setTableTwoId(Long tableTwoId) {
this.tableTwoId = tableTwoId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public TableOne getTableOne() {
return tableOne;
}
public void setTableOne(TableOne tableOne) {
this.tableOne = tableOne;
}
public TableTwo getTableTwo() {
return tableTwo;
}
public void setTableTwo(TableTwo tableTwo) {
this.tableTwo = tableTwo;
}
}
package entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@SuppressWarnings("serial")
@Embeddable()
public class TableOneTwoPK implements Serializable {
@Column(nullable = false, insertable = false, updatable = false)
private Long tableOneId;
@Column(nullable = false, insertable = false, updatable = false)
private Long tableTwoId;
public TableOneTwoPK() {
}
public Long getTableOneId() {
return tableOneId;
}
public void setTableOneId(Long tableOneId) {
this.tableOneId = tableOneId;
}
public Long getTableTwoId() {
return tableTwoId;
}
public void setTableTwoId(Long tableTwoId) {
this.tableTwoId = tableTwoId;
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof TableOneTwoPK))
return false;
TableOneTwoPK other = (TableOneTwoPK) obj;
return this.tableOneId.equals(other.tableOneId) && this.tableTwoId.equals(other.tableTwoId);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
Using the following JUnit test harness:
Code:
package test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import junit.framework.TestCase;
import entity.TableOne;
import entity.TableOneTwo;
import entity.TableTwo;
public class TableOneTwoTest extends TestCase {
EntityManagerFactory emf;
EntityManager em;
@Override
protected void setUp() throws Exception {
emf = Persistence.createEntityManagerFactory("Test");
em = emf.createEntityManager();
}
public void testCreateEntities() {
EntityTransaction tx = em.getTransaction();
tx.begin();
TableOne tableOne = new TableOne();
tableOne.setData("Table One Data");
TableTwo tableTwo = new TableTwo();
tableTwo.setData("Table Two Data");
TableOneTwo tableOneTwo = new TableOneTwo();
tableOneTwo.setData("Table One & Two Data");
tableOne.getTableOneTwos().add(tableOneTwo);
tableTwo.getTableOneTwos().add(tableOneTwo);
tableOneTwo.setTableOne(tableOne);
tableOneTwo.setTableTwo(tableTwo);
em.persist(tableOne);
em.persist(tableTwo);
em.persist(tableOneTwo);
tx.commit();
}
public void testCreateSingleEntity() {
EntityTransaction tx = em.getTransaction();
tx.begin();
TableOne tableOne = em.find(TableOne.class, 10L);
TableTwo tableTwo = em.find(TableTwo.class, 10L);
TableOneTwo tableOneTwo = new TableOneTwo();
tableOneTwo.setData("Table One & Two Data");
tableOneTwo.setTableOne(tableOne);
tableOneTwo.setTableTwo(tableTwo);
em.persist(tableOneTwo);
tx.commit();
}
}
Using the above test harness results in a “duplicate column” exception from the DB as a result of the following generated SQL:
Quote:
/* insert entity.TableOneTwo
*/ insert
into
TABLEONE_TABLETWO
(DATA, TABLEONEID, TABLETWOID, tableOneId, tableTwoId)
values
(?, ?, ?, ?, ?)
15/10/2006 15:54:30 org.hibernate.util.JDBCExceptionReporter logExceptions
WARNING: SQL Error: 957, SQLState: 42000
15/10/2006 15:54:30 org.hibernate.util.JDBCExceptionReporter logExceptions
SEVERE: ORA-00957: duplicate column name
It seems to be ignoring the “updateable = false” in the TableOneTwoPK class. Interestingly enough if I include the “name” attribute in this class, then I get a “Repeated column in mapping” exception.
I’ve tried numerous approaches in the annotations, such as using an @Embedded compound key class. See the following code updates to the TableOneTwo class only:
Code:
package entity;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity()
@Table(name = "TABLEONE_TABLETWO")
public class TableOneTwo {
@Id()
private TableOneTwoPK Id;
@Basic()
@Column(name = "DATA")
private String data;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "TABLEONEID", referencedColumnName = "TABLEONEID", nullable = false)
private TableOne tableOne;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "TABLETWOID", referencedColumnName = "TABLETWOID", nullable = false)
private TableTwo tableTwo;
public TableOneTwo() {
}
public TableOneTwoPK getId() {
return Id;
}
public void setId(TableOneTwoPK id) {
Id = id;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public TableOne getTableOne() {
return tableOne;
}
public void setTableOne(TableOne tableOne) {
this.tableOne = tableOne;
}
public TableTwo getTableTwo() {
return tableTwo;
}
public void setTableTwo(TableTwo tableTwo) {
this.tableTwo = tableTwo;
}
}
Which results in the following error:
Quote:
“javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save(): entity2.TableOneTwo”
The only way I have been able to get this structure to persist is via the @Embedded PK annotation and removal of the Cascade settings, then to force multiple saves on each of the entities, eg a “save” on the TableOne and TableTwo classes so that they are assigned Id’s, which I then set into the TableOneTwo class then call save on it.
Is it possible to “Cascade” save this structure and have Hibernate resolve the generation and assignment of the generated Id’s into the Join table?