Java 持久化/多对多
Java 中的 ManyToMany
关系是指源对象具有一个存储目标对象集合的属性,并且如果这些目标对象具有反向关系回到源对象,它也将是一个 ManyToMany
关系。Java 和 JPA 中的所有关系都是单向的,这意味着如果源对象引用目标对象,则不能保证目标对象也与源对象具有关系。这与关系数据库不同,在关系数据库中,关系通过外键定义,并进行查询,从而始终存在反向查询。
JPA 还定义了 一对多
关系,它类似于 ManyToMany
关系,除了反向关系(如果定义的话)是一个 ManyToOne
关系。JPA 中 OneToMany
和 ManyToMany
关系的主要区别在于 ManyToMany
始终使用中间关系联接表来存储关系,而 OneToMany
既可以使用联接表,也可以使用目标对象表中的外键引用源对象表的主键。
在 JPA 中,ManyToMany
关系是通过 @ManyToMany
注解或 <many-to-many>
元素定义的。
所有 ManyToMany
关系都需要一个 JoinTable
。JoinTable
是使用 @JoinTable
注解和 <join-table>
XML 元素定义的。JoinTable
定义了到源对象主键的外键(joinColumns
)和到目标对象主键的外键(inverseJoinColumns
)。通常,JoinTable
的主键是两个外键的组合。
EMPLOYEE (表)
ID | FIRSTNAME | LASTNAME |
1 | Bob | Way |
2 | Sarah | Smith |
EMP_PROJ (表)
EMP_ID | PROJ_ID |
1 | 1 |
1 | 2 |
2 | 1 |
PROJECT (表)
ID | NAME |
1 | GIS |
2 | SIG |
@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;
.....
}
<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>
尽管 ManyToMany
关系在数据库中始终是双向的,但对象模型可以选择是否在两个方向上进行映射,以及在哪个方向上进行映射。如果您选择在两个方向上映射关系,则必须将一个方向定义为所有者,而另一个方向必须使用 mappedBy
属性来定义其映射。这也避免了在两个地方重复 JoinTable
信息。
如果未使用 mappedBy
,则持久化提供程序将假定存在两个独立的关系,并且您最终将获得插入到联接表中的重复行。如果您具有概念上的双向关系,但在数据库中具有两个不同的联接表,则您不能使用 mappedBy
,因为您需要维护两个独立的表。
与所有双向关系一样,您的对象模型和应用程序负责维护两个方向上的关系。JPA 中没有魔法,如果您在集合的一侧添加或删除,则还必须在另一侧添加或删除,请参阅 对象损坏。从技术上讲,如果您只在关系的所有者一侧添加/删除,则数据库将被正确更新,但您的对象模型将不同步,这可能会导致问题。
@Entity
public class Project {
@Id
@Column(name="ID")
private long id;
...
@ManyToMany(mappedBy="projects")
private List<Employee> employees;
...
}
- 如果您有双向
ManyToMany
关系,请确保您在关系的双方都添加了对象。 - 请参阅 对象损坏。
- 请参阅 映射具有其他列的联接表
- 如果您有双向
ManyToMany
关系,您必须在关系的一侧使用mappedBy
,否则它将被假定为两个不同的关系,并且您将获得插入到联接表中的重复行。
一个常见问题是,两个类具有 ManyToMany
关系,但关系联接表具有其他数据。例如,如果 Employee
与 Project
具有 ManyToMany
关系,但 PROJ_EMP 联接表还具有 IS_PROJECT_LEAD
列。在这种情况下,最佳解决方案是创建一个类来模拟联接表。因此,将创建一个 ProjectAssociation
类。它将具有 ManyToOne
到 Employee
和 Project
,以及其他数据的属性。Employee
和 Project
将具有 OneToMany
到 ProjectAssociation
。一些 JPA 提供程序还提供对映射到具有其他数据的联接表的其他支持。
不幸的是,在 JPA 中,这种类型的模型映射变得更加复杂,因为它需要一个复合主键。关联对象的 Id
由 Employee
和 Project
的 id 组成。JPA 1.0 规范不允许在 ManyToOne
上使用 Id
,因此关联类必须具有两个重复属性来存储 id,并使用 IdClass
,这些重复属性必须与 ManyToOne
属性保持同步。一些 JPA 提供程序可能允许 ManyToOne
成为 Id
的一部分,因此这在某些 JPA 提供程序中可能更简单。为了简化您的操作,建议您向关联类添加一个生成的 Id
属性。这将使对象拥有一个更简单的 Id
,并且不需要复制 Employee
和 Project
的 id。
无论连接表中的额外数据是什么,都可以使用相同的模式。另一种用法是,如果您在两个对象之间具有 Map
关系,并且第三个无关对象或数据代表 Map
密钥。JPA 规范要求 Map
密钥是 Map
值的属性,因此可以使用“关联对象”模式来建模关系。
如果连接表中的额外数据仅在数据库中需要,而在 Java 中未使用,例如审计信息,那么也可以使用数据库触发器来自动设置数据。
EMPLOYEE (表)
ID | FIRSTNAME | LASTNAME |
1 | Bob | Way |
2 | Sarah | Smith |
PROJ_EMP (表)
EMPLOYEEID | PROJECTID | IS_PROJECT_LEAD |
1 | 1 | true |
1 | 2 | false |
2 | 1 | false |
PROJECT (表)
ID | NAME |
1 | GIS |
2 | SIG |
@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;
}
}
- 如果给定的示例不适合您的期望,请尝试此链接中指示的解决方案