Introduction
Mapping one-to-many relationship with JPA 2.0, can be done in 2 ways:
- @OneToMany, with optional @JoinColumn
- @OneToMany with @JoinTable
Here I will look closer on option one, which is most common. @JoinTable is more common with @ManyToMany relationship/mapping.
Databas Design
_____________ _____________
| | | |
| Employee | | Phone |
|_____________| |_____________|
| | | |
| *employeeId | --> | *phoneId |
| firstName | | *employeeId |
|_____________| | areaCode |
| |
|_____________|
Bidrectional OneToMany Mapping
package se.magnuskkarlsson.example.jpa;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long employeeId;
@OneToMany(mappedBy = "owner")
private List<Phone> phones;
@Column
private String firstName;
// ----------------------- Logic Methods -----------------------
public void addPhone(Phone phone) {
this.getPhones().add(phone);
if (phone.getOwner() != this) {
phone.setOwner(this);
}
}
// ----------------------- Helper Methods -----------------------
// ----------------------- Get and Set Methods -----------------------
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public List<Phone> getPhones() {
if (phones == null) {
phones = new ArrayList<Phone>();
}
return phones;
}
public void setPhones(List<Phone> phones) {
this.phones = phones;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
package se.magnuskkarlsson.example.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Phone {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long phoneId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "OWNER_ID")
private Employee owner;
@Column
private int areaCode;
// ----------------------- Logic Methods -----------------------
// ----------------------- Helper Methods -----------------------
// ----------------------- Get and Set Methods -----------------------
public Long getPhoneId() {
return phoneId;
}
public void setPhoneId(Long phoneId) {
this.phoneId = phoneId;
}
public Employee getOwner() {
return owner;
}
public void setOwner(Employee employee) {
this.owner = employee;
if (!employee.getPhones().contains(this)) {
// warning this may cause performance issues if you have a large
// data set since this operation is O(n)
employee.getPhones().add(this);
}
}
public int getAreaCode() {
return areaCode;
}
public void setAreaCode(int areaCode) {
this.areaCode = areaCode;
}
}
Now lets write some test code to test this.
private EntityManagerFactory emf;
private EntityManager em;
@Before
public void setUp() throws Exception {
emf = Persistence.createEntityManagerFactory("it");
em = emf.createEntityManager();
}
@After
public void tearDown() throws Exception {
em.close();
emf.close();
}
@Test
public void testRelationship() throws Exception {
em.getTransaction().begin();
Phone phone1 = new Phone();
em.persist(phone1);
Phone phone2 = new Phone();
em.persist(phone2);
Employee emp = new Employee();
emp.addPhone(phone1);
emp.addPhone(phone2);
em.persist(emp);
em.getTransaction().commit();
em.getTransaction().begin();
// SELECT DISTINCT
// http://stackoverflow.com/questions/8199512/jpql-inner-join-without-duplicate-records
// JOIN FETCH
// https://en.wikibooks.org/wiki/Java_Persistence/Relationships#Join_Fetching
String sql = "SELECT DISTINCT e FROM Employee e JOIN FETCH e.phones";
TypedQuery<Employee> query = em.createQuery(sql, Employee.class);
List<Employee> list = query.getResultList();
System.out.println("QUERY RESULT");
for (Employee e : list) {
System.out.println(e);
for (Phone p : e.getPhones()) {
System.out.println(" " + p);
}
}
em.getTransaction().commit();
}
And the debug output from Hibernate
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
insert
into
Employee
values
( )
Hibernate:
update
Phone
set
OWNER_ID=?
where
id=?
Hibernate:
update
Phone
set
OWNER_ID=?
where
id=?
Hibernate:
select
distinct employee0_.EMP_ID as EMP_ID1_1_0_,
phones1_.id as id1_2_1_,
phones1_.OWNER_ID as OWNER_ID2_2_1_,
phones1_.OWNER_ID as OWNER_ID2_1_0__,
phones1_.id as id1_2_0__
from
Employee employee0_
inner join
Phone phones1_
on employee0_.EMP_ID=phones1_.OWNER_ID
QUERY RESULT
se.magnuskkarlsson.example.jpa.Employee@232864a3
se.magnuskkarlsson.example.jpa.Phone@84c5310
se.magnuskkarlsson.example.jpa.Phone@32dcce09
As we can see the above create operation, generates 5 SQL statements (3 INSERTS and 2 UPDATES).
The same behavior with multiple INSERT followed by UPDATE, happens when you want to create a new Phone. Then you need to load the Employee
class and then call addPhone(Phone)
. This is not optimal as you can see. Now lets consider another approach with unidirectional mapping.
First Attempt Unidirectional OneToMany Mapping
First we update our mapping annotations./p>
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long employeeId;
// @OneToMany(mappedBy = "owner")
@OneToMany
@JoinColumn(name = "employeeId", referencedColumnName = "employeeId")
private List<Phone> phones;
@Column
private String firstName;
public void addPhone(Phone phone) {
this.getPhones().add(phone);
phone.setEmployeeId(getEmployeeId());
}
// the rest is left out for brevity
}
@Entity
public class Phone {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long phoneId;
@Column(nullable = false)
private Long employeeId;
@Column
private int areaCode;
// the rest is left out for brevity
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
insert
into
Employee
values
( )
Hibernate:
update
Phone
set
OWNER_ID=?
where
id=?
Hibernate:
update
Phone
set
OWNER_ID=?
where
id=?
Hibernate:
select
distinct employee0_.EMP_ID as EMP_ID1_1_0_,
phones1_.id as id1_2_1_,
phones1_.OWNER_ID as OWNER_ID2_2_1_,
phones1_.OWNER_ID as OWNER_ID2_1_0__,
phones1_.id as id1_2_0__
from
Employee employee0_
inner join
Phone phones1_
on employee0_.EMP_ID=phones1_.OWNER_ID
QUERY RESULT
se.magnuskkarlsson.example.jpa.Employee@46764885
se.magnuskkarlsson.example.jpa.Phone@6ecc295c
se.magnuskkarlsson.example.jpa.Phone@7c2a88f4
As we can see, we still need 5 SQL operations. But we can do better. Think how the underlying SQL is working. There is nothing magic when JPA or ORM. Lets first create parent Employee
and then create children Phone
and explicit set foreign key.
Second Attempt Unidirectional OneToMany Mapping
Mapping is the same, but "business logic" is changed.
Employee emp = new Employee();
emp.setFirstName("Bob");
em.persist(emp);
Phone phone1 = new Phone();
phone1.setEmployeeId(emp.getEmployeeId());
phone1.setAreaCode(613);
em.persist(phone1);
Phone phone2 = new Phone();
phone2.setEmployeeId(emp.getEmployeeId());
phone2.setAreaCode(416);
em.persist(phone2);
// Employee is cached in first second cache, since reusing Session
em.detach(emp);
Hibernate:
insert
into
Employee
values
( )
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
insert
into
Phone
(OWNER_ID)
values
(?)
Hibernate:
select
distinct employee0_.EMP_ID as EMP_ID1_1_0_,
phones1_.id as id1_2_1_,
phones1_.OWNER_ID as OWNER_ID2_2_1_,
phones1_.OWNER_ID as OWNER_ID2_1_0__,
phones1_.id as id1_2_0__
from
Employee employee0_
inner join
Phone phones1_
on employee0_.EMP_ID=phones1_.OWNER_ID
QUERY RESULT
se.magnuskkarlsson.example.jpa.Employee@a7072fd
se.magnuskkarlsson.example.jpa.Phone@232864a3
se.magnuskkarlsson.example.jpa.Phone@72f1b266
As we can see the number of SQL statement is now reduced to the natural number that is sensible needed. And when we need to create a new child Phone
we do not need to load and add it to parent Employee
.
Is there any downside with this solution. Not really if we would like bidirectional behavior we could add a @Transient
parameter and set it lazy, just as we would have done with if the mapping was bidirectional. One would maybe think of keeping bidirectional mapping eager, but that is not a good pattern/practice, since you could risk to load huge object graphs into memory, which would impact the performance negative.
@Entity
public class Phone {
@Transient
private Employee employee;
// the rest is left out for brevity
}
The Rest of the Files
To make this complete here is the rest of the code.
src/test/resources/META-INF
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="it" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>se.magnuskkarlsson.example.jpa.Company</class>
<class>se.magnuskkarlsson.example.jpa.Employee</class>
<class>se.magnuskkarlsson.example.jpa.Phone</class>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<validation-mode>CALLBACK</validation-mode>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<!--
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<property name="hibernate.connection.driver_class" value="org.h2.Driver" />
<property name="hibernate.connection.url" value="jdbc:h2:mem:" />
-->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/EXAMPLE" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="root" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.generate_statistics" value="false" />
</properties>
</persistence-unit>
</persistence>
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>se.magnuskkarlsson.examples</groupId>
<artifactId>example-jpa</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Example JPA</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputEncoding>UTF-8</project.build.outputEncoding>
<hibernate-version>4.2.14.Final</hibernate-version>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>provided</scope>
</dependency>
<!-- Test Support -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.185</version>
<scope>test</scope>
</dependency>
<!-- <dependency> <groupId>org.apache.derby</groupId> <artifactId>derbyclient</artifactId>
<version>10.7.1.1</version> <scope>test</scope> </dependency> -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-infinispan</artifactId>
<version>${hibernate-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.1.Final</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<debug>true</debug>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.5</version>
<configuration>
<ejbVersion>3.1</ejbVersion>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.5</version>
</plugin>
</plugins>
</build>
</project>