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.
Put the following Junit test case base class in your src/test tree. You can delete the sample test method once we add our first real test. JUnit will fail a test case if it cannot locate a @Test to run.
package myorg.relex;
import static org.junit.Assert.*;
import javax.persistence.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.*;
public class One2OneTest extends JPATestBase {
private static Logger log = LoggerFactory.getLogger(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
The notion of a uni-directional relationship is solely a characterization of what the Java class at either end of the relationship knows about the other. For uni-directional relationships only one class references the other while the other passively participates in the relationship.
In this first case we are going to model the relationship from the owning side of the relationship as a foreign key in the owning entity's table.
Create the following entity class in your src/main tree to represent the passive side of the relationship. I am calling this "passive" (or "ignorant") because it will know nothing of the relationships we will form within this section. This is different than the "inverse" side we will address in the bi-directional case.
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;
}
}
Notice there is no reference to the owning Player class within this entity. This fact alone makes it uni-directional
Create the following entity class in your src/main tree to represent the owning side of the relationship. It is currently incomplete.
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.jpa.HibernatePersistenceProvider</provider>
...
<class>myorg.relex.one2one.Person</class>
<class>myorg.relex.one2one.Player</class>
...
</persistence-unit>
Attempt to build the module and note the error that results. The error is stating the provider does not know how to map the non-serializable Person class to a column within the Player table.
org.hibernate.MappingException: Could not determine type for: myorg.relex.one2one.Person, at table: RELATIONEX_PLAYER, for columns: [org.hibernate.mapping.Column(person).
If you look back at the Class mapping topic, we were able to map a serialized relationship to a BLOB column. That is what we are accidentally doing here if we leave off the @XxxToXxx relationship specification.
Add a JPA @OneToOne relationship mapping from the Player to Person. Also include a definitions to...
Make the Person required for the Player
Specify the Person must be also fetched when obtaining the Player
Specify a foreign key column in the Player table that references the Person table
@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;
The Player table contains a foreign key referencing the Person table. Note the foreign key *value* (PERSON_ID) is not modeled within the Player entity class. Only the *relationship* to the Person has been depicted within the Player. If we want the person ID value, we can ask the person object related to the player.
The foreign key column is required to be supplied ("not null"). This means that all Players must have a Person
The foreign key column is required to be unique. This means that only one Player may reference one Person using the PERSON_ID.
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)
The provider is stating that our test case is trying to persist the Player when the reference to the Person references an unmanaged Person object. We need add a persist of the Person prior to hitting the call to flush.
Update the test method to persist both the Person and Player prior to the flush.
em.persist(person);
em.persist(player);
We will look at cascades a bit later which may or may not be appropriate to solve this dependent/parent table persistence.
Rebuild and observe the results of the test method. Note the Person and Player being persisted and the PERSON_ID of the Player being set to the generated primary key value of the Person. During the find(), the Person and Player are both obtained through a database join. Since the Person is required for the Player and we requested an EAGER fetch type, a database inner join is performed between the Player and Person tables.
$ 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=?
If we made the Person optional the database query is converted from an inner join to an outer join -- allowing Players without a Person to be returned.
@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=?
Also note if we modified the fetch specification to LAZY, the join is removed entirely and replaced with a single select of the Player table during the find() and then a follow-up select of the Person table once we got to the player.getPerson().getName() calls.
@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=?
If we comment out the calls to getPerson.getName(), only a single select on the Player is performed and the Person is never retrieved. That is the performance power of LAZY load.
//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=?
Add the following code to the test method to perform a query of the two tables using SQL in order to verify the expected mappings and values
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]
Add the following delete logic to the test method to remove the Person object. It is currently incomplete.
//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()));
Attempt to re-build the module and note the error that occurs. The problem is we have attempted to delete the Person row from the database while a foreign key from the Player was still referencing it.
.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());
Rebuild the module and note the success of the test method and the sensible delete order within the database.
Hibernate: delete from RELATIONEX_PLAYER where id=? Hibernate: delete from RELATIONEX_PERSON where id=?
We have finished a pass at the first way to hook up a one-to-one, uni-directional relationship by using a foreign key. With that, we also showed the database impact of making the relationship optional and modifying the fetch type. We also purposely created errors common to persisting and deleting obejcts with foreign key references.
Next we are going to realize the one-to-one uni-directional relationship from the dependent to parent entity using a join table. The implementation of the dependent entity is identical to what we did in the FK-join except for changing the @JoinColumn to a @JoinTable
Add the following entity class to your src/main tree. The comments make it incomplete and use a default mapping for the @OneToOne relationship.
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>
Build the module and observe the generated database schema. Notice the default mapping for the relationship is a foreign key join.
$ 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;
Update the mapping to use a a join table using the @JoinTable annotation. The name of the join table is required in this case, but leave the rest of the mapping defaulted at this point.
@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;
Note...
The provider derived names for the Person.id and Member.id foreign keys in the join table
The "id" column of the join table is the primary key and has a primary key join relationship with the dependent's table.
The "person_id" of the join table is also constrained to be unique since this is a one-to-one relationship. We can only have a single entry in this table referencing the parent entity.
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;
The JoinTable.name property was used to name the table
The JoinTable.joinColumns property was used define column(s) pointing to this dependent entity
The JoinTable.inverseJoinColumns property was used to define column(s) pointing to the parent entity
Multiple @JoinColumns would have been necessary only when using composite keys
Re-build the module and note the generated database schema for the join table. The columns now have the custom names we assigned.
$ 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());
}
Build the module, run the new test method, and observe the database output. Notice the extra insert for the join table
$ 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=?
If you make the relationship optional then the inner join to the Person changes to a left outer join -- allowing us to locate Members that have no Person related.
@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=?
If you change from EAGER to LAZY fetch type, the provider then has the option of skipping the two extra tables until the Person is actually needed. Note, however, in the provided output that the provider joined with at least the join table so that it could build a lightweight reference to the Person.
@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=?
Using LAZY fetch mode, the provider is able to postpone getting the parent object until it is actually requested.
Hibernate: select person0_.id as id1_0_, person0_.name as name1_0_ from RELATIONEX_PERSON person0_ where person0_.id=?
Add the following test of the SQL structure to the test method. Here we can assert what we believe the mapping and values should be in the database when forming the one-to-one relationship using the join table.
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());
Re-build the module run the test method of interest, and note the success of our assertions on the schema and the produced values.
$ 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()));
Re-build, not the successful results of our assertions, and the database output. A row is deleted from the Member and join table when the Member is deleted. Person row is deleted when we finally delete the person.
$ 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=? ...
We have completed our one-to-one, uni-directional relationship implemented through a join table. It required an extra table, and some more verbose mappings -- but not any structural change to the dependent entity class.
Next we will attempt to remove the separate foreign key column from the dependent table or the separate join table mapping the dependent and parent tables. We will instead map the dependent to the parent using a join of their primary key values. This means that the primary keys of both entities/tables must be the same value. The parent's primary key can be automatically generated -- but the dependent's primary key value must be based on the parent's value. As you will see, that will cause a slight complication in ordering the persists of the two entities.
Add the following entity class to your src/main tree to implement a one-to-one, uni-directional, primary key join. In this entity class, we have replaced the @JoinColumn with a @PrimaryKeyJoinColumn specification. This tells the provider not to create a separate foreign key column in the database and to reuse the primary key column to form the relation to the Person.
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;
}
}
Note...
The dependent entity has an @Id property compatible with the type in the parent entity @Id
The dependent entity @Id is not generated -- it must be assigned
The relationship to the parent entity is defined as being realized through the value in the primary key
The dependent entity class requires the parent be provided in the constructor and provides no setters for the relation. JPA has no requirement for this but is an appropriate class design since the person is a required relation, the source of the primary key, and it is illegal to change the value of a primary key in the database.
Add the new entity class to the persistence unit.
<class>myorg.relex.one2one.Employee</class>
Build the module and observe the database schema generated. Notice the Employee table does not have a separate foreign key column and its primary key is assigned the duties of the foreign key.
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;
Add the following test method to your existing one-to-one test case. It is incomplete at this point and will cause an error.
@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());
}
Attempt to build and execute the new test method and observe the results. The problem is the the primary key is not being set and the required foreign key is being realized by the unset primary key.
$ 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]
Move the persistence of the parent entity so that it is in place prior to being assigned to the dependent entity. That way the dependent entity will be receiving the primary key value in time for it to be persisted.
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);
Re-build the module and re-run the test method. It should now be able to persist both entities and successfully pull them back.
$ 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 (?, ?) ...
Notice that -- in the primary key join case -- the query to the database uses two separate selects rather than a single select with a join as done with the FK-join case. We can tell the fetch mode is EAGER by the fact that the select for the parent table occurs prior to making a call to the parent.
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...
If you change the relationship to required/LAZY you will notice by the location of "calling person..." -- the second select occurs at the point where the parent is being dereferenced and called.
@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=?
One odd thing of note -- if we change the relationship to optional/LAZY, the provider performs the same type of query as when it was required/EAGER.
@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...
Add the following to your test method to verify the tables, columns, and values we expect at the raw SQL level.
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());
Rebuild the module and execute the test method to verify the assertions about the raw SQL structure and values.
$ 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]
Add the following cleanup logic and to test the ability to delete the entities and their relationships.
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=?
You have finished modeling a one-to-one, uni-directional relationship using a primary key join. Using this technique saved the dependent of using a separate foreign key column but created the requirement that the parent entity be persisted first. We also saw how changing the required and fetch mode could impact the underlying quieries to the database. In the next section we will show how a new feature in JPA 2.0 can ease the propagation of the parent primary key to the dependent entity.
JPA 2.0 added a new annotation called @MapsId that can ease the propagation of the parent primary key to the dependent entity. There are several uses of @MapsId. We will first look at its capability to identify the foreign key of a dependent entity as being the source of the primary key value. We saw in the FK-join case where the provider automatically propagates FK values to dependent entities but not PK-joins. Rather than saying the PK realizes the FK. @MapsId seems to state the FK realizes the PK. Lets take a concrete look...
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>
Rebuild the module and take a look at the generated database schema for what was initially defined above. Notice how the dependent table has been define to have both a primary key and a separate foreign key. Lets fix that so there is only a single column to represent the two purposes like what we did for the PK-join case.
$ 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;
Update the dependent entity class to inform the provider to derive the primary key value from the assigned foreign key relationship using the @MapsId annotation.
@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;
If you look back over the entire class design you should notice that the class provides no way to ever assign the @Id except through @MapsId.
Rebuild the module and review the generated database schema. Notice how the provider is now using the column named after the foreign key as the primary key and has eliminated the separate primary key.
$ 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;
Add the following test method to your existing one-to-one test case. Notice the design of the test method persists the parent and dependent class together -- without having to worry about deriving the parent primary key first. That is very convenient.
@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());
}
Re-build the module and run the the new test method. Notice the provider issues two separate selects; one select each for the dependent and parent entity.
$ 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()));
Re-build the module, re-run the test method, and note the successful deletion of the two entity rows.
$ 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=?
You have completed implementing a one-to-one, uni-directional relationship using a @MapsId to derive the primary key of the dependent entity from the foreign key to the parent entity. This allowed the persist() of the two entities to occur without worrying about a sequencing them in separate actions to the database.
This section will cover cases where one wants to map a one-to-one primary key join to a parent entity that uses a composite primary key. The dependent entity may use either an @IdClass/@PrimaryKeyJoin or an @EmbeddedId/@MapsId to realize this relationship and identity.
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;
}
}
The above entity class uses two properties to form its primary key -- thus it requires a composite primary key to represent the PK within JPA.
Put the following composite primary key in place. It is defined as @Embeddable so that it can be used both as an @IdClass and an @EmbeddableId.
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>
Continue on with mapping the dependent entity using an @IdClass and @EmbeddedId. You will find the @IdClass technique acts much like the @PrimaryKeyJoin we performed earlier. The @EmbeddedId technique acts much like the @MapsId case as well.
This sub-section will map the dependent class to the parent using an @IdClass.
Put the following dependent entity class in you src/main tree. It is incomplete at this point and will generate the default mapping for the relationship to the class using the composite PK. Since we eventually want to derive the primary key(s) for this dependent entity from the parent entity -- we also model the same properties as @Id and use and @IdClass to represent the PK within JPA. At this point -- the composite identity of the dependent entity is independent of the relationship.
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>
Build the module and observe the database schema generated for the entity classes involved. Notice how the dependent table has seemingly duplicate columns. There is a TICKET_DATE/TIME set of columns that represent the dependent entity's composite primary key. There is also a show_date/time set of columns to reference the parent entity -- which also uses a composite primary key. If the referenced entity of a foreign relationship uses a composite primary key -- then the value of the foreign key also expresses a composite set of properties.
$ 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;
Re-build the module and observe how the default mapping of the @PrimaryKeyJoin was realized when using the composite primary key.
$ 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;
In this case, the provider was able to generate default mappings that are exactly what we would have created manually. You could have enabled the following custom mappings to explicitly map primary key column values from the dependent table columns to the parent table columns.
@OneToOne(optional=false, fetch=FetchType.EAGER)
@PrimaryKeyJoinColumns({
@PrimaryKeyJoinColumn(name="TICKET_DATE", referencedColumnName="date"),
@PrimaryKeyJoinColumn(name="TICKET_TIME", referencedColumnName="time"),
})
private ShowEvent show;
Note there can only be a single @PrimaryKeyJoin annotated against a method. Multiple @PrimaryKeyJoin columns must be wrapped within a @PrimaryKeyJoinColumns annotation to work.
$ 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;
Add the following test method to your one-to-one test case. Although the @IdClass/@PrimaryKeyJoin is very similar to the @Id/PrimaryKeyJoin covered earlier, this approach is being simplified by the fact the primary key of the parent is not dynamically generated. The relationship assembly can occur as soon as the we derive the natural key values for the parent entity.
@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...
Add the following to the test method to verify our assertions about the structure of the database tables and their values related to this example.
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]
Add the following cleanup logic and assertions to verify the rows have been deleted for the dependent and parent entities.
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=?
You have completed mapping a one-to-one uni-directional relationship that is based on a composite primary in the parent and the composite key mapped in the dependent table as an @IdClass.
In this second example of @MapsId, we will be informing the provider that the primary key for the dependent table is realized by the foreign key and, in this case, is a composite primary key. We must use an @EmbeddedId in order for this to work correctly.
Add the following entity class to your src/main tree. It is not complete at this point and schema generation will show there bring a separate primary and foreign key.
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>
Build the module and observe the generated schema. Notice the separate use of date/time for the primary key and show_date/time for the foreign key.
$ 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;
Update the dependent table mapping so that the foreign key is used to realize the primary key for the entity. Notice also the class provides no way to set the @EmbeddedId exception thru the @MapsId on the foreign key.
@OneToOne(optional=false, fetch=FetchType.EAGER)
@MapsId //provider maps this composite FK to @EmbeddedId PK value
private ShowEvent show;
Re-build the module and observe the generated database schema. Note the primary key has now been mapped to the show_date/time foreign key columns.
$ 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...
Add the following to verify our assertions about the SQL structure and values underlying the JPA abstraction.
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());
Re-build the module and re-run the test method to verify the underlying SQL structure is how assume it to be.
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]
Add the following removal logic to test that we can remove the two entities and their associated rows.
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=?
You have not completed the mapping of a one-to-one, uni-directional relationship using a composite key and realized through the use of an @EmbeddedId and @MapsId.
In this chapter, we have so far only addressed uni-directional relationships -- where only one side of the relationship was aware of the other at the Java class level. We can also make our relationships bi-directional for easy navigation to/from either side. This requires no change the database and is only a change at the Java and mapping levels.
In bi-directional relationships, it is important to understand there are two sides/types to the relationship; owning side and inverse side.
The owning side of the relation defines the mapping to the database. This is what we did for the uni-directional sections above
The inverse side of the relation must refer to its owning side mapping through the "mappedBy" property of the @XxxToXxx annotation
For OneToOne relationships, the owning side contains the foreign key or defines the join table
The provider will initialize the state of the inverse side during calls like find() and refresh(), but will not update its value during application changes to the owning side. This is the application programmer's job to make the two references consistent.
The provider will only trigger persistence changes thru changes to the owning side
Lets start the discussion of one-to-one bi-directional using a set of entities that are pretty much joined at the hip. Their properties have been mapped to separate database tables and Java entity classes, but they will never reference a different instance. For this reason we will assign them a common primary key, join them by that common primary key value, and propagate the primary key to the dependent class using @MapsId.
Add the following inverse side entity class to your src/main tree. It is currently incomplete and will soon cause an error.
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;
}
}
Add the following owning side entity class to your src/main tree. It is currently incomplete and will not yet generate the desired primary key mapping we desire in this case.
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;
}
}
It is important to note that -- in the case of a bi-directional relationship -- the application developer is responsible for setting both sides of the relationship even though JPA is only concerned with the inverse side when making changes to the database. We can either make the assignment here ...
applicant.setApplication(this); //must maintain inverse side
... or from the code that called the ctor(application) in the first place.
Application application = new Application(applicant);
applicant.setApplication(application); //must maintain inverse side
Add the two entity classes to your persistence unit.
<class>myorg.relex.one2one.Applicant</class>
<class>myorg.relex.one2one.Application</class>
Attempt to build the module and note the error from the provider attempting to map the Application entity properties.
$ 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)]
The error occurs because...
The entity uses FIELD access and an un-annotated field property was found that has no default mapping
The referenced Application entity does not implement Serializable and cannot be stuffed into a BLOB column within Applicant (and nor do we want it to)
Lets initially get beyond the error by marking the property as @Transient. This will allow the Java attribute to exist in memory but will not have any mapping to the database. That may be what we ultimately want for some cases, but not here. We are only using @Transient to get back to a stable state while we work through any other mapping issues in front of us.
@Transient
private Application application;
Re-build the module and observe the generated database schema so far. Notice the expected uni-direction behavior has been recreated with our current mapping.
$ 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;
Currently we are seeing...
The inverse/parent Applicant entity table has no reference to the owning/dependent Application since it is @Transient
The owning/dependent Application entity table has a foreign key reference to the inverse Applicant entity table.
The owning/dependent Application entity is realizing the relation through a foreign key join rather than a primary key join.
Fix the mapping from the owning/dependent Application to the inverse/parent Applicant entity by adding @MapsId to the owning side definition.
@MapsId //foreign key realizes primary key
@OneToOne(//lack of mappedBy identifies this as owning side
optional=false, fetch=FetchType.EAGER)
private Applicant applicant;
Re-build the module and observe the generated database schema so far. Notice the former ID primary key column for the owning/dependent Application entity table was removed and its role taken by the APPLICANT_ID foreign key column because of the @MapsId annotation.
$ 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;
Since primary keys cannot be optional, only mandatory relationships can be created through primary keys joins. The parent can not be deleted without also removing the dependent entity (first). The inverse side must be in place with the primary key to be shared. The owning side of primary key join cannot be the entity generating the primary key.
Attempt to fix the parent entity by replacing the @Transient specification with a @OneToOne relationship mapping. However, in doing it exactly this way we are causing an error with the database mapping we will soon see...
@OneToOne(
// mappedBy="applicant", //identifies property on owning side
fetch=FetchType.LAZY)
// @Transient
private Application application;
Re-build the module and observe the generated database schema. Notice that our "inverse" Applicant entity table has inherited an unwanted database column ("application_applicant_id") and foreign key to the "owning" Application entity table. That circular reference is not a bi-directional relationship -- it is two, independent uni-directional relationships.
$ 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;
Fix the mistaken mapping by making the parent entity the inverse side of the relationship using the property "mappedBy".
@OneToOne(
mappedBy="applicant", //identifies property on owning side
fetch=FetchType.LAZY)
private Application application;
Re-build the module and observe the generated database schema. We now have the database schema we need to implement a one-to-one, bi-directional relationship realized through a common, generated primary key value.
$ 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;
We now have...
The parent Applicant entity table has a generated PK
The parent/inverse Applicant entity table has no foreign key reference to the dependent/owning Application entity table.
The dependent/owning Application entity table has a foreign key reference to the parent/inverse Applicant entity table. This is used to form the relationship.
The parent/inverse Applicant with the generated primary key can be inserted at any time.
The dependent/owning Application with the non-null foreign key can only be inserted after the parent/inverse Application entity
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
}
Build the module, run the new test method, and notice the database output shows a good bit of what we expect from our uni-directional experience.
$ 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 (?, ?)
Add the following to the test method to verify the actions to the database when the entities are being found from the owning/dependent side of the relationship. This should be similar to our uni-directional case since we are using the entity class with the foreign key in the find(). However, our mapping seems to cause some additional database interaction.
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());
Re-build the module, run the new test method, and notice the database output contains three select statements, including an extra select for the owning side after both the owning and inverse sides have been retrieved during the EAGER fetch.
$ 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...
Add the following to the test method to verify the actions to the database when the entities are being found from the inverse/parent side of the relationship. This is something we could not do in the uni-directional case since the only one side of the relationship knew about the other.
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()));
Re-build the module, run the test method, and notice the database output shows the inverse/parent being obtained first by primary key and then the owning/dependent entity being obtained through its foreign key/primary key.
$ 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...
Hardly scientific, but know that in this mapping case and provider software version, we end up saving one query to the database when accessing our primary key joined entities form the inverse/parent side of the bi-directional relationship.
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=?
The previous case dealt with a 1:1 relationship where the entities were tightly coupled with one another -- sharing the same primary key. In this case we will look at 0..1 relationship that must provide the flexibility to be optional as well as re-assigned.
Add the following inverse/parent entity class to your src/main tree. It is currently incomplete and has a common error that will cause mapping issues in a short while.
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;
}
}
The above entity class was assigned an override for the entity name ('@Entity(name="RelationAuto")') so that it would not conflict with the Auto entity provided in the initial template.
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>
Build the module and observe the generated database schema. We have repeated the error from the previous section where two uni-directional relationships where defined instead of a single bi-directional relationship. We should have no foreign key relationship from the inverse/parent entity table (AUTO) to the owning/dependent entity table (DRIVER).
$ 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;
Correct the relationship specification in the inverse/parent entity class by adding a "mappedBy" property that references the incoming property from the owning/dependent entity.
@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;
We now have...
An inverse/parent entity table (AUTO) with no reference to the owning/dependent entity table (DRIVER)
The owning/dependent entity table (DRIVER) has a foreign key separate from its primary key to reference the inverse/parent entity class (AUTO)
The foreign key is constrained to be non-null since the Auto entity was defined to be required by the Driver entity relationship.
If we had switched owning/inverse roles between the two entity classes, then a foreign key to the DRIVER in the AUTO would have been nullable.
Add the following as a new test method in your existing one-to-one test case. It currently has a persistence ordering problem that will cause an error in a following step.
@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);
}
The relationship owner/dependent entity is being persisted before the inverse/parent entity. This means the inverse/parent will be transient and not have a primary key value when the owning/dependent entity is persisted -- thus cause an issue persisting the relationship.
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:(..)
Correct the ordering of the persist() requests so the inverse/parent entity is persisted prior to the owning/dependent entity.
em.persist(auto);
em.persist(driver);
Re-build the module, execute the new test method, and observe the successful persist() of both entities.
$ 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());
Re-build the module, re-run the test method, and observe the database activity to obtain the entities from the owning/dependent-side. For some reason, the provider makes two selects to fully resolve both the owning and inverse sides of the relationship using EAGER fetch mode from the inverse side. If you look closely at the where clauses...
the first appears to be attempting to locate the specific Driver we were looking for in the find()
the second appears to be populating the required Auto property of the Driver -- not realizing the first query already resolved the entity and, in this case, the Driver instance we just came from has to be the one for this Auto.
$ 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...
The above is not an indication of an error. It is an indication that there is always room to analyze the automatically generated queries and create manual optimizations where necessary and appropriate.
Add the additional tests to verify access to the entity pair from the inverse/parent side of the relationship.
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());
Re-build the module, re-run the test method, and observe the database output from the additional steps.
$ 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...
Add the following to test the 0..1, optional aspects of the Driver relative to the Auto. In this case we are deleting the Driver and should not get one back during the next pull from the database.
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());
Re-build the module, re-run the test method, and observe the database output from the additional steps. The Driver has been updated to reference a different Auto object. That means the existing Auto object no longer has a Driver in the database.
$ 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()));
Re-build the module, re-run the test method, and notice from the successful delete of the objects using the supplied ordering.
You have finished implementing a one-to-one, bi-directional relationship with 0..1 semantics from the inverse to owning side (i.e., owning side optional and/or changeable). In the next section we will quickly run through an example where we make the owning side mandatory and the inverse side optional.
The previous case the owning/dependent side of the relationship was optional and the inverse/parent side was mandatory. In this quick example we want to switch the database mapping so the owning/dependent side is optional. We will use a copy of the entity classes we used last time except switch owning/inverse sides.
Copy the Auto entity class to Auto2 and change the relationship ownership from inverse to owning. Also assign the entity to a new database table (AUTO2). and be sure to update all references to the Driver class to Driver2.
@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;
}
Copy the Driver entity class to Driver2 and change the relationship ownership from owning to inverse. Also assigned the entity to a new database table (DRIVER2) and be sure to update all references of the Auto class to Auto2.
@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>
Build the module and observe the generated database schema. I have included both versions below. Notice...
The foreign key has switched from the Driver entity table to the Auto entity table.
The foreign key is nullable when defined in the Auto entity table since the Driver is optional in the relationship.
The foreign key to the Auto is not constrained to be unique. That surprises me since this is a one-to-one relationship and not two Autos should be referencing the same Driver (If so, we would have a Many-To-One). However, later we will see the the provider enforcing the uniqueness in code rather than the database.
$ 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;
Copy the previous test method and change all Auto class references to Auto2 and all Driver references to Driver2. You might find it easier to copy the test method in blocks starting with the creates.
@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();
}
If you build and run the test method for just the persist() portion you should notice the following results.
Auto is inserted without a reference to the Driver
Driver is inserted with no knowledge of Auto
Auto is updated with foreign key to Driver
$ 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();
...we could avoid the extra update call since the foreign key value value for Driver would be known at the time the Auto was persisted in this second case.
$ 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, ?, ?)
Add the following to your test method to obtain the entity pair from the inverse/parent side. Note to change the types from Driver to Driver2 as well as the logging and any comments dealing with inverse, parent, and dependent.
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());
Re-build the module, re-run the test method, and observe the actions taken with the database when accessing the relationship from the inverse/parent side of the relationship. Notice how the joins have been replaced by multiple selects and we have eliminated the redundancy of database calls using this combination.
$ 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...
Add the following to obtain the pair of entities from the owning side. Note to change all Auto references to Auto2 and update comments and log statements referencing parent, inverse, and 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());
Re-build the module, re-run the test method, and observe the actions taken with the database when accessing the entities from the owning side of the relation. The owning/dependent entity is first located by itself due to the LAZY and optional specification of the Auto. The inverse/parent entity is located and then its reference back to the Auto is independently populated with an extra call.
$ 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=?
Add the following to the test method to test changing the Driver from one Auto to another and demonstrating the database interactions that occur now that the relationship is on the Auto side and not the Driver side. Note that with the change in database mapping we must manually clear the relationship from the previous Auto before assigning the Driver to a new Auto. That has not been done below and will cause an error.
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());
Remember what happened in the previous case. We created a new Auto instance and then updated the Driver.FK to reference that instance. Since the first Auto was no longer referenced, it had no Driver.
Re-build the module, re-run the test method, and observe the difference in database interactions. We again create a new Auto instance and assign it to the Driver. However, with the new database mapping the assignment is within the new Auto. The first Auto instance is still holding holding onto its reference at this point and causing the provider to fail. This is the uniqueness constraint I was talking about earlier when we reviewed the database schema.
$ 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
Update the test method to clear the Driver reference from the first Auto prior to assigning the Driver to the new Auto. This is required because we have a 1:1 relationship and only a single Driver can be referenced by a single Auto. Before we did not do this because the truck.setDriver() was updating the owning side. Now it is updating the inverse side -- which is not of interest to the JPA provider.
auto2.setDriver(null); //must remove reference to former driver
truck.setDriver(driver);//prior to assigning to new driver for 1:1
Re-build the module, re-run the updated test method, and observe the new interactions with the database that allow the modification to complete. The new owning/dependent entity instance (truck) is created, the first owning/dependent entity instance (auto) is cleared of its driver, and then the new owning/dependent entity instance (truck) us updated to reference the driver.
$ 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=?
We could eliminate one of the database updates by moving the persist() of the truck to after the driver was set.
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()));
Re-build the module, re-run the test method, and notice the interaction that occurs with the database. The provider allows the Driver to be deleted first -- but first clears the Auto of the foreign key reference and then moves on to the rest of the deletes.
$ 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=?
We can get rid of the extra database update if we rearrange the deletes to remove the owning/dependent entities first.
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=?
You have finished mapping a one-to-one, bi-directional relationship that uses a 0..1 relationship for the inverse/parent side. This caused the foreign key to be moved to the optional (Auto) side of the relationship where the it was allowed to be nullable and had to be kept unique.
In this section will will go through some automated actions your EntityManager provider can do for your application behind the scenes. These actions automate what you would otherwise need to do in extra EntityManager calls or additional tracking of entity use. The first case is broad in scope and applies to cascading actions that occur during the other CRUD actions. The second case is confined to the removal of orphaned parent entities.
In this example we will demonstrate how cascades can be setup and automated from the owning side of a relationship.
Put the following entity class in place in your src/main tree. This class will be the passive/ignorant side of a one-to-one, uni-directional relationship.
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;
}
}
Put the following entity clas in place in your src/main tree. This class will be the owning side of a one-to-one, uni-directional relationship. It is currently incomplete and will need to be updated later.
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>
Build the module and verify the database schema created for this exercise is as follows. The specific schema has very little to do with implementing the JPA cascades, but it is of general interest to any JPA mapping exercise.
$ 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]
The problem is the test method only persisted the licapp and not the license that it references. We could fix this by adding a call to em.persist(license) prior to calling em.persist(licapp), but lets solve this with cascades instead.
It is not always appropriate for the dependent entity to create the missing parent, but in this case we are going to rationalize this it is appropriate -- especially when the instance is there and all we want to to automate the persistence of the overall (small) object tree. Update the relationship in the licapp to cascade persist calls to the related License entity.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST
})
private License license;
Re-build the module, re-run the test method and observe that the error has gone away and the license is now being automatically persisted with the licapp.
$ 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, ?, ?)
Put the following test code in place to demonstrate behavior of cascading a call to detach(). This section of the test looks to detach the existing entities and then instantiate new instances within the local cache.
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();
Re-build the module, attempt to re-run the unit test, and observe the following error when asserting the detached state.
$ 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;
Re-build the module, re-run the test method, and observe how the change allowed the detach() to propagate down to the license entity.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2OneTest#testOne2OneCascadeFromOwner ... [INFO] BUILD SUCCESS
Add the following to your test method in order to demonstrate how to cascade refresh() to relationships. The strategy derives a new renewal and modified date/timestamp for the license and licapp entities, updates the database directly through a bulk query, and then synchronizes the entity state with the database using refresh().
Bulk query changes bypass the entity cache and render any cached instances for updated database rows out of sync.
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());
Re-build the module and attempt to run the additional tests. This will fail since we only synchonized the licapp with the database state.
$ 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;
Re-build the module, re-run the test method, and observe the successful results for the refresh() being cascaded to the license. Notice how both entities are re-fetched from the database.
$ 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
Add the following to your test method to demonstrate the ability to cascade merge() calls thru relationships. The code updates two detached entities, merges them using the entity manager, and then checks the state of the resultant managed entities.
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());
Re-build the module, re-run the test method, and observe the error that occurs. The problem is that only changes to the licapp are considered during the merge. Any changes to license is ignored.
$ 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
})
Re-build the module, re-run the test method, and observe how the test now passes. Updates from licapp and license are issued to the database.
$ 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
Add the following to your test method to demonstrate cascades for the remove() method. In this code we attempt to remove just the licapp but expect both the licapp and related license to be deleted from the database when 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()));
Re-build the module, attempt to run the updated test method, and observe the following error. The trouble is only the licapp is deleted from the database and the license is being ignored.
$ 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;
Re-build the module, re-run the test method, and observe how the tests now pass. Both the licapp and license are being deleted during the call to remove().
$ 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
You have finished implementing cascade functionality from the owning side of a one-to-one relationship. As you can see, the cascade capability can save extract calls to the EntityManager when there is a large object graph to be persisted or acted on in other ways. However, as you can also guess -- not every call should be cascaded at all times. Use this capability wisely as you can encounter many situations where the cascade could lead to severe issues or at least poorer performance than desired.
In this example we will demonstrate how cascades can be setup and automated from the inverse side of a relationship.
Add the following inverse/parent entity to your src/main source tree. It is currently incomplete and will cause errors later when we attempt to cascade entity manager commands from the inverse to owning side.
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;
}
}
Add the following owning/dependent entity class to your src/main tree. Although this class will own the foreign key and JPA relation, it is designed to not initiate any cascade calls from the owning side to the inverse side.
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>
Add the following test method to your existing one-to-one JUnit test case. At this point in time we are testing the ability to cascade the PERSIST call from the inverse/parent side of the relation to the owning/dependent side.
@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));
}
Build the module and attempt to run the new unit test. The test will fail at this point because the inverse side has not been configured to cascade the PERSIST call to the owning side.
$ 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;
Rebuild the module and re-run the unit test to verify the persist is now being cascaded from the inverse to owning side of the relationship.
$ 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
Add the following code snippet to the test method to verify the capability to cascade a DETACH from the inverse to owning side of a relationship.
log.debug("detach both instances from the persistence context");
em.detach(ticket);
assertFalse("ticket managed", em.contains(ticket));
assertFalse("passenger managed", em.contains(passenger));
Rebuild and attempt to re-run the test method. The test method will fail because the inverse side is not configured to cascade the DETACH.
$ 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
Add the following code snippet to your test method to demonstrate the capability to cascade a REFRESH.
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()));
Rebuild the module and attempt to re-run the test method. It should fail at this point because your inverse side is not configured to cascade the REFRESH. Notice how only the state for the ticket was retrieved from the database during the refresh() call.
$ 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
Fix the issue by configuring the inverse side to cascade the REFRESH so that both entities will be updated with the state of the database when the inverse side is refreshed.
@OneToOne(mappedBy="ticket", fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REFRESH,
// CascadeType.MERGE,
// CascadeType.REMOVE
})
private Passenger passenger;
Rebuild the module and re-run the test method. It should pass at this point. Notice how both the state for the inverse and owning side is retrieved from the database during the refresh of the inverse side.
$ 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
Add the following code snippet to your test method to demonstrate the ability to cascade a MERGE from the inverse side.
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()));
Rebuild the module and attempt to run the updated test method. This will fail because the inverse side is not yet configured to cascade the MERGE. Notice how only the inverse side is being updated and not the owning entity.
$ 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;
Rebuild the module and re-run the test method. The test method should now pass because to MERGE is bing cascaded from the inverse to owning side of the relation. Notice how both sides are being updated.
$ 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()));
Rebuild the module and attempt to run the updated test method. This will fail because the inverse side is not configured to cascade the DELETE. Notice how only the inverse side is being deleted from the database during the call to remove.
$ 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;
Rebuild the module and re-run the test method. The test method should pass this time because the DELETE is now configured to be cascaded from the inverse to owning side. Notice how both sides of the relationship are being deleted from the database, starting with the owning/dependent side to prevent a foreign key constraint violation.
$ 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
You have now finished demonstrating how entity manager actions can be automatically cascaded from the inverse/parent side of a relationship to the owning/dependent side of a bi-directional relationship. This is not always an appropriate or desired behavior but it is notable that cascade direction and relationship ownership are independent of one another. You can configure cascades to be originated from the owning or inverse side of a relationship.
Orphan removal can be handy when the target of a OneToXxx relationship becomes unreferenced by its referencing entity and the target entity only exists to support that referencing entity. This is different from cascade=DELETE. In this case the referencing entity is not being deleted. It is de-referencing the parent. The basic rules are as follows.
Referencing entity removes (nulls in the case of OneToOne) its relationship to the target entity
em.remove() on the orphaned target entity is applied
Orphaned target entity must not be reassigned to new child. It exists for the sole use of the referencing entity.
Not necessary to declare cascade=DELETE for this relationship
Put the following entity class in your src/main tree. This entity class represents a parent entity that exists solely for the use of a dependent entity that will be related to it in a follow-on step.
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;
}
}
Put the following entity class in your src/main tree. This entity provides an example of a dependent entity with a parent that only exists to support this instance. If this instance ceases to reference the parent -- the parent will be removed by the provider.
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>
Add the following test method to your existing one-to-one JUnit test case. At this point in time, the test just creates an instance of the two related entities. Consistent with the concept that the parent is only there to support the parent -- we have maintained no reference to the parent except through the dependent entity.
@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));
}
Build the module and run the new test method. Notice how the two entities are inserted into the database and the foreign key column for the dependent entity table is set to reference the parent.
$ 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.
In this chapter we mapped two entity classes through a one-to-one relationship using multiple techniques and under different situations.
Uni-directional and Bi-directional Relationships
Simple and Composite Primary/Foreign Key
Primary Key, Foreign Key and Link Table Joins
Cascading actions across relationships
Cascade and Orphan Removal