Java Persistence/ManyToMany
ManyToMany
[edit | edit source]A ManyToMany
relationship in Java is where the source object has an attribute that stores a collection of target objects and (if) those target objects had the inverse relationship back to the source object it would also be a ManyToMany
relationship. All relationships in Java and JPA are unidirectional, in that if a source object references a target object there is no guarantee that the target object also has a relationship to the source object. This is different than a relational database, in which relationships are defined through foreign keys and querying such that the inverse query always exists.
JPA also defines a OneToMany
relationship, which is similar to a ManyToMany
relationship except that the inverse relationship (if it were defined) is a ManyToOne
relationship. The main difference between a OneToMany
and a ManyToMany
relationship in JPA is that a ManyToMany
always makes use of a intermediate relational join table to store the relationship, where as a OneToMany
can either use a join table, or a foreign key in target object's table referencing the source object table's primary key.
In JPA a ManyToMany
relationship is defined through the @ManyToMany
annotation or the <many-to-many>
element.
All ManyToMany
relationships require a JoinTable
. The JoinTable
is defined using the @JoinTable
annotation and <join-table>
XML element. The JoinTable
defines a foreign key to the source object's primary key (joinColumns
), and a foreign key to the target object's primary key (inverseJoinColumns
). Normally the primary key of the JoinTable
is the combination of both foreign keys.
Example of a ManyToMany relationship database
[edit | edit source]EMPLOYEE (table)
ID | FIRSTNAME | LASTNAME |
1 | Bob | Way |
2 | Sarah | Smith |
EMP_PROJ (table)
EMP_ID | PROJ_ID |
1 | 1 |
1 | 2 |
2 | 1 |
PROJECT (table)
ID | NAME |
1 | GIS |
2 | SIG |
Example of a ManyToMany relationship annotation
[edit | edit source]@Entity
public class Employee {
@Id
@Column(name="ID")
private long id;
...
@ManyToMany
@JoinTable(
name="EMP_PROJ",
joinColumns=@JoinColumn(name="EMP_ID", referencedColumnName="ID"),
inverseJoinColumns=@JoinColumn(name="PROJ_ID", referencedColumnName="ID"))
private List<Project> projects;
.....
}
Example of a ManyToMany relationship XML
[edit | edit source]<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id">
<column name="EMP_ID"/>
</id>
<set name="projects" table="EMP_PROJ" lazy="true" cascade="none" sort="natural" optimistic-lock="false">
<key column="EMP_ID" not-null="true" />
<many-to-many class="com.flipswap.domain.Project" column="PROJ_ID" />
</set>
</attributes>
</entity>
Bi-directional Many to Many
[edit | edit source]Although a ManyToMany
relationship is always bi-directional on the database, the object model can choose if it will be mapped in both directions, and in which direction it will be mapped in. If you choose to map the relationship in both directions, then one direction must be defined as the owner and the other must use the mappedBy
attribute to define its mapping. This also avoids having to duplicate the JoinTable
information in both places.
If the mappedBy
is not used, then the persistence provider will assume there are two independent relationships, and you will end up getting duplicate rows inserted into the join table. If you have a conceptual bi-directional relationship, but have two different join tables in the database, then you must not use the mappedBy
, as you need to maintain two independent tables.
As with all bi-directional relationships it is your object model's and application's responsibility to maintain the relationship in both direction. There is no magic in JPA, if you add or remove to one side of the collection, you must also add or remove from the other side, see object corruption. Technically the database will be updated correctly if you only add/remove from the owning side of the relationship, but then your object model will be out of synch, which can cause issues.
Example of an inverse ManyToMany relationship annotation
[edit | edit source]@Entity
public class Project {
@Id
@Column(name="ID")
private long id;
...
@ManyToMany(mappedBy="projects")
private List<Employee> employees;
...
}
See Also
[edit | edit source]Common Problems
[edit | edit source]Object not in collection after refresh.
[edit | edit source]- If you have a bi-directional
ManyToMany
relationship, ensure that you add to both sides of the relationship. - See Object corruption.
Additional columns in join table.
[edit | edit source]Duplicate rows inserted into the join table.
[edit | edit source]- If you have a bidirectional
ManyToMany
relationship, you must usemappedBy
on one side of the relationship, otherwise it will be assumed to be two different relationships and you will get duplicate rows inserted into the join table.
Advanced
[edit | edit source]Mapping a Join Table with Additional Columns
[edit | edit source]A frequent problem is that two classes have a ManyToMany
relationship, but the relational join table has additional data. For example if Employee
has a ManyToMany
with Project
but the PROJ_EMP join table also has an IS_PROJECT_LEAD
column. In this case the best solution is to create a class that models the join table. So a ProjectAssociation
class would be created. It would have a ManyToOne
to Employee
and Project
, and attributes for the additional data. Employee
and Project
would have a OneToMany
to the ProjectAssociation
. Some JPA providers also provide additional support for mapping to join tables with additional data.
Unfortunately mapping this type of model becomes more complicated in JPA because it requires a composite primary key. The association object's Id
is composed of the Employee
and Project
ids. The JPA 1.0 spec does not allow an Id
to be used on a ManyToOne
so the association class must have two duplicate attributes to also store the ids, and use an IdClass
, these duplicate attributes must be kept in synch with the ManyToOne
attributes. Some JPA providers may allow a ManyToOne
to be part of an Id
, so this may be simpler with some JPA providers. To make your life simpler, I would recommend adding a generated Id
attribute to the association class. This will give the object a simpler Id
and not require duplicating the Employee
and Project
ids.
This same pattern can be used no matter what the additional data in the join table is. Another usage is if you have a Map
relationship between two objects, with a third unrelated object or data representing the Map
key. The JPA spec requires that the Map
key be an attribute of the Map
value, so the association object pattern can be used to model the relationship.
If the additional data in the join table is only required on the database and not used in Java, such as auditing information, it may also be possible to use database triggers to automatically set the data.
Example join table association object database
[edit | edit source]EMPLOYEE (table)
ID | FIRSTNAME | LASTNAME |
1 | Bob | Way |
2 | Sarah | Smith |
PROJ_EMP (table)
EMPLOYEEID | PROJECTID | IS_PROJECT_LEAD |
1 | 1 | true |
1 | 2 | false |
2 | 1 | false |
PROJECT (table)
ID | NAME |
1 | GIS |
2 | SIG |
Example join table association object annotations
[edit | edit source]@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(mappedBy="employee")
private List<ProjectAssociation> projects;
...
}
@Entity
public class Project {
@Id
private long id;
...
@OneToMany(mappedBy="project")
private List<ProjectAssociation> employees;
...
// Add an employee to the project.
// Create an association object for the relationship and set its data.
public void addEmployee(Employee employee, boolean teamLead) {
ProjectAssociation association = new ProjectAssociation();
association.setEmployee(employee);
association.setProject(this);
association.setEmployeeId(employee.getId());
association.setProjectId(this.getId());
association.setIsTeamLead(teamLead);
if(this.employees == null)
this.employees = new ArrayList<>();
this.employees.add(association);
// Also add the association object to the employee.
employee.getProjects().add(association);
}
}
@Entity
@Table(name="PROJ_EMP")
@IdClass(ProjectAssociationId.class)
public class ProjectAssociation {
@Id
private long employeeId;
@Id
private long projectId;
@Column(name="IS_PROJECT_LEAD")
private boolean isProjectLead;
@ManyToOne
@PrimaryKeyJoinColumn(name="EMPLOYEEID", referencedColumnName="ID")
/* if this JPA model doesn't create a table for the "PROJ_EMP" entity,
* please comment out the @PrimaryKeyJoinColumn, and use the ff:
* @JoinColumn(name = "employeeId", updatable = false, insertable = false)
* or @JoinColumn(name = "employeeId", updatable = false, insertable = false, referencedColumnName = "id")
*/
private Employee employee;
@ManyToOne
@PrimaryKeyJoinColumn(name="PROJECTID", referencedColumnName="ID")
/* the same goes here:
* if this JPA model doesn't create a table for the "PROJ_EMP" entity,
* please comment out the @PrimaryKeyJoinColumn, and use the ff:
* @JoinColumn(name = "projectId", updatable = false, insertable = false)
* or @JoinColumn(name = "projectId", updatable = false, insertable = false, referencedColumnName = "id")
*/
private Project project;
...
}
public class ProjectAssociationId implements Serializable {
private long employeeId;
private long projectId;
...
public int hashCode() {
return (int)(employeeId + projectId);
}
public boolean equals(Object object) {
if (object instanceof ProjectAssociationId) {
ProjectAssociationId otherId = (ProjectAssociationId) object;
return (otherId.employeeId == this.employeeId) && (otherId.projectId == this.projectId);
}
return false;
}
}
- If the given examples won't suit your expectations, try the solution indicated in this link: