Java Persistence/ManyToMany

From Wikibooks, open books for an open world
Jump to: navigation, search

ManyToMany[edit]

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]

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]

@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]

<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.Company"
				column="PROJ_ID" />
		</set>
    </attributes>
</entity>

Bi-directional Many to Many[edit]

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]

@Entity
public class Project {
  @Id
  @Column(name="PROJ_ID")
  private long id;
  ...
  @ManyToMany(mappedBy="projects")
  private List<Employee> employees;
  ...
}

See Also[edit]

Common Problems[edit]

Object not in collection after refresh.[edit]
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]
See Mapping a Join Table with Additional Columns
Duplicate rows inserted into the join table.[edit]
If you have a bidirectional ManyToMany relationship, you must use mappedBy on one side of the relationship, otherwise it will be assumed to be two difference relationships and you will get duplicate rows inserted into the join table.

Advanced[edit]

Mapping a Join Table with Additional Columns[edit]

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]

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]

@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);
 
    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:

http://giannigar.wordpress.com/2009/09/04/mapping-a-many-to-many-join-table-with-extra-column-using-jpa/