Enterprise Java Development@TOPIC@
In this chapter we will work thru several ways to relate two entities in a one-to-one relationship. As the name implies each side of the relationship has no more than one instance of the other. That sounds easy -- and it is if we keep in mind that this is a unique relationship (i.e., no other instance has it) from both sides.
Create a JUnit test class to host tests for the one-to-one mappings.
package myorg.relex;
import static org.junit.Assert.*;
import javax.persistence.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.*;
public class One2OneTest extends JPATestBase {
private static Log log = LogFactory.getLog(One2OneTest.class);
@Test
public void testSample() {
log.info("testSample");
}
}
Verify the new JUnit test class builds and executes to completion
relationEx]$ mvn clean test -P\!h2db -Ph2srv ... -HHH000401: using driver [org.h2.Driver] at URL [jdbc:h2:tcp://localhost:9092/h2db/ejava] ... [INFO] BUILD SUCCESS
package myorg.relex.one2one;
import javax.persistence.*;
/**
* Target of uni-directional relationship
*/
@Entity
@Table(name="RELATIONEX_PERSON")
public class Person {
@Id @GeneratedValue
private int id;
private String name;
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
package myorg.relex.one2one;
import javax.persistence.*;
/**
* Provides example of one-to-one unidirectional relationship
* using foreign key.
*/
@Entity
@Table(name="RELATIONEX_PLAYER")
public class Player {
public enum Position { DEFENSE, OFFENSE, SPECIAL_TEAMS};
@Id @GeneratedValue
private int id;
@Enumerated(EnumType.STRING)
@Column(length=16)
private Position position;
//@OneToOne
private Person person;
public int getId() { return id; }
public Person getPerson() { return person; }
public void setPerson(Person person) {
this.person = person;
}
public Position getPosition() { return position; }
public void setPosition(Position position) {
this.position = position;
}
}
Add the two entity classes to the persistence unit housed in src/test tree
<persistence-unit name="relationEx-test">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
...
<class>myorg.relex.one2one.Person</class>
<class>myorg.relex.one2one.Player</class>
...
</persistence-unit>
org.hibernate.MappingException: Could not determine type for: myorg.relex.one2one.Person, at table: RELATIONEX_PLAYER, for columns: [org.hibernate.mapping.Column(person).
Add a JPA @OneToOne relationship mapping from the Player to Person. Also include a definitions to...
@OneToOne(optional=false,fetch=FetchType.EAGER)
@JoinColumn(name="PERSON_ID")
private Person person;
Build the module and observe the database schema generated.
create table RELATIONEX_PERSON ( id integer generated by default as identity, name varchar(255), primary key (id) ); create table RELATIONEX_PLAYER ( id integer generated by default as identity, position varchar(16), PERSON_ID integer not null, primary key (id), unique (PERSON_ID) ); alter table RELATIONEX_PLAYER add constraint FK58E275714BE1E366 foreign key (PERSON_ID) references RELATIONEX_PERSON;
Add the following test method to your existing JUnit test case. It is currently incomplete.
@Test
public void testOne2OneUniFK() {
log.info("*** testOne2OneUniFK ***");
Person person = new Person();
person.setName("Johnny Unitas");
Player player = new Player();
player.setPerson(person);
player.setPosition(Player.Position.OFFENSE);
//em.persist(person);
em.persist(player); //provider will propagate person.id to player.FK
//clear the persistence context and get new instances
em.flush(); em.clear();
Player player2 = em.find(Player.class, player.getId());
assertEquals("unexpected position", player.getPosition(), player2.getPosition());
assertEquals("unexpected name", player.getPerson().getName(), player2.getPerson().getName());
}
Attempt to re-build the module and note the error.
./target/surefire-reports/myorg.relex.One2OneTest.txt :::::::::::::: ------------------------------------------------------------------------------- Test set: myorg.relex.One2OneTest ------------------------------------------------------------------------------- Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.874 sec <<< FAILURE! testOne2OneUniFK(myorg.relex.One2OneTest) Time elapsed: 0.171 sec <<< ERROR! java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: myorg.relex.one2one.Player.person -> myorg.relex.one2one.Person at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1358) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1289) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1295) at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:976) at myorg.relex.One2OneTest.testOne2OneUniFK(One2OneTest.java:29)
Update the test method to persist both the Person and Player prior to the flush.
em.persist(person);
em.persist(player);
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniFK ... -*** testOne2OneUniFK *** Hibernate: insert into RELATIONEX_PERSON (id, name) values (null, ?) Hibernate: insert into RELATIONEX_PLAYER (id, PERSON_ID, position) values (null, ?, ?) Hibernate: select player0_.id as id2_1_, player0_.PERSON_ID as PERSON3_2_1_, player0_.position as position2_1_, person1_.id as id1_0_, person1_.name as name1_0_ from RELATIONEX_PLAYER player0_ inner join RELATIONEX_PERSON person1_ on player0_.PERSON_ID=person1_.id where player0_.id=?
@OneToOne(optional=true,fetch=FetchType.EAGER)
@JoinColumn(name="PERSON_ID")
private Person person;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniFK ... Hibernate: select player0_.id as id2_1_, player0_.PERSON_ID as PERSON3_2_1_, player0_.position as position2_1_, person1_.id as id1_0_, person1_.name as name1_0_ from RELATIONEX_PLAYER player0_ left outer join RELATIONEX_PERSON person1_ on player0_.PERSON_ID=person1_.id where player0_.id=?
@OneToOne(optional=false,fetch=FetchType.LAZY)
@JoinColumn(name="PERSON_ID")
private Person person;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniFK ... Hibernate: select player0_.id as id2_0_, player0_.PERSON_ID as PERSON3_2_0_, player0_.position as position2_0_ from RELATIONEX_PLAYER player0_ where player0_.id=? Hibernate: <<<=== caused by player.getPerson().getName() select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=?
//assertEquals("unexpected name", player.getPerson().getName(), player2.getPerson().getName());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniFK ... Hibernate: select player0_.id as id2_0_, player0_.PERSON_ID as PERSON3_2_0_, player0_.position as position2_0_ from RELATIONEX_PLAYER player0_ where player0_.id=?
Object[] cols = (Object[]) em.createNativeQuery(
"select person.id person_id, person.name, " +
"player.id player_id, player.person_id player_person_id " +
"from RELATIONEX_PLAYER player " +
"join RELATIONEX_PERSON person on person.id = player.person_id " +
"where player.id = ?1")
.setParameter(1, player.getId())
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected person_id", person.getId(), ((Number)cols[0]).intValue());
assertEquals("unexpected person_name", person.getName(), (String)cols[1]);
assertEquals("unexpected player_id", player.getId(), ((Number)cols[2]).intValue());
assertEquals("unexpected player_person_id", person.getId(), ((Number)cols[3]).intValue());
Rebuild the module to verify the SQL mappings is what we expected.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniFK ... Hibernate: select person.id person_id, person.name, player.id player_id, player.person_id player_person_id from RELATIONEX_PLAYER player join RELATIONEX_PERSON person on person.id = player.person_id where player.id = ? -row=[1, Johnny Unitas, 1, 1]
//em.remove(player2);
em.remove(player2.getPerson());
em.flush();
assertNull("person not deleted", em.find(Person.class, person.getId()));
assertNull("player not deleted", em.find(Player.class, player.getId()));
.Hibernate: delete from RELATIONEX_PERSON where id=? /target/surefire-reports/myorg.relex.One2OneTest.txt :::::::::::::: ------------------------------------------------------------------------------- Test set: myorg.relex.One2OneTest ------------------------------------------------------------------------------- Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 3.551 sec <<< FAILURE! testOne2OneUniFK(myorg.relex.One2OneTest) Time elapsed: 1.103 sec <<< ERROR! javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Referential integrity constraint violation: "FK58E275714BE1E366: PUBLIC.RELATIONEX_PLAYER FOREIGN KEY(PERSON_ID) REFERENCES PUBLIC.RELATIONEX_PERSON(ID) (1)"; SQL statement: delete from RELATIONEX_PERSON where id=? [23503-168] at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1361) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1289) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1295) at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:976) at myorg.relex.One2OneTest.testOne2OneUniFK(One2OneTest.java:37)
Fix the problem by deleting the Player prior to the Person.
em.remove(player2); em.remove(player2.getPerson());
Hibernate: delete from RELATIONEX_PLAYER where id=? Hibernate: delete from RELATIONEX_PERSON where id=?
package myorg.relex.one2one;
import javax.persistence.*;
/**
* Provides example of one-to-one unidirectional relationship
* using join table.
*/
@Entity
@Table(name="RELATIONEX_MEMBER")
public class Member {
public enum Role { PRIMARY, SECONDARY};
@Id @GeneratedValue
private int id;
@OneToOne(optional=false,fetch=FetchType.EAGER)
/*@JoinTable(name="RELATIONEX_MEMBER_PERSON",
joinColumns={
@JoinColumn(name="MEMBER_ID", referencedColumnName="ID"),
}, inverseJoinColumns={
@JoinColumn(name="PERSON_ID", referencedColumnName="ID"),
}
)*/
private Person person;
@Enumerated(EnumType.STRING)
@Column(length=16)
private Role role;
protected Member() {}
public Member(Person person) {
this.person = person;
}
public int getId() { return id; }
public Person getPerson() { return person; }
public Role getRole() { return role; }
public void setRole(Role role) {
this.role = role;
}
}
Add the entity to the persistence unit
<class>myorg.relex.one2one.Member</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_MEMBER ( id integer generated by default as identity, role varchar(16), person_id integer not null, primary key (id), unique (person_id) ); ... alter table RELATIONEX_MEMBER add constraint FK5366652A4BE1E366 foreign key (person_id) references RELATIONEX_PERSON;
@OneToOne(optional=false,fetch=FetchType.EAGER)
@JoinTable(name="RELATIONEX_MEMBER_PERSON")/*,
joinColumns={
@JoinColumn(name="MEMBER_ID", referencedColumnName="ID"),
}, inverseJoinColumns={
@JoinColumn(name="PERSON_ID", referencedColumnName="ID"),
}
)*/
private Person person;
Re-build the module and observe the generated database schema for our new @JoinTable relationship.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_PERSON ( id integer generated by default as identity, name varchar(255), primary key (id) ); ... create table RELATIONEX_MEMBER ( id integer generated by default as identity, role varchar(16), primary key (id) ); create table RELATIONEX_MEMBER_PERSON ( person_id integer not null, id integer not null, primary key (id), unique (person_id) ); ... alter table RELATIONEX_MEMBER_PERSON add constraint FK3D65E40A13E64581 foreign key (id) references RELATIONEX_MEMBER; alter table RELATIONEX_MEMBER_PERSON add constraint FK3D65E40A4BE1E366 foreign key (person_id) references RELATIONEX_PERSON;
Finish the @JoinTable mapping by making the join table column mapping explicit.
@JoinTable(name="RELATIONEX_MEMBER_PERSON",
joinColumns={
@JoinColumn(name="MEMBER_ID", referencedColumnName="ID"),
}, inverseJoinColumns={
@JoinColumn(name="PERSON_ID", referencedColumnName="ID"),
}
)
private Person person;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_MEMBER_PERSON ( PERSON_ID integer not null, MEMBER_ID integer not null, primary key (MEMBER_ID), unique (PERSON_ID) );
Add the following test method to you existing one-to-one test case.
@Test
public void testOne2OneUniJoinTable() {
log.info("*** testOne2OneUniJoinTable ***");
Person person = new Person();
person.setName("Joe Smith");
Member member = new Member(person);
member.setRole(Member.Role.SECONDARY);
em.persist(person);
em.persist(member); //provider will propagate person.id to player.FK
//clear the persistence context and get new instances
em.flush(); em.clear();
Member member2 = em.find(Member.class, member.getId());
assertEquals("unexpected role", member.getRole(), member2.getRole());
assertEquals("unexpected name", member.getPerson().getName(), member2.getPerson().getName());
}
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniJoinTable ... -*** testOne2OneUniJoinTable *** Hibernate: insert into RELATIONEX_PERSON (id, name) values (null, ?) Hibernate: insert into RELATIONEX_MEMBER (id, role) values (null, ?) Hibernate: insert into RELATIONEX_MEMBER_PERSON (PERSON_ID, MEMBER_ID) values (?, ?) Hibernate: select member0_.id as id3_1_, member0_.role as role3_1_, member0_1_.PERSON_ID as PERSON1_4_1_, person1_.id as id1_0_, person1_.name as name1_0_ from RELATIONEX_MEMBER member0_ left outer join RELATIONEX_MEMBER_PERSON member0_1_ on member0_.id=member0_1_.MEMBER_ID inner join RELATIONEX_PERSON person1_ on member0_1_.PERSON_ID=person1_.id where member0_.id=?
@OneToOne(optional=true,fetch=FetchType.EAGER)
@JoinTable(name="RELATIONEX_MEMBER_PERSON",
Hibernate: select member0_.id as id3_1_, member0_.role as role3_1_, member0_1_.PERSON_ID as PERSON1_4_1_, person1_.id as id1_0_, person1_.name as name1_0_ from RELATIONEX_MEMBER member0_ left outer join RELATIONEX_MEMBER_PERSON member0_1_ on member0_.id=member0_1_.MEMBER_ID left outer join RELATIONEX_PERSON person1_ on member0_1_.PERSON_ID=person1_.id where member0_.id=?
@OneToOne(optional=false,fetch=FetchType.LAZY)
@JoinTable(name="RELATIONEX_MEMBER_PERSON",
Hibernate: select member0_.id as id3_0_, member0_.role as role3_0_, member0_1_.PERSON_ID as PERSON1_4_0_ from RELATIONEX_MEMBER member0_ left outer join RELATIONEX_MEMBER_PERSON member0_1_ on member0_.id=member0_1_.MEMBER_ID where member0_.id=?
Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=?
Object[] cols = (Object[]) em.createNativeQuery(
"select person.id person_id, person.name, " +
"member.id member_id, member.role member_role, " +
"link.member_id link_member, link.person_id link_person " +
"from RELATIONEX_MEMBER member " +
"join RELATIONEX_MEMBER_PERSON link on link.member_id = member.id " +
"join RELATIONEX_PERSON person on link.person_id = person.id " +
"where member.id = ?1")
.setParameter(1, member.getId())
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected person_id", person.getId(), ((Number)cols[0]).intValue());
assertEquals("unexpected person_name", person.getName(), (String)cols[1]);
assertEquals("unexpected member_id", member.getId(), ((Number)cols[2]).intValue());
assertEquals("unexpected member_role", member.getRole().name(), (String)cols[3]);
assertEquals("unexpected link_member_id", member.getId(), ((Number)cols[4]).intValue());
assertEquals("unexpected link_person_id", person.getId(), ((Number)cols[5]).intValue());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniJoinTable ... Hibernate: select person.id person_id, person.name, member.id member_id, member.role member_role, link.member_id link_member, link.person_id link_person from RELATIONEX_MEMBER member join RELATIONEX_MEMBER_PERSON link on link.member_id = member.id join RELATIONEX_PERSON person on link.person_id = person.id where member.id = ? -row=[1, Joe Smith, 1, SECONDARY, 1, 1]
Add the following cleanup to the test method.
em.remove(member2); em.remove(member2.getPerson()); em.flush(); assertNull("person not deleted", em.find(Person.class, person.getId())); assertNull("member not deleted", em.find(Member.class, member.getId()));
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniJoinTable ... Hibernate: delete from RELATIONEX_MEMBER_PERSON where MEMBER_ID=? Hibernate: delete from RELATIONEX_MEMBER where id=? Hibernate: delete from RELATIONEX_PERSON where id=? ...
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* Provides example of one-to-one unidirectional relationship
* using a primary key join.
*/
@Entity
@Table(name="RELATIONEX_EMPLOYEE")
public class Employee {
@Id //pk value must be assigned, not generated
private int id;
@OneToOne(optional=false,fetch=FetchType.EAGER)
@PrimaryKeyJoinColumn //informs provider the FK derived from PK
private Person person;
@Temporal(TemporalType.DATE)
private Date hireDate;
protected Employee() {}
public Employee(Person person) {
this.person = person;
if (person != null) { id = person.getId(); }
}
public int getId() { return person.getId(); }
public Person getPerson() { return person; }
public Date getHireDate() { return hireDate; }
public void setHireDate(Date hireDate) {
this.hireDate = hireDate;
}
}
Add the new entity class to the persistence unit.
<class>myorg.relex.one2one.Employee</class>
create table RELATIONEX_EMPLOYEE ( id integer not null, hireDate date, primary key (id) ); create table RELATIONEX_PERSON ( id integer generated by default as identity, name varchar(255), primary key (id) ); alter table RELATIONEX_EMPLOYEE add constraint FK813A593E1907563C foreign key (id) references RELATIONEX_PERSON;
@Test
public void testOne2OneUniPKJ() {
log.info("*** testOne2OneUniPKJ ***");
Person person = new Person();
person.setName("Ozzie Newsome");
//em.persist(person);
//em.flush(); //generate the PK for the person
Employee employee = new Employee(person);//set PK/FK -- provider will not auto propagate
employee.setHireDate(new GregorianCalendar(1996, Calendar.JANUARY, 1).getTime());
em.persist(person);
em.persist(employee);
//clear the persistence context and get new instances
em.flush(); em.clear();
Employee employee2 = em.find(Employee.class, employee.getPerson().getId());
log.info("calling person...");
assertEquals("unexpected name", employee.getPerson().getName(), employee2.getPerson().getName());
}
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniPKJ ... -*** testOne2OneUniPKJ *** Hibernate: insert into RELATIONEX_PERSON (id, name) values (null, ?) Hibernate: insert into RELATIONEX_EMPLOYEE (hireDate, id) values (?, ?) -SQL Error: 23506, SQLState: 23506 -Referential integrity constraint violation: "FK813A593E1907563C: PUBLIC.RELATIONEX_EMPLOYEE FOREIGN KEY(ID) REFERENCES PUBLIC.RELATIONEX_PERSON(ID) (0)"; SQL statement: insert into RELATIONEX_EMPLOYEE (hireDate, id) values (?, ?) [23506-168]
em.persist(person);
em.flush(); //generate the PK for the person
Employee employee = new Employee(person);//set PK/FK -- provider will not auto propagate
employee.setHireDate(new GregorianCalendar(1996, Calendar.JANUARY, 1).getTime());
//em.persist(person);
em.persist(employee);
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniPKJ ... -*** testOne2OneUniPKJ *** Hibernate: insert into RELATIONEX_PERSON (id, name) values (null, ?) Hibernate: insert into RELATIONEX_EMPLOYEE (hireDate, id) values (?, ?) ...
Hibernate: select employee0_.id as id6_0_, employee0_.hireDate as hireDate6_0_ from RELATIONEX_EMPLOYEE employee0_ where employee0_.id=? Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=? -calling person... ...
If you change the relationship to optional/EAGER, the select changes to a single outer join.
@OneToOne(optional=true,fetch=FetchType.EAGER)
@PrimaryKeyJoinColumn //informs provider the FK derived from PK
private Person person;
Hibernate: select employee0_.id as id6_1_, employee0_.hireDate as hireDate6_1_, person1_.id as id1_0_, person1_.name as name1_0_ from RELATIONEX_EMPLOYEE employee0_ left outer join RELATIONEX_PERSON person1_ on employee0_.id=person1_.id where employee0_.id=? -calling person...
@OneToOne(optional=false,fetch=FetchType.LAZY)
@PrimaryKeyJoinColumn //informs provider the FK derived from PK
private Person person;
Hibernate: select employee0_.id as id6_0_, employee0_.hireDate as hireDate6_0_ from RELATIONEX_EMPLOYEE employee0_ where employee0_.id=? -calling person... Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=?
@OneToOne(optional=true,fetch=FetchType.LAZY)
@PrimaryKeyJoinColumn //informs provider the FK derived from PK
private Person person;
Hibernate: select employee0_.id as id6_0_, employee0_.hireDate as hireDate6_0_ from RELATIONEX_EMPLOYEE employee0_ where employee0_.id=? Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=? -calling person...
Object[] cols = (Object[]) em.createNativeQuery(
"select person.id person_id, person.name, " +
"employee.id employee_id " +
"from RELATIONEX_EMPLOYEE employee " +
"join RELATIONEX_PERSON person on person.id = employee.id " +
"where employee.id = ?1")
.setParameter(1, employee.getId())
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected person_id", person.getId(), ((Number)cols[0]).intValue());
assertEquals("unexpected person_name", person.getName(), (String)cols[1]);
assertEquals("unexpected employee_id", employee.getId(), ((Number)cols[2]).intValue());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniPKJ ... Hibernate: select person.id person_id, person.name, employee.id employee_id from RELATIONEX_EMPLOYEE employee join RELATIONEX_PERSON person on person.id = employee.id where employee.id = ? -row=[1, Ozzie Newsome, 1]
em.remove(employee2);
em.remove(employee2.getPerson());
em.flush();
assertNull("person not deleted", em.find(Person.class, person.getId()));
assertNull("employee not deleted", em.find(Employee.class, employee.getId()));
Re-build the module and verify the ability to delete the dependent and parent entities.
Hibernate: delete from RELATIONEX_EMPLOYEE where id=? Hibernate: delete from RELATIONEX_PERSON where id=?
Add the following entity class to your src/main tree. It is incomplete at this point in time.
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This class demonstrates a one-to-one, uni-directional relationship
* where the foreign key is used to define the primary key with the
* use of @MapsId
*/
@Entity
@Table(name="RELATIONEX_COACH")
public class Coach {
public enum Type {HEAD, ASSISTANT };
@Id //provider sets to FK value with help from @MapsId
private int id;
@OneToOne(optional=false, fetch=FetchType.EAGER)
// @MapsId //informs provider the PK is derived from FK
private Person person;
@Enumerated(EnumType.STRING) @Column(length=16)
private Type type;
public Coach() {}
public Coach(Person person) {
this.person = person;
}
public int getId() { return person==null ? 0 : person.getId(); }
public Person getPerson() { return person; }
public Type getType() { return type; }
public void setType(Type type) {
this.type = type;
}
}
Add the entity class to the persistence unit.
<class>myorg.relex.one2one.Coach</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_COACH ( id integer not null, type varchar(16), person_id integer not null, primary key (id), unique (person_id) ); ... alter table RELATIONEX_COACH add constraint FK75C513EA4BE1E366 foreign key (person_id) references RELATIONEX_PERSON;
@Id //provider sets to FK value with help from @MapsId
private int id;
@OneToOne(optional=false, fetch=FetchType.EAGER)
@MapsId //informs provider the PK is derived from FK
private Person person;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_COACH ( type varchar(16), person_id integer not null, primary key (person_id), unique (person_id) ); ... alter table RELATIONEX_COACH add constraint FK75C513EA4BE1E366 foreign key (person_id) references RELATIONEX_PERSON;
@Test
public void testOne2OneUniMapsId() {
log.info("*** testOne2OneUniMapsId ***");
Person person = new Person();
person.setName("John Harbaugh");
Coach coach = new Coach(person);
coach.setType(Coach.Type.HEAD);
em.persist(person);
em.persist(coach); //provider auto propagates person.id to coach.FK mapped to coach.PK
//flush commands to database, clear cache, and pull back new instance
em.flush(); em.clear();
Coach coach2 = em.find(Coach.class, coach.getId());
log.info("calling person...");
assertEquals("unexpected name", coach.getPerson().getName(), coach2.getPerson().getName());
}
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniMapsId ... -*** testOne2OneUniMapsId *** Hibernate: insert into RELATIONEX_PERSON (id, name) values (null, ?) Hibernate: insert into RELATIONEX_COACH (type, person_id) values (?, ?) Hibernate: select coach0_.person_id as person2_5_0_, coach0_.type as type5_0_ from RELATIONEX_COACH coach0_ where coach0_.person_id=? Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=? -calling person...
Add the following assertions about the SQL structure and values.
Object[] cols = (Object[]) em.createNativeQuery(
"select person.id person_id, person.name, " +
"coach.person_id coach_id " +
"from RELATIONEX_COACH coach " +
"join RELATIONEX_PERSON person on person.id = coach.person_id " +
"where coach.person_id = ?1")
.setParameter(1, coach.getId())
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected person_id", person.getId(), ((Number)cols[0]).intValue());
assertEquals("unexpected person_name", person.getName(), (String)cols[1]);
assertEquals("unexpected coach_id", coach.getId(), ((Number)cols[2]).intValue());
Rebuild the module, re-run the test method, and observe the results of the new assertions.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniMapsId ... Hibernate: select person.id person_id, person.name, coach.person_id coach_id from RELATIONEX_COACH coach join RELATIONEX_PERSON person on person.id = coach.person_id where coach.person_id = ? -row=[1, John Harbaugh, 1]
Add cleanup logic and assertions of the removal of the two entity rows.
em.remove(coach2);
em.remove(coach2.getPerson());
em.flush();
assertNull("person not deleted", em.find(Person.class, person.getId()));
assertNull("coach not deleted", em.find(Coach.class, coach.getId()));
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniMapsId ... Hibernate: delete from RELATIONEX_COACH where person_id=? Hibernate: delete from RELATIONEX_PERSON where id=?
To get started, put the following parent class in place in your src/main tree.
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class represents the passive side of a one-to-one
* uni-directional relationship where the parent uses
* a composite primary key that must be represented in
* the dependent entity's relationship.
*/
@Entity
@Table(name="RELATIONEX_SHOWEVENT")
@IdClass(ShowEventPK.class)
public class ShowEvent {
@Id
@Temporal(TemporalType.DATE)
private Date date;
@Id
@Temporal(TemporalType.TIME)
private Date time;
@Column(length=20)
private String name;
public ShowEvent() {}
public ShowEvent(Date date, Date time) {
this.date = date;
this.time = time;
}
public Date getDate() { return date; }
public Date getTime() { return time; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
package myorg.relex.one2one;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Embeddable;
/**
* This class will be used as an IdClass for the ShowEvent
* entity.
*/
@Embeddable
public class ShowEventPK implements Serializable {
private static final long serialVersionUID = 1L;
private Date date;
private Date time;
protected ShowEventPK(){}
public ShowEventPK(Date date, Date time) {
this.date = date;
this.time = time;
}
public Date getDate() { return date; }
public Date getTime() { return time; }
@Override
public int hashCode() { return date.hashCode() + time.hashCode(); }
@Override
public boolean equals(Object obj) {
try {
return date.equals(((ShowEventPK)obj).date) &&
time.equals(((ShowEventPK)obj).time);
} catch (Exception ex) { return false; }
}
}
Add the above parent entity to the persistence unit.
<class>myorg.relex.one2one.ShowEvent</class>
This sub-section will map the dependent class to the parent using an @IdClass.
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class provides an example of a the owning entity of a
* one-to-one, uni-directional relationship where the dependent's
* primary key is derived from the parent and the parent uses
* a composite primary key.
*/
@Entity
@Table(name="RELATIONEX_SHOWTICKETS")
@IdClass(ShowEventPK.class)
public class ShowTickets {
@Id
@Temporal(TemporalType.DATE)
@Column(name="TICKET_DATE")
private Date date;
@Id
@Temporal(TemporalType.TIME)
@Column(name="TICKET_TIME")
private Date time;
@OneToOne(optional=false, fetch=FetchType.EAGER)
/*
@PrimaryKeyJoinColumns({
@PrimaryKeyJoinColumn(name="TICKET_DATE", referencedColumnName="date"),
@PrimaryKeyJoinColumn(name="TICKET_TIME", referencedColumnName="time"),
})
*/
private ShowEvent show;
@Column(name="TICKETS")
private int ticketsLeft;
public ShowTickets() {}
public ShowTickets(ShowEvent show) {
this.date = show.getDate();
this.time = show.getTime();
this.show = show;
}
public Date getDate() { return show==null ? null : show.getDate(); }
public Date getTime() { return show==null ? null : show.getTime(); }
public ShowEvent getShow() { return show; }
public int getTicketsLeft() { return ticketsLeft; }
public void setTicketsLeft(int ticketsLeft) {
this.ticketsLeft = ticketsLeft;
}
}
Add the dependent entity class to the persistence unit.
<class>myorg.relex.one2one.ShowTickets</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_SHOWEVENT ( date date not null, time time not null, name varchar(20), primary key (date, time) ); create table RELATIONEX_SHOWTICKETS ( TICKET_DATE date not null, TICKET_TIME time not null, TICKETS integer, show_date date not null, show_time time not null, primary key (TICKET_DATE, TICKET_TIME), unique (show_date, show_time) ); ... alter table RELATIONEX_SHOWTICKETS add constraint FK93AB7C9AE3196D0 foreign key (show_date, show_time) references RELATIONEX_SHOWEVENT;
Update the relationship with a default mapping for a @PrimaryKeyJoin.
@OneToOne(optional=false, fetch=FetchType.EAGER)
@PrimaryKeyJoinColumn /*s({
@PrimaryKeyJoinColumn(name="TICKET_DATE", referencedColumnName="date"),
@PrimaryKeyJoinColumn(name="TICKET_TIME", referencedColumnName="time"),
})*/
private ShowEvent show;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_SHOWTICKETS ( TICKET_DATE date not null, TICKET_TIME time not null, TICKETS integer, primary key (TICKET_DATE, TICKET_TIME) ); ... alter table RELATIONEX_SHOWTICKETS add constraint FK93AB7C9A1C31D972 foreign key (TICKET_DATE, TICKET_TIME) references RELATIONEX_SHOWEVENT;
@OneToOne(optional=false, fetch=FetchType.EAGER)
@PrimaryKeyJoinColumns({
@PrimaryKeyJoinColumn(name="TICKET_DATE", referencedColumnName="date"),
@PrimaryKeyJoinColumn(name="TICKET_TIME", referencedColumnName="time"),
})
private ShowEvent show;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... TICKET_DATE date not null, TICKET_TIME time not null, TICKETS integer, primary key (TICKET_DATE, TICKET_TIME) ); ... alter table RELATIONEX_SHOWTICKETS add constraint FK93AB7C9A1C31D972 foreign key (TICKET_DATE, TICKET_TIME) references RELATIONEX_SHOWEVENT;
@Test
public void testOne2OneUniIdClass() {
log.info("*** testOneToOneUniIdClass ***");
Date showDate = new GregorianCalendar(1975+new Random().nextInt(100),
Calendar.JANUARY, 1).getTime();
Date showTime = new GregorianCalendar(0, 0, 0, 0, 0, 0).getTime();
ShowEvent show = new ShowEvent(showDate, showTime);
show.setName("Rocky Horror");
ShowTickets tickets = new ShowTickets(show); //parent already has natural PK by this point
tickets.setTicketsLeft(300);
em.persist(show);
em.persist(tickets);
//flush commands to database, clear cache, and pull back new instance
em.flush(); em.clear();
ShowTickets tickets2 = em.find(ShowTickets.class, new ShowEventPK(tickets.getDate(), tickets.getTime()));
log.info("calling parent...");
assertEquals("unexpected name", tickets.getShow().getName(), tickets2.getShow().getName());
}
Re-build the module and note the creation of the parent and dependent entities.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniIdClass ... -*** testOne2OneUniIdClass *** Hibernate: insert into RELATIONEX_SHOWEVENT (name, date, time) values (?, ?, ?) Hibernate: insert into RELATIONEX_SHOWTICKETS (TICKETS, TICKET_DATE, TICKET_TIME) values (?, ?, ?)
The provider uses a set of selects to fully assemble our object tree for use.
Hibernate: select showticket0_.TICKET_DATE as TICKET1_8_0_, showticket0_.TICKET_TIME as TICKET2_8_0_, showticket0_.TICKETS as TICKETS8_0_ from RELATIONEX_SHOWTICKETS showticket0_ where showticket0_.TICKET_DATE=? and showticket0_.TICKET_TIME=? Hibernate: select showevent0_.date as date7_0_, showevent0_.time as time7_0_, showevent0_.name as name7_0_ from RELATIONEX_SHOWEVENT showevent0_ where showevent0_.date=? and showevent0_.time=? -calling parent...
Object[] cols = (Object[]) em.createNativeQuery(
"select show.date show_date, show.time show_time, " +
"tickets.ticket_date ticket_date, tickets.ticket_time ticket_time, tickets.tickets " +
"from RELATIONEX_SHOWEVENT show " +
"join RELATIONEX_SHOWTICKETS tickets on show.date = tickets.ticket_date and show.time = tickets.ticket_time " +
"where tickets.ticket_date = ?1 and tickets.ticket_time = ?2")
.setParameter(1, tickets.getShow().getDate(), TemporalType.DATE)
.setParameter(2, tickets.getShow().getTime(), TemporalType.TIME)
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected show_date", tickets2.getShow().getDate(), (Date)cols[0]);
assertEquals("unexpected show_time", tickets2.getShow().getTime(), (Date)cols[1]);
assertEquals("unexpected ticket_date", tickets2.getDate(), (Date)cols[2]);
assertEquals("unexpected ticket_time", tickets2.getTime(), (Date)cols[3]);
assertEquals("unexpected ticketsLeft", tickets2.getTicketsLeft(), ((Number)cols[4]).intValue());
Re-build the module and observe the success of the SQL portion of the test method.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniIdClass ... Hibernate: select show.date show_date, show.time show_time, tickets.ticket_date ticket_date, tickets.ticket_time ticket_time, tickets.tickets from RELATIONEX_SHOWEVENT show join RELATIONEX_SHOWTICKETS tickets on show.date = tickets.ticket_date and show.time = tickets.ticket_time where tickets.ticket_date = ? and tickets.ticket_time = ? -row=[2033-01-01, 00:00:00, 2033-01-01, 00:00:00, 300]
em.remove(tickets2);
em.remove(tickets2.getShow());
em.flush();
assertNull("tickets not deleted", em.find(ShowEvent.class,
new ShowEventPK(show.getDate(), show.getTime())));
assertNull("show not deleted", em.find(ShowTickets.class,
new ShowEventPK(tickets.getDate(), tickets.getTime())));
Re-build the module and observe the successful results of the completed test method.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniIdClass ... Hibernate: delete from RELATIONEX_SHOWTICKETS where TICKET_DATE=? and TICKET_TIME=? Hibernate: delete from RELATIONEX_SHOWEVENT where date=? and time=?
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class provides an example of a the owning entity of a
* one-to-one, uni-directional relationship where the dependent's
* primary key is derived from the parent, the parent uses
* a composite primary key, and the dependent used an @EmeddedId
* and @MapsId.
*/
@Entity
@Table(name="RELATIONEX_BOXOFFICE")
public class BoxOffice {
@EmbeddedId
private ShowEventPK pk; //will be set by provider with help of @MapsId
@OneToOne(optional=false, fetch=FetchType.EAGER)
// @MapsId //provider maps this composite FK to @EmbeddedId PK value
private ShowEvent show;
@Column(name="TICKETS")
private int ticketsLeft;
protected BoxOffice() {}
public BoxOffice(ShowEvent show) {
this.show = show;
}
public Date getDate() { return show==null ? null : show.getDate(); }
public Date getTime() { return show==null ? null : show.getTime(); }
public ShowEvent getShow() { return show; }
public int getTicketsLeft() { return ticketsLeft; }
public void setTicketsLeft(int ticketsLeft) {
this.ticketsLeft = ticketsLeft;
}
}
Add the dependent entity class to the persistence unit.
<class>myorg.relex.one2one.BoxOffice</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BOXOFFICE ( date timestamp not null, time timestamp not null, TICKETS integer, show_date date not null, show_time time not null, primary key (date, time), unique (show_date, show_time) ); alter table RELATIONEX_BOXOFFICE add constraint FK64CED797E3196D0 foreign key (show_date, show_time) references RELATIONEX_SHOWEVENT;
@OneToOne(optional=false, fetch=FetchType.EAGER)
@MapsId //provider maps this composite FK to @EmbeddedId PK value
private ShowEvent show;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BOXOFFICE ( TICKETS integer, show_date date, show_time time not null, primary key (show_date, show_time), unique (show_date, show_time) ); alter table RELATIONEX_BOXOFFICE add constraint FK64CED797E3196D0 foreign key (show_date, show_time) references RELATIONEX_SHOWEVENT;
Add the following test method to your one-to-one test case.
@Test
public void testOne2OneUniEmbeddedId() {
log.info("*** testOne2OneUniEmbedded ***");
Date showDate = new GregorianCalendar(1975+new Random().nextInt(100),
Calendar.JANUARY, 1).getTime();
Date showTime = new GregorianCalendar(0, 0, 0, 0, 0, 0).getTime();
ShowEvent show = new ShowEvent(showDate, showTime);
show.setName("Rocky Horror");
BoxOffice boxOffice = new BoxOffice(show);
boxOffice.setTicketsLeft(500);
em.persist(show);
em.persist(boxOffice); //provider auto propagates parent.cid to dependent.FK mapped to dependent.cid
//flush commands to database, clear cache, and pull back new instance
em.flush(); em.clear();
BoxOffice boxOffice2 = em.find(BoxOffice.class, new ShowEventPK(boxOffice.getDate(), boxOffice.getTime()));
log.info("calling parent...");
assertEquals("unexpected name", boxOffice.getShow().getName(), boxOffice2.getShow().getName());
}
Re-build the module and run the test method above.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneUniEmbeddedId
...
-*** testOne2OneUniEmbedded ***
Hibernate:
insert
into
RELATIONEX_SHOWEVENT
(name, date, time)
values
(?, ?, ?)
Hibernate:
insert
into
RELATIONEX_BOXOFFICE
(TICKETS, show_date, show_time)
values
(?, ?, ?)
Hibernate:
select
boxoffice0_.show_date as show2_9_0_,
boxoffice0_.show_time as show3_9_0_,
boxoffice0_.TICKETS as TICKETS9_0_
from
RELATIONEX_BOXOFFICE boxoffice0_
where
boxoffice0_.show_date=?
and boxoffice0_.show_time=?
Hibernate:
select
showevent0_.date as date7_0_,
showevent0_.time as time7_0_,
showevent0_.name as name7_0_
from
RELATIONEX_SHOWEVENT showevent0_
where
showevent0_.date=?
and showevent0_.time=?
-calling parent...
Object[] cols = (Object[]) em.createNativeQuery(
"select show.date show_date, show.time show_time, " +
"tickets.show_date ticket_date, tickets.show_time ticket_time, tickets.tickets " +
"from RELATIONEX_SHOWEVENT show " +
"join RELATIONEX_BOXOFFICE tickets on show.date = tickets.show_date and show.time = tickets.show_time " +
"where tickets.show_date = ?1 and tickets.show_time = ?2")
.setParameter(1, boxOffice.getShow().getDate(), TemporalType.DATE)
.setParameter(2, boxOffice.getShow().getTime(), TemporalType.TIME)
.getSingleResult();
log.info("row=" + Arrays.toString(cols));
assertEquals("unexpected show_date", boxOffice2.getShow().getDate(), (Date)cols[0]);
assertEquals("unexpected show_time", boxOffice2.getShow().getTime(), (Date)cols[1]);
assertEquals("unexpected ticket_date", boxOffice2.getDate(), (Date)cols[2]);
assertEquals("unexpected ticket_time", boxOffice2.getTime(), (Date)cols[3]);
assertEquals("unexpected ticketsLeft", boxOffice2.getTicketsLeft(), ((Number)cols[4]).intValue());
Hibernate: select show.date show_date, show.time show_time, tickets.show_date ticket_date, tickets.show_time ticket_time, tickets.tickets from RELATIONEX_SHOWEVENT show join RELATIONEX_BOXOFFICE tickets on show.date = tickets.show_date and show.time = tickets.show_time where tickets.show_date = ? and tickets.show_time = ? -row=[1994-01-01, 00:00:00, 1994-01-01, 00:00:00, 500]
em.remove(boxOffice2);
em.remove(boxOffice2.getShow());
em.flush();
assertNull("tickets not deleted", em.find(ShowEvent.class,
new ShowEventPK(show.getDate(), show.getTime())));
assertNull("show not deleted", em.find(BoxOffice.class,
new ShowEventPK(boxOffice.getDate(), boxOffice.getTime())));
Observe the output of the deletes. It is consistent with before.
Hibernate: delete from RELATIONEX_BOXOFFICE where show_date=? and show_time=? Hibernate: delete from RELATIONEX_SHOWEVENT where date=? and time=?
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This class provides an example of the inverse side of a
* one-to-one bi-directional relationship.
*/
@Entity
@Table(name="RELATIONEX_APPLICANT")
public class Applicant {
@Id @GeneratedValue
private int id;
@Column(length=32)
private String name;
// @OneToOne(mappedBy="applicant", //identifies property on owning side
// fetch=FetchType.LAZY)
// @Transient
private Application application;
public Applicant(){}
public Applicant(int id) {
this.id = id;
}
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public Application getApplication() { return application; }
public void setApplication(Application application) {
this.application = application;
}
}
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class provides an example of the owning side
* of a one-to-one, bi-directional relationship.
*/
@Entity
@Table(name="RELATIONEX_APPLICATION")
public class Application {
@Id
private int id;
// @MapsId //foreign key realizes primary key
@OneToOne(//lack of mappedBy identifies this as owning side
optional=false, fetch=FetchType.EAGER)
private Applicant applicant;
@Temporal(TemporalType.DATE)
private Date desiredStartDate;
protected Application() {}
public Application(Applicant applicant) {
this.applicant = applicant;
if (applicant != null) {
applicant.setApplication(this); //must maintain inverse side
}
}
public int getId() { return id; }
public Applicant getApplicant() { return applicant; }
public Date getDesiredStartDate() { return desiredStartDate; }
public void setDesiredStartDate(Date desiredStartDate) {
this.desiredStartDate = desiredStartDate;
}
}
Add the two entity classes to your persistence unit.
<class>myorg.relex.one2one.Applicant</class>
<class>myorg.relex.one2one.Application</class>
$ mvn clean process-test-classes ... org.hibernate.MappingException: Could not determine type for: myorg.relex.one2one.Application, at table: RELATIONEX_APPLICANT, for columns: [org.hibernate.mapping.Column(application)]
@Transient
private Application application;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_APPLICANT ( id integer generated by default as identity, name varchar(32), primary key (id) ); create table RELATIONEX_APPLICATION ( id integer not null, desiredStartDate date, applicant_id integer not null, primary key (id), unique (applicant_id) ); ... alter table RELATIONEX_APPLICATION add constraint FK8B404CA01EF7E92E foreign key (applicant_id) references RELATIONEX_APPLICANT;
@MapsId //foreign key realizes primary key
@OneToOne(//lack of mappedBy identifies this as owning side
optional=false, fetch=FetchType.EAGER)
private Applicant applicant;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_APPLICANT ( id integer generated by default as identity, name varchar(32), primary key (id) ); create table RELATIONEX_APPLICATION ( desiredStartDate date, applicant_id integer not null, primary key (applicant_id), unique (applicant_id) ); ... alter table RELATIONEX_APPLICATION add constraint FK8B404CA01EF7E92E foreign key (applicant_id) references RELATIONEX_APPLICANT;
@OneToOne(
// mappedBy="applicant", //identifies property on owning side
fetch=FetchType.LAZY)
// @Transient
private Application application;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_APPLICANT ( id integer generated by default as identity, name varchar(32), application_applicant_id integer, primary key (id) ); create table RELATIONEX_APPLICATION ( desiredStartDate date, applicant_id integer not null, primary key (applicant_id), unique (applicant_id) ); ... alter table RELATIONEX_APPLICANT add constraint FK8C43FE52AB28790B foreign key (application_applicant_id) references RELATIONEX_APPLICATION; alter table RELATIONEX_APPLICATION add constraint FK8B404CA01EF7E92E foreign key (applicant_id) references RELATIONEX_APPLICANT;
@OneToOne(
mappedBy="applicant", //identifies property on owning side
fetch=FetchType.LAZY)
private Application application;
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_APPLICANT ( id integer generated by default as identity, name varchar(32), primary key (id) ); create table RELATIONEX_APPLICATION ( desiredStartDate date, applicant_id integer not null, primary key (applicant_id), unique (applicant_id) ); ... alter table RELATIONEX_APPLICATION add constraint FK8B404CA01EF7E92E foreign key (applicant_id) references RELATIONEX_APPLICANT;
Add the following test method to your existing one-to-one test case.
@Test
public void testOne2OneBiPKJ() {
log.info("*** testOne2OneBiPKJ() ***");
Applicant applicant = new Applicant();
applicant.setName("Jason Garret");
Application application = new Application(applicant);
application.setDesiredStartDate(new GregorianCalendar(2008, Calendar.JANUARY, 1).getTime());
em.persist(applicant); //provider will generate a PK
em.persist(application); //provider will propogate parent.PK to dependent.FK/PK
}
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiPKJ ... -*** testOne2OneBiPKJ() ***
Hibernate: insert into RELATIONEX_APPLICANT (id, name) values (null, ?) Hibernate: insert into RELATIONEX_APPLICATION (desiredStartDate, applicant_id) values (?, ?)
em.flush(); em.clear();
log.info("finding dependent...");
Application application2 = em.find(Application.class, application.getId());
log.info("found dependent...");
assertTrue("unexpected startDate",
application.getDesiredStartDate().equals(
application2.getDesiredStartDate()));
log.info("calling parent...");
assertEquals("unexpected name", application.getApplicant().getName(), application2.getApplicant().getName());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiPKJ ... -finding dependent... Hibernate: select applicatio0_.applicant_id as applicant2_11_0_, applicatio0_.desiredStartDate as desiredS1_11_0_ from RELATIONEX_APPLICATION applicatio0_ where applicatio0_.applicant_id=? Hibernate: select applicant0_.id as id10_0_, applicant0_.name as name10_0_ from RELATIONEX_APPLICANT applicant0_ where applicant0_.id=? Hibernate: select applicatio0_.applicant_id as applicant2_11_0_, applicatio0_.desiredStartDate as desiredS1_11_0_ from RELATIONEX_APPLICATION applicatio0_ where applicatio0_.applicant_id=? -found dependent... -calling parent...
em.flush(); em.clear();
log.info("finding parent...");
Applicant applicant2 = em.find(Applicant.class, applicant.getId());
log.info("found parent...");
assertEquals("unexpected name", applicant.getName(), applicant2.getName());
log.info("calling dependent...");
assertTrue("unexpected startDate",
applicant.getApplication().getDesiredStartDate().equals(
applicant2.getApplication().getDesiredStartDate()));
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiPKJ ... -finding parent... Hibernate: select applicant0_.id as id10_0_, applicant0_.name as name10_0_ from RELATIONEX_APPLICANT applicant0_ where applicant0_.id=? Hibernate: select applicatio0_.applicant_id as applicant2_11_0_, applicatio0_.desiredStartDate as desiredS1_11_0_ from RELATIONEX_APPLICATION applicatio0_ where applicatio0_.applicant_id=? -found parent... -calling dependent...
Add the following to the test method to verify delete actions.
em.remove(applicant2.getApplication());
em.remove(applicant2);
em.flush();
assertNull("applicant not deleted", em.find(Applicant.class, applicant2.getId()));
assertNull("application not deleted", em.find(Application.class, applicant2.getApplication().getId()));
Re-build the module and notice the successful deletion.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiPKJ ... Hibernate: delete from RELATIONEX_APPLICATION where applicant_id=? Hibernate: delete from RELATIONEX_APPLICANT where id=?
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This class is an example of the inverse/parent side of a one-to-one,
* bi-directional relationship that allows 0..1 and changing related entities.
*/
@Entity(name="RelationAuto")
@Table(name="RELATIONEX_AUTO")
public class Auto {
public enum Type { CAR, TRUCK };
@Id @GeneratedValue
private int id;
@Enumerated(EnumType.STRING)
@Column(length=10)
private Type type;
@OneToOne(
// mappedBy="auto",
optional=true, fetch=FetchType.LAZY)
private Driver driver;
public Auto() {}
public int getId() { return id;}
public Type getType() { return type; }
public void setType(Type type) {
this.type = type;
}
public Driver getDriver() { return driver; }
public void setDriver(Driver driver) {
this.driver = driver;
}
}
Add the following owning/dependent entity class to the src/main tree.
package myorg.relex.one2one; import javax.persistence.*; /** * This class provides an example of the owning/dependent side of a one-to-one * relationship where the inverse/parent represents a 0..1 or changing relation. */ @Entity @Table(name="RELATIONEX_DRIVER") public class Driver { @Id @GeneratedValue private int id; @Column(length=20) private String name; @OneToOne( optional=false, //we must have the auto for this driver fetch=FetchType.EAGER) private Auto auto; protected Driver() {} public Driver(Auto auto) { this.auto = auto; } public int getId() { return id; } public Auto getAuto() { return auto; } public void setAuto(Auto auto) { //drivers can switch Autos this.auto = auto; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
Add the two entity classes to the persistence unit
<class>myorg.relex.one2one.Auto</class>
<class>myorg.relex.one2one.Driver</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_AUTO ( id integer generated by default as identity, type varchar(10), driver_id integer, <!!!!==== this should not bet here primary key (id) ); ... create table RELATIONEX_DRIVER ( id integer generated by default as identity, name varchar(20), auto_id integer not null, primary key (id), unique (auto_id) ); ... alter table RELATIONEX_AUTO <!!!!==== this should not bet here add constraint FK3558203FB3D04E86 foreign key (driver_id) references RELATIONEX_DRIVER; ... alter table RELATIONEX_DRIVER add constraint FK44C072B81E349026 foreign key (auto_id) references RELATIONEX_AUTO;
@OneToOne(
mappedBy="auto",
optional=true, fetch=FetchType.LAZY)
private Driver driver;
Re-build the module and notice the correct schema produced.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_AUTO ( id integer generated by default as identity, type varchar(10), primary key (id) ); ... create table RELATIONEX_DRIVER ( id integer generated by default as identity, name varchar(20), auto_id integer not null, primary key (id), unique (auto_id) ); ... alter table RELATIONEX_DRIVER add constraint FK44C072B81E349026 foreign key (auto_id) references RELATIONEX_AUTO;
@Test
public void testOne2OneBiOwningOptional() {
log.info("*** testOne2OneBiOwningOptional() ***");
Auto auto = new Auto(); //auto is inverse/parent side
auto.setType(Auto.Type.CAR);
Driver driver = new Driver(auto); //driver is owning/dependent side
driver.setName("Danica Patrick");
auto.setDriver(driver); //application must maintain inverse side
em.persist(driver);
em.persist(auto);
}
Attempt to build the module, execute the new test method, and observe the error.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiOwningOptional ... -*** testOne2OneBiOwningOptional() *** Hibernate: insert into RELATIONEX_DRIVER (id, auto_id, name) values (null, ?, ?) -SQL Error: 23502, SQLState: 23502 -NULL not allowed for column "AUTO_ID"; SQL statement: insert into RELATIONEX_DRIVER (id, auto_id, name) values (null, ?, ?) [23502-168] -tearDown() started, em=org.hibernate.ejb.EntityManagerImpl@25824994 -tearDown() complete, em=org.hibernate.ejb.EntityManagerImpl@25824994 -closing entity manager factory -HHH000030: Cleaning up connection pool [jdbc:h2:tcp://localhost:9092/h2db/ejava] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 3.23 sec <<< FAILURE! Results : Tests in error: testOne2OneBiOwningOptional(myorg.relex.One2OneTest): org.hibernate.exception.ConstraintViolationException: NULL not allowed for column "AUTO_ID"; SQL statement:(..)
em.persist(auto);
em.persist(driver);
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiOwningOptional ... -*** testOne2OneBiOwningOptional() *** Hibernate: insert into RELATIONEX_AUTO (id, type) values (null, ?) Hibernate: insert into RELATIONEX_DRIVER (id, auto_id, name) values (null, ?, ?)
Add the following to access the pair through the owning/dependent side of the relation.
em.flush(); em.clear();
log.info("finding dependent...");
Driver driver2 = em.find(Driver.class, driver.getId());
log.info("found dependent...");
assertEquals("unexpected name", driver.getName(), driver2.getName());
log.info("calling parent...");
assertEquals("unexpected name", driver.getAuto().getType(), driver2.getAuto().getType());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiOwningOptional ... -finding dependent... Hibernate: select driver0_.id as id13_1_, driver0_.auto_id as auto3_13_1_, driver0_.name as name13_1_, auto1_.id as id12_0_, auto1_.type as type12_0_ from RELATIONEX_DRIVER driver0_ inner join <!!!!==== Auto is a required relation for Driver RELATIONEX_AUTO auto1_ on driver0_.auto_id=auto1_.id where driver0_.id=? <!!!!==== looking for Driver we asked for in find() Hibernate: select driver0_.id as id13_1_, driver0_.auto_id as auto3_13_1_, driver0_.name as name13_1_, auto1_.id as id12_0_, auto1_.type as type12_0_ from RELATIONEX_DRIVER driver0_ inner join RELATIONEX_AUTO auto1_ on driver0_.auto_id=auto1_.id where driver0_.auto_id=? <!==== looking for Auto associated with Driver -found dependent... -calling parent...
em.flush(); em.clear(); log.info("finding parent..."); Auto auto2 = em.find(Auto.class, auto.getId()); log.info("found parent..."); assertEquals("unexpected type", auto.getType(), auto.getType()); log.info("calling dependent..."); assertEquals("unexpected name", auto.getDriver().getName(), auto2.getDriver().getName());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiOwningOptional ... -finding parent... Hibernate: select auto0_.id as id12_0_, auto0_.type as type12_0_ from RELATIONEX_AUTO auto0_ where auto0_.id=? Hibernate: select driver0_.id as id13_1_, driver0_.auto_id as auto3_13_1_, driver0_.name as name13_1_, auto1_.id as id12_0_, auto1_.type as type12_0_ from RELATIONEX_DRIVER driver0_ inner join RELATIONEX_AUTO auto1_ on driver0_.auto_id=auto1_.id where driver0_.auto_id=? -found parent... -calling dependent...
Auto truck = new Auto(); truck.setType(Auto.Type.TRUCK); em.persist(truck); driver = em.find(Driver.class, driver.getId()); //get the managed instance driver.setAuto(truck); truck.setDriver(driver); em.flush(); em.clear(); Auto auto3 = em.find(Auto.class, auto.getId()); Driver driver3 = em.find(Driver.class, driver.getId()); Auto truck3 = em.find(Auto.class, truck.getId()); assertNull("driver not removed from auto", auto3.getDriver()); assertEquals("driver not assigned to truck", truck.getId(), driver3.getAuto().getId()); assertEquals("truck not assigned to driver", driver.getId(), truck3.getDriver().getId());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiOwningOptional ... Hibernate: insert into RELATIONEX_AUTO (id, type) values (null, ?) Hibernate: update RELATIONEX_DRIVER set auto_id=?, name=? where id=? ...
Add the final cleanup and cleanup sanity checks for this example.
em.remove(truck3.getDriver()); em.remove(truck3); em.remove(auto3); em.flush(); assertNull("driver not deleted", em.find(Driver.class, truck3.getDriver().getId())); assertNull("auto not deleted", em.find(Auto.class, auto.getId())); assertNull("truck not deleted", em.find(Auto.class, truck.getId()));
@Entity(name="RelationAuto2")
@Table(name="RELATIONEX_AUTO2")
public class Auto2 {
...
@OneToOne(
optional=true, fetch=FetchType.LAZY)
private Driver2 driver;
...
public Driver2 getDriver() { return driver; }
public void setDriver(Driver2 driver) {
this.driver = driver;
}
@Entity
@Table(name="RELATIONEX_DRIVER2")
public class Driver2 {
...
@OneToOne(mappedBy="driver",//driver is now the inverse side
optional=false, //we must have the auto for this driver
fetch=FetchType.EAGER)
private Auto2 auto;
protected Driver2() {}
public Driver2(Auto2 auto) {
this.auto = auto;
}
...
public Auto2 getAuto() { return auto; }
public void setAuto(Auto2 auto) { //drivers can switch Autos
this.auto = auto;
}
...
Add the two new entities to the persistence unit.
<class>myorg.relex.one2one.Auto2</class>
<class>myorg.relex.one2one.Driver2</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_AUTO ( id integer generated by default as identity, type varchar(10), primary key (id) ); create table RELATIONEX_AUTO2 ( id integer generated by default as identity, type varchar(10), driver_id integer, primary key (id) ); ... create table RELATIONEX_DRIVER ( id integer generated by default as identity, name varchar(20), auto_id integer not null, primary key (id), unique (auto_id) ); create table RELATIONEX_DRIVER2 ( id integer generated by default as identity, name varchar(20), primary key (id) ); ... alter table RELATIONEX_AUTO2 add constraint FK75ABE7D3B3D04E86 foreign key (driver_id) references RELATIONEX_DRIVER; ... alter table RELATIONEX_DRIVER add constraint FK44C072B81E349026 foreign key (auto_id) references RELATIONEX_AUTO;
@Test
public void testOne2OneBiInverseOptional() {
log.info("*** testOne2OneBiInverseOptional() ***");
Auto2 auto = new Auto2(); //auto is owning/dependent side
auto.setType(Auto2.Type.CAR);
Driver2 driver = new Driver2(auto); //driver is inverse/parent side
driver.setName("Danica Patrick");
auto.setDriver(driver); //owning side must be set
em.persist(auto);
em.persist(driver);
em.flush();
}
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... -*** testOne2OneBiInverseOptional() *** Hibernate: insert into RELATIONEX_AUTO2 (id, driver_id, type) values (null, ?, ?) Hibernate: insert into RELATIONEX_DRIVER2 (id, name) values (null, ?) Hibernate: update RELATIONEX_AUTO2 set driver_id=?, type=? where id=?
If we reversed to persist of driver and auto...
em.persist(driver);
em.persist(auto);
em.flush();
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... -*** testOne2OneBiInverseOptional() *** Hibernate: insert into RELATIONEX_DRIVER2 (id, name) values (null, ?) Hibernate: insert into RELATIONEX_AUTO2 (id, driver_id, type) values (null, ?, ?)
em.flush(); em.clear();
log.info("finding parent...");
Driver2 driver2 = em.find(Driver2.class, driver.getId());
log.info("found parent...");
assertEquals("unexpected name", driver.getName(), driver2.getName());
log.info("calling dependent...");
assertEquals("unexpected name", driver.getAuto().getType(), driver2.getAuto().getType());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... -finding parent... Hibernate: select driver2x0_.id as id15_0_, driver2x0_.name as name15_0_ from RELATIONEX_DRIVER2 driver2x0_ where driver2x0_.id=? Hibernate: select auto2x0_.id as id14_0_, auto2x0_.driver_id as driver3_14_0_, auto2x0_.type as type14_0_ from RELATIONEX_AUTO2 auto2x0_ where auto2x0_.driver_id=? -found parent... -calling dependent...
em.flush(); em.clear();
log.info("finding dependent...");
Auto2 auto2 = em.find(Auto2.class, auto.getId());
log.info("found dependent...");
assertEquals("unexpected type", auto.getType(), auto.getType());
log.info("calling parent...");
assertEquals("unexpected name", auto.getDriver().getName(), auto2.getDriver().getName());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... -finding dependent... Hibernate: select auto2x0_.id as id14_0_, auto2x0_.driver_id as driver3_14_0_, auto2x0_.type as type14_0_ from RELATIONEX_AUTO2 auto2x0_ where auto2x0_.id=? -found dependent... -calling parent... Hibernate: select driver2x0_.id as id15_0_, driver2x0_.name as name15_0_ from RELATIONEX_DRIVER2 driver2x0_ where driver2x0_.id=? Hibernate: select auto2x0_.id as id14_0_, auto2x0_.driver_id as driver3_14_0_, auto2x0_.type as type14_0_ from RELATIONEX_AUTO2 auto2x0_ where auto2x0_.driver_id=?
Auto2 truck = new Auto2();
truck.setType(Auto2.Type.TRUCK);
em.persist(truck);
driver = em.find(Driver2.class, driver.getId()); //get the managed instance
driver.setAuto(truck);
// auto2.setDriver(null); //must remove reference to former driver
truck.setDriver(driver);//prior to assigning to new driver for 1:1
em.flush(); em.clear();
Auto2 auto3 = em.find(Auto2.class, auto.getId());
Driver2 driver3 = em.find(Driver2.class, driver.getId());
Auto2 truck3 = em.find(Auto2.class, truck.getId());
assertNull("driver not removed from auto", auto3.getDriver());
assertEquals("driver not assigned to truck", truck.getId(), driver3.getAuto().getId());
assertEquals("truck not assigned to driver", driver.getId(), truck3.getDriver().getId());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... Hibernate: insert <!!!!==== create new Auto instance into RELATIONEX_AUTO2 (id, driver_id, type) values (null, ?, ?) Hibernate: <!!!!==== relate new Auto to existing Driver update RELATIONEX_AUTO2 set driver_id=?, type=? where id=? ... Tests in error: testOne2OneBiInverseOptional(myorg.relex.One2OneTest): org.hibernate.HibernateException: More than one row with the given identifier was found: 1, for class: myorg.relex.one2one.Auto2
auto2.setDriver(null); //must remove reference to former driver
truck.setDriver(driver);//prior to assigning to new driver for 1:1
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... Hibernate: insert <!!!!== new Auto (truck) is created into RELATIONEX_AUTO2 (id, driver_id, type) values (null, ?, ?) Hibernate: <!!!!== existing Auto(auto) is cleared of reference to Driver update RELATIONEX_AUTO2 set driver_id=?, type=? where id=? Hibernate: update <!!!!== new Auto (truck) updated to reference Driver RELATIONEX_AUTO2 set driver_id=?, type=? where id=?
Auto2 truck = new Auto2();
truck.setType(Auto2.Type.TRUCK);
driver = em.find(Driver2.class, driver.getId()); //get the managed instance
driver.setAuto(truck);
auto2.setDriver(null); //must remove reference to former driver
truck.setDriver(driver);//prior to assigning to new driver for 1:1
em.persist(truck);
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... Hibernate: insert into RELATIONEX_AUTO2 (id, driver_id, type) values (null, ?, ?) Hibernate: update RELATIONEX_AUTO2 set driver_id=?, type=? where id=?
Add the following cleanup calls and verification tests to the test method.
em.remove(truck3.getDriver());
em.remove(truck3);
em.remove(auto3);
em.flush();
assertNull("driver not deleted", em.find(Driver.class, truck3.getDriver().getId()));
assertNull("auto not deleted", em.find(Auto.class, auto.getId()));
assertNull("truck not deleted", em.find(Auto.class, truck.getId()));
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... Hibernate: update RELATIONEX_AUTO2 set driver_id=?, type=? where id=? Hibernate: delete from RELATIONEX_DRIVER2 where id=? Hibernate: delete from RELATIONEX_AUTO2 where id=? Hibernate: delete from RELATIONEX_AUTO2 where id=?
em.remove(truck3);
em.remove(auto3);
em.remove(truck3.getDriver());
em.flush();
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneBiInverseOptional ... Hibernate: delete from RELATIONEX_AUTO2 where id=? Hibernate: delete from RELATIONEX_AUTO2 where id=? Hibernate: delete from RELATIONEX_DRIVER2 where id=?
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class provides an example of a recipient of cascade actions.
*/
@Entity
@Table(name="RELATIONEX_LICENSE")
public class License {
@Id @GeneratedValue
private int id;
@Temporal(TemporalType.DATE)
private Date renewal;
public int getId() { return id; }
public Date getRenewal() { return renewal; }
public void setRenewal(Date renewal) {
this.renewal = renewal;
}
}
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This class provides an example initiation of cascade actions to a
* related entity.
*/
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
@Id @GeneratedValue
private int id;
@Temporal(TemporalType.TIMESTAMP)
private Date updated;
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
// CascadeType.PERSIST,
// CascadeType.DETACH,
// CascadeType.REMOVE,
// CascadeType.REFRESH,
// CascadeType.MERGE
})
private License license;
public LicenseApplication() {}
public LicenseApplication(License license) {
this.license = license;
}
public int getId() { return id; }
public License getLicense() { return license; }
public Date getUpdated() { return updated; }
public void setUpdated(Date updated) {
this.updated = updated;
}
}
Add the two new entities to the persistence unit.
<class>myorg.relex.one2one.License</class>
<class>myorg.relex.one2one.LicenseApplication</class>
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_LICAPP ( id integer generated by default as identity, updated timestamp, license_id integer not null, primary key (id), unique (license_id) ); create table RELATIONEX_LICENSE ( id integer generated by default as identity, renewal date, primary key (id) ); ... alter table RELATIONEX_LICAPP add constraint FK51E55C6BE67289CE foreign key (license_id) references RELATIONEX_LICENSE;
Put the following test method in place in your existing one-to-one test case.
@Test
public void testOne2OneCascadeFromOwner() {
log.info("*** testOne2OneCascadeFromOwner ***");
License license = new License();
license.setRenewal(new GregorianCalendar(2012,1,1).getTime());
LicenseApplication licapp = new LicenseApplication(license);
licapp.setUpdated(new Date());
em.persist(licapp);
em.flush();
}
Re-build the module, attempt to run the new unit test, and observe the reported error.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -*** testOne2OneCascadeFromOwner *** Hibernate: insert into RELATIONEX_LICAPP (id, license_id, updated) values (null, ?, ?) -SQL Error: 23502, SQLState: 23502 -NULL not allowed for column "LICENSE_ID"; SQL statement: insert into RELATIONEX_LICAPP (id, license_id, updated) values (null, ?, ?) [23502-168]
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST
})
private License license;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -*** testOne2OneCascadeFromOwner *** Hibernate: insert into RELATIONEX_LICENSE (id, renewal) values (null, ?) Hibernate: insert into RELATIONEX_LICAPP (id, license_id, updated) values (null, ?, ?)
assertTrue("licapp was not managed???", em.contains(licapp));
assertTrue("license was not managed???", em.contains(license));
em.detach(licapp);
assertFalse("licapp still managed", em.contains(licapp));
assertFalse("license still managed", em.contains(license));
licapp = em.find(LicenseApplication.class, licapp.getId());
license = licapp.getLicense();
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... $ more `find . -name *.txt | grep reports` ------------------------------------------------------------------------------- Test set: myorg.relex.One2OneTest ------------------------------------------------------------------------------- Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 7.046 sec <<< FAILURE! testOne2OneCascadeFromOwner(myorg.relex.One2OneTest) Time elapsed: 0.724 sec <<< FAILURE! java.lang.AssertionError: license still managed at org.junit.Assert.fail(Assert.java:93) at org.junit.Assert.assertTrue(Assert.java:43) at org.junit.Assert.assertFalse(Assert.java:68) at myorg.relex.One2OneTest.testOne2OneCascadeFromOwner(One2OneTest.java:529)
Fix the immediate error by enabling cascade=DETACH from the licapp to the license entity.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH
})
private License license;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... [INFO] BUILD SUCCESS
Date newDate = new GregorianCalendar(2014, 1, 1).getTime();
Date newUpdate = new Date(licapp.getUpdated().getTime()+1);
assertEquals("unexpected update count", 1,
em.createQuery("update License lic set lic.renewal=:renewal where lic.id=:id")
.setParameter("renewal", newDate, TemporalType.DATE)
.setParameter("id", license.getId())
.executeUpdate());
assertEquals("unexpected update count", 1,
em.createQuery("update LicenseApplication licapp set licapp.updated=:updated where licapp.id=:id")
.setParameter("updated", newUpdate, TemporalType.TIMESTAMP)
.setParameter("id", licapp.getId())
.executeUpdate());
assertFalse("unexpected updated value prior to refresh",
licapp.getUpdated().getTime() == newUpdate.getTime());
assertFalse("unexpected renewal value prior to refresh",
license.getRenewal().getTime() == newDate.getTime());
log.info("database updated");
em.refresh(licapp);
log.info("entities refreshed");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ");
assertTrue(String.format("licapp not refreshed, exp=%s, act=%s", df.format(newUpdate), df.format(licapp.getUpdated())),
licapp.getUpdated().getTime() == newUpdate.getTime());
assertTrue(String.format("license not refreshed, exp=%s, act=%s", df.format(newDate), df.format(license.getRenewal())),
license.getRenewal().getTime() == newDate.getTime());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -database updated Hibernate: select licenseapp0_.id as id17_0_, licenseapp0_.license_id as license3_17_0_, licenseapp0_.updated as updated17_0_ from RELATIONEX_LICAPP licenseapp0_ where licenseapp0_.id=? -entities refreshed ... Failed tests: testOne2OneCascadeFromOwner(myorg.relex.One2OneTest): license not refreshed, exp=2014-02-01 00:00:00.000-0500, act=2012-02-01 00:00:00.000-0500
Correct the issue by adding cascade=REFRESH to the relationship in the licapp entity.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH
})
private License license;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -database updated Hibernate: select license0_.id as id16_0_, license0_.renewal as renewal16_0_ from RELATIONEX_LICENSE license0_ where license0_.id=? Hibernate: select licenseapp0_.id as id17_1_, licenseapp0_.license_id as license3_17_1_, licenseapp0_.updated as updated17_1_, license1_.id as id16_0_, license1_.renewal as renewal16_0_ from RELATIONEX_LICAPP licenseapp0_ inner join RELATIONEX_LICENSE license1_ on licenseapp0_.license_id=license1_.id where licenseapp0_.id=? -entities refreshed ... [INFO] BUILD SUCCESS
em.detach(licapp);
newDate = new GregorianCalendar(2016, 1, 1).getTime();
newUpdate = new Date(licapp.getUpdated().getTime()+1);
assertFalse("licapp still managed", em.contains(licapp));
assertFalse("license still managed", em.contains(licapp.getLicense()));
licapp.setUpdated(newUpdate);
licapp.getLicense().setRenewal(newDate);
log.info("merging changes to detached entities");
licapp=em.merge(licapp);
em.flush();
log.info("merging complete");
assertTrue("merged licapp not managed", em.contains(licapp));
assertTrue("merged licapp.license not managed", em.contains(licapp.getLicense()));
assertTrue(String.format("licapp not merged, exp=%s, act=%s", df.format(newUpdate), df.format(licapp.getUpdated())),
licapp.getUpdated().getTime() == newUpdate.getTime());
assertTrue(String.format("license not merged, exp=%s, act=%s", df.format(newDate), df.format(license.getRenewal())),
licapp.getLicense().getRenewal().getTime() == newDate.getTime());
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -merging changes to detached entities Hibernate: select licenseapp0_.id as id17_0_, licenseapp0_.license_id as license3_17_0_, licenseapp0_.updated as updated17_0_ from RELATIONEX_LICAPP licenseapp0_ where licenseapp0_.id=? Hibernate: select license0_.id as id16_0_, license0_.renewal as renewal16_0_ from RELATIONEX_LICENSE license0_ where license0_.id=? Hibernate: update RELATIONEX_LICAPP set license_id=?, updated=? where id=? -merging complete ... Failed tests: testOne2OneCascadeFromOwner(myorg.relex.One2OneTest): license not merged, exp=2016-02-01 00:00:00.000-0500, act=2016-02-01 00:00:00.000-0500
Attempt to fix the issue by adding cascade=MERGE to the relationship defined in licapp.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH,
CascadeType.MERGE
})
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... -merging changes to detached entities Hibernate: select licenseapp0_.id as id17_1_, licenseapp0_.license_id as license3_17_1_, licenseapp0_.updated as updated17_1_, license1_.id as id16_0_, license1_.renewal as renewal16_0_ from RELATIONEX_LICAPP licenseapp0_ inner join RELATIONEX_LICENSE license1_ on licenseapp0_.license_id=license1_.id where licenseapp0_.id=? Hibernate: update RELATIONEX_LICENSE set renewal=? where id=? Hibernate: update RELATIONEX_LICAPP set license_id=?, updated=? where id=? -merging complete
em.remove(licapp); em.flush(); assertNull("licapp not deleted", em.find(LicenseApplication.class, licapp.getId())); assertNull("licapp.license not deleted", em.find(License.class, licapp.getLicense().getId()));
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... Hibernate: delete from RELATIONEX_LICAPP where id=? ... Failed tests: testOne2OneCascadeFromOwner(myorg.relex.One2OneTest): licapp.license not deleted
Attempt to fix the problem by adding cascade=DELETE to the relationship defined within licapp.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REMOVE,
CascadeType.REFRESH,
CascadeType.MERGE
})
private License license;
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... Hibernate: delete from RELATIONEX_LICAPP where id=? Hibernate: delete from RELATIONEX_LICENSE where id=? ... [INFO] BUILD SUCCESS
package myorg.relex.one2one;
import java.util.Date;
import javax.persistence.*;
/**
* This entity class provides an example of cascades being originated from
* the inverse side of a bi-directional relationship.
*/
@Entity
@Table(name="RELATIONEX_TICKET")
public class Ticket {
@Id @GeneratedValue
private int id;
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
// CascadeType.PERSIST,
// CascadeType.DETACH,
// CascadeType.REFRESH,
// CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
@Temporal(TemporalType.DATE)
Date date;
public Ticket(){}
public Ticket(int id) { this.id = id; }
public int getId() { return id; }
public Passenger getPassenger() { return passenger; }
public void setPassenger(Passenger passenger) {
this.passenger = passenger;
}
public Date getDate() { return date; }
public void setDate(Date date) {
this.date = date;
}
}
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This entity class provides an example of the owning side of a
* bi-directional relation where all cascades are being initiated
* from the inverse side (i.e., not from here).
*/
@Entity
@Table(name="RELATIONEX_PASSENGER")
public class Passenger {
@Id @GeneratedValue
private int id;
@OneToOne(optional=false)
private Ticket ticket;
@Column(length=32, nullable=false)
private String name;
protected Passenger() {}
public Passenger(int id) { this.id = id; }
public Passenger(Ticket ticket, String name) {
this.ticket = ticket;
this.name = name;
}
public int getId() { return id; }
public Ticket getTicket() { return ticket; }
public void setTicket(Ticket ticket) {
this.ticket = ticket;
}
public String getName() { return name;}
public void setName(String name) {
this.name = name;
}
}
Add the two entity classes to the persistence unit.
<class>myorg.relex.one2one.Ticket</class>
<class>myorg.relex.one2one.Passenger</class>
@Test
public void testOne2OneCascadeFromInverse() {
log.info("*** testOne2OneCascadeFromInverse ***");
Ticket ticket = new Ticket();
ticket.setDate(new GregorianCalendar(2013, Calendar.MARCH, 16).getTime());
Passenger passenger = new Passenger(ticket, "Fred"); //set inverse side
ticket.setPassenger(passenger); //set the owning side
em.persist(ticket); //persist from inverse side
em.flush();
assertTrue("ticket not managed", em.contains(ticket));
assertTrue("passenger not managed", em.contains(passenger));
}
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -*** testOne2OneCascadeFromInverse *** Hibernate: insert into RELATIONEX_TICKET (id, date) values (null, ?) ... testOne2OneCascadeFromInverse(myorg.relex.One2OneTest): org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: myorg.relex.one2one.Ticket.passenger -> myorg.relex.one2one.Passenger
Fix the inverse/parent side of the relationship by adding a cascade of PERSIST.
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
// CascadeType.DETACH,
// CascadeType.REFRESH,
// CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -*** testOne2OneCascadeFromInverse *** Hibernate: insert into RELATIONEX_TICKET (id, date) values (null, ?) Hibernate: insert into RELATIONEX_PASSENGER (id, name, ticket_id) values (null, ?, ?) ... [INFO] BUILD SUCCESS
log.debug("detach both instances from the persistence context");
em.detach(ticket);
assertFalse("ticket managed", em.contains(ticket));
assertFalse("passenger managed", em.contains(passenger));
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -detach both instances from the persistence context ... Failed tests: testOne2OneCascadeFromInverse(myorg.relex.One2OneTest): passenger managed
Fix the inverse side by adding a cascade of DETACH to the relationship.
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
// CascadeType.REFRESH,
// CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
Rebuild and re-run the test method with the correction in place. This should now pass.
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -detach both instances from the persistence context ... [INFO] BUILD SUCCESS
log.debug("perform a bulk update to both objects");
ticket = em.find(Ticket.class, ticket.getId());
Date newDate=new GregorianCalendar(2013, Calendar.APRIL, 1).getTime();
String newName = "Frederick";
em.createQuery("update Ticket t set t.date=:date")
.setParameter("date", newDate,TemporalType.DATE)
.executeUpdate();
em.createQuery("update Passenger p set p.name=:name where p.name='Fred'")
.setParameter("name", newName)
.executeUpdate();
assertFalse("unexpected date", newDate.equals(ticket.getDate()));
assertFalse("unexpected name", newName.equals(ticket.getPassenger().getName()));
em.refresh(ticket);
assertTrue("date not refreshed", newDate.equals(ticket.getDate()));
assertTrue("name not refreshed", newName.equals(ticket.getPassenger().getName()));
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -perform a bulk update to both objects Hibernate: select ticket0_.id as id18_1_, ticket0_.date as date18_1_, passenger1_.id as id19_0_, passenger1_.name as name19_0_, passenger1_.ticket_id as ticket3_19_0_ from RELATIONEX_TICKET ticket0_ left outer join RELATIONEX_PASSENGER passenger1_ on ticket0_.id=passenger1_.ticket_id where ticket0_.id=? Hibernate: update RELATIONEX_TICKET set date=? Hibernate: update RELATIONEX_PASSENGER set name=? where name='Fred' Hibernate: select ticket0_.id as id18_0_, ticket0_.date as date18_0_ from RELATIONEX_TICKET ticket0_ where ticket0_.id=? ... Failed tests: testOne2OneCascadeFromInverse(myorg.relex.One2OneTest): name not refreshed
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH,
// CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -perform a bulk update to both objects Hibernate: select ticket0_.id as id18_1_, ticket0_.date as date18_1_, passenger1_.id as id19_0_, passenger1_.name as name19_0_, passenger1_.ticket_id as ticket3_19_0_ from RELATIONEX_TICKET ticket0_ left outer join RELATIONEX_PASSENGER passenger1_ on ticket0_.id=passenger1_.ticket_id where ticket0_.id=? Hibernate: update RELATIONEX_TICKET set date=? Hibernate: update RELATIONEX_PASSENGER set name=? where name='Fred' Hibernate: select passenger0_.id as id19_0_, passenger0_.name as name19_0_, passenger0_.ticket_id as ticket3_19_0_ from RELATIONEX_PASSENGER passenger0_ where passenger0_.id=? Hibernate: select ticket0_.id as id18_1_, ticket0_.date as date18_1_, passenger1_.id as id19_0_, passenger1_.name as name19_0_, passenger1_.ticket_id as ticket3_19_0_ from RELATIONEX_TICKET ticket0_ left outer join RELATIONEX_PASSENGER passenger1_ on ticket0_.id=passenger1_.ticket_id where ticket0_.id=? Hibernate: select passenger0_.id as id19_1_, passenger0_.name as name19_1_, passenger0_.ticket_id as ticket3_19_1_, ticket1_.id as id18_0_, ticket1_.date as date18_0_ from RELATIONEX_PASSENGER passenger0_ inner join RELATIONEX_TICKET ticket1_ on passenger0_.ticket_id=ticket1_.id where passenger0_.ticket_id=? ... [INFO] BUILD SUCCESS
log.debug("merging changes from inverse side");
Ticket ticket2 = new Ticket(ticket.getId());
ticket2.setDate(new GregorianCalendar(2014, Calendar.APRIL, 1).getTime());
Passenger passenger2 = new Passenger(passenger.getId());
passenger2.setName("Rick");
ticket2.setPassenger(passenger2);
passenger2.setTicket(ticket2);
assertFalse("unexpected date", ticket2.getDate().equals(ticket.getDate()));
assertFalse("unexpected name", ticket2.getPassenger().getName().equals(ticket.getPassenger().getName()));
ticket=em.merge(ticket2);
em.flush();
assertTrue("date not merged", ticket2.getDate().equals(ticket.getDate()));
assertTrue("name not merged", ticket2.getPassenger().getName().equals(ticket.getPassenger().getName()));
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -merging changes from inverse side Hibernate: select passenger0_.id as id19_1_, passenger0_.name as name19_1_, passenger0_.ticket_id as ticket3_19_1_, ticket1_.id as id18_0_, ticket1_.date as date18_0_ from RELATIONEX_PASSENGER passenger0_ inner join RELATIONEX_TICKET ticket1_ on passenger0_.ticket_id=ticket1_.id where passenger0_.ticket_id=? -tearDown() started, em=org.hibernate.ejb.EntityManagerImpl@5dfadfd6 Hibernate: update RELATIONEX_TICKET set date=? where id=? ... Failed tests: testOne2OneCascadeFromInverse(myorg.relex.One2OneTest): name not merged
Fix the issue by configuring the inverse side to cascade the MERGE.
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH,
CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... Hibernate: update RELATIONEX_PASSENGER set name=?, ticket_id=? where id=? Hibernate: update RELATIONEX_TICKET set date=? where id=? ... [INFO] BUILD SUCCESS
Add the following code snippet to your test method to demonstrate the cascade from the inverse side.
log.debug("delete the entities from the inverse side");
assertNotNull("ticket not found", em.find(Ticket.class, ticket.getId()));
assertNotNull("passenger not found", em.find(Passenger.class, ticket.getPassenger().getId()));
em.remove(ticket);
em.flush();
assertNull("ticket not removed", em.find(Ticket.class, ticket.getId()));
assertNull("passenger not removed", em.find(Passenger.class, ticket.getPassenger().getId()));
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -delete the entities from the inverse side Hibernate: delete from RELATIONEX_TICKET where id=? -SQL Error: 23503, SQLState: 23503 -Referential integrity constraint violation: "FKD08633EA1BCEB406: PUBLIC.RELATIONEX_PASSENGER FOREIGN KEY(TICKET_ID) REFERENCES PUBLIC.RELATIONEX_TICKET(ID) (1)"; SQL statement: delete from RELATIONEX_TICKET where id=? [23503-168]
Fix the issue by configuring the inverse side to cascade the DELETE.
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH,
CascadeType.MERGE,
CascadeType.REMOVE
})
private Passenger passenger;
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromInverse ... -delete the entities from the inverse side Hibernate: delete from RELATIONEX_PASSENGER where id=? Hibernate: delete from RELATIONEX_TICKET where id=? ... [INFO] BUILD SUCCESS
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This entity class provides an example of an entity that
* will get deleted when no longer referenced by its dependent
* entity in a one-to-one relation. This is called orphan removal.
*/
@Entity
@Table(name="RELATIONEX_RESIDENCE")
public class Residence {
@Id @GeneratedValue
private int id;
@Column(length=16, nullable=false)
private String city;
@Column(length=2, nullable=false)
private String state;
protected Residence() {}
public Residence(int id) { this.id = id; }
public Residence(String city, String state) {
this.city = city;
this.state = state;
}
public int getId() { return id; }
public String getCity() { return city; }
public void setCity(String city) {
this.city = city;
}
public String getState() { return state;}
public void setState(String state) {
this.state = state;
}
}
package myorg.relex.one2one;
import javax.persistence.*;
/**
* This entity class provides an example of a dependent with a relationship to a parent entity that
* should only exist to support this entity. When this entity ceases to reference the parent, it
* will become "orphaned" and subject to orphanRemoval by the provider.
*/
@Entity
@Table(name="RELATIONEX_ATTENDEE")
public class Attendee {
@Id @GeneratedValue
private int id;
//orphanRemoval will take care of dereference and DELETE from dependent Attendee
@OneToOne(cascade=CascadeType.PERSIST, orphanRemoval=true)
private Residence residence;
private String name;
public int getId() { return id; }
public Residence getResidence() { return residence; }
public void setResidence(Residence residence) {
this.residence = residence;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
Add the two entity classes to the persistence unit using the persistence.xml.
<class>myorg.relex.one2one.Residence</class>
<class>myorg.relex.one2one.Attendee</class>
@Test
public void testOrphanRemoval() {
log.info("*** testOrphanRemoval ***");
log.debug("start by verifying the state of the database");
int startCount = em.createQuery("select count(r) from Residence r", Number.class)
.getSingleResult().intValue();
log.debug("create a new attendee and residence");
Attendee attendee = new Attendee();
attendee.setName("jones");
attendee.setResidence(new Residence("Columbia", "MD"));
em.persist(attendee);
em.flush();
log.debug("verify we have a new residence in the database");
assertEquals("unexpected number of residences", startCount+1,
em.createQuery("select count(r) from Residence r", Number.class)
.getSingleResult().intValue());
log.debug("verify we can find our new instance");
int originalId=attendee.getResidence().getId();
assertNotNull("could not find residence", em.find(Residence.class, originalId));
}
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOrphanRemoval ... -start by verifying the state of the database Hibernate: select count(residence0_.id) as col_0_0_ from RELATIONEX_RESIDENCE residence0_ limit ? -create a new attendee and residence Hibernate: insert into RELATIONEX_RESIDENCE (id, city, state) values (null, ?, ?) Hibernate: insert into RELATIONEX_ATTENDEE (id, name, residence_id) values (null, ?, ?) -verify we have a new residence in the database Hibernate: select count(residence0_.id) as col_0_0_ from RELATIONEX_RESIDENCE residence0_ limit ? -verify we can find our new instance
Add the following to your test method to orphan your original parent.
log.debug("have attendee change residence");
//ISSUE: https://hibernate.atlassian.net/browse/HHH-6484
//MORE: https://hibernate.atlassian.net/browse/HHH-5559
// attendee.setResidence(null);
// em.flush();
attendee.setResidence(new Residence("Baltimore", "MD"));
em.flush();
log.debug("verify we have the same number of residences");
assertEquals("unexpected number of residences", startCount+1,
em.createQuery("select count(r) from Residence r", Number.class)
.getSingleResult().intValue());
log.debug("verify the new instance replaced the original instance");
assertNull("found original residence", em.find(Residence.class, originalId));
assertNotNull("could not find new residence", em.find(Residence.class, attendee.getResidence().getId()));
Rebuild the module and re-run the test method to exercise the additional test snippet. There is a small debate about what should happen. Ideally the older, orphaned parent should get deleted when the new parent takes its place as documented in the following trouble tickets( HHH-6484 and HHH-5559). However, that does not end up being the case
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOrphanRemoval ... -have attendee change residence Hibernate: insert into RELATIONEX_RESIDENCE (id, city, state) values (null, ?, ?) Hibernate: update RELATIONEX_ATTENDEE set name=?, residence_id=? where id=? -verify we have the same number of residences Hibernate: select count(residence0_.id) as col_0_0_ from RELATIONEX_RESIDENCE residence0_ limit ? ... Failed tests: testOrphanRemoval(myorg.relex.One2OneTest): unexpected number of residences expected:<1> but was:<2>
Uncomment setting the parent reference to null and calling em.flush() on the session. This will cause the desired behavior with the extra steps/calls.
attendee.setResidence(null);
em.flush();
attendee.setResidence(new Residence("Baltimore", "MD"));
Rebuild the module and re-run the test method with the above lines uncommented. This should cause the older, orphaned parent to be deleted. The assignment to the new parent is an independent step.
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOrphanRemoval ... -have attendee change residence Hibernate: update RELATIONEX_ATTENDEE set name=?, residence_id=? where id=? Hibernate: delete from RELATIONEX_RESIDENCE where id=? Hibernate: insert into RELATIONEX_RESIDENCE (id, city, state) values (null, ?, ?) Hibernate: update RELATIONEX_ATTENDEE set name=?, residence_id=? where id=? -verify we have the same number of residences Hibernate: select count(residence0_.id) as col_0_0_ from RELATIONEX_RESIDENCE residence0_ limit ? -verify the new instance replaced the original instance Hibernate: select residence0_.id as id18_0_, residence0_.city as city18_0_, residence0_.state as state18_0_ from RELATIONEX_RESIDENCE residence0_ where residence0_.id=?
Add the following statements to verify a simple nulling of the parent. With the work-around above, there should be no surprise here that this works. However, the flush() is unnecessary because it appears to be happening automatically because of the follow-on query.
log.debug("remove reference to the current residence");
attendee.setResidence(null);
//em.flush(); -- note flush is done during follow-on query
log.debug("verify all residences created during this test have been deleted");
assertEquals("unexpected number of residences", startCount,
em.createQuery("select count(r) from Residence r", Number.class)
.getSingleResult().intValue());
Rebuild the module and re-run the test method with the final exercise of the orphan management capability. Notice how the query made the manual call to em.flush() unnecessary.
$ mvn clean test -Dtest=myorg.relex.One2OneTest#testOrphanRemoval ... -remove reference to the current residence -verify all residences created during this test have been deleted Hibernate: update RELATIONEX_ATTENDEE set name=?, residence_id=? where id=? Hibernate: delete from RELATIONEX_RESIDENCE where id=? Hibernate: select count(residence0_.id) as col_0_0_ from RELATIONEX_RESIDENCE residence0_ limit ? ... [INFO] BUILD SUCCESS
You have finished working with orphanRemoval and found that the provider will automatically delete the parent entity when the reference from the dependent is nulled out and the session is flushed to the database. You saw how a simple replace did not cause the behavior and noticed there were a few trouble tickets written against that lack of behavior that causes your business logic to take extra steps to have the orphan removed.