Java 持久性/OneToMany
Java 中的 OneToMany
关系是指源对象有一个属性存储目标对象的集合,并且如果这些目标对象具有反向关系回到源对象,则它将是 ManyToOne
关系。 Java 和 JPA 中的所有关系都是单向的,也就是说,如果源对象引用目标对象,则不能保证目标对象也与源对象存在关系。 这与关系数据库不同,在关系数据库中,关系通过外键定义,并进行查询,以便始终存在反向查询。
JPA 还定义了 ManyToMany
关系,它类似于 OneToMany
关系,只是反向关系(如果定义)是 ManyToMany
关系。 JPA 中 OneToMany
和 ManyToMany
关系的主要区别在于 ManyToMany
始终使用中间关系联接表来存储关系,而 OneToMany
可以使用联接表,也可以使用目标对象表中的外键来引用源对象表的主键。 如果 OneToMany
使用目标对象表中的外键,JPA 要求关系是双向的(必须在目标对象中定义反向 ManyToOne
关系),并且源对象必须使用 mappedBy
属性来定义映射。
在 JPA 中,OneToMany
关系通过 @OneToMany
注解或 <one-to-many>
元素定义。
EMPLOYEE (表)
EMP_ID | FIRSTNAME | LASTNAME | SALARY | MANAGER_ID |
1 | Bob | Way | 50000 | 2 |
2 | Sarah | Smith | 75000 | null |
PHONE (表)
ID | TYPE | AREA_CODE | P_NUMBER | OWNER_ID |
1 | home | 613 | 792-0000 | 1 |
2 | work | 613 | 896-1234 | 1 |
3 | work | 416 | 123-4444 | 2 |
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToMany(mappedBy="owner")
private List<Phone> phones;
...
}
@Entity
public class Phone {
@Id
private long id;
...
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="OWNER_ID")
private Employee owner;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones" target-entity="org.acme.Phone" mapped-by="owner"/>
</attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
<attributes>
<id name="id"/>
<many-to-one name="owner" fetch="LAZY">
<join-column name="OWNER_ID"/>
</many-to-one>
</attributes>
</entity>
请注意,此 @OneToMany
映射需要反向 @ManyToOne
映射才能完成,请参阅 ManyToOne。
关系是双向的,因此当应用程序更新关系的一端时,另一端也应该更新,并保持同步。 在 JPA 中,与 Java 一样,维护关系的责任在于应用程序或对象模型。 如果您的应用程序向关系的一端添加内容,那么它必须向另一端添加内容。
这可以通过对象模型中的 add 或 set 方法解决,这些方法处理关系的两端,因此应用程序代码不需要担心它。 有两种方法可以解决这个问题,您可以将关系维护代码仅添加到关系的一端,并仅从一端使用 setter(例如,将另一端设为 protected),或者将其添加到两端,并确保避免无限循环。
例如
public class Employee {
private List phones;
...
public void addPhone(Phone phone) {
this.phones.add(phone);
if (phone.getOwner() != this) {
phone.setOwner(this);
}
}
...
}
public class Phone {
private Employee owner;
...
/**
* You have to ensure that the previous owner of this phone is no longer the owner of this
* phone before you attribute it a new owner. Ensure this either by a
* @requires !this.employee.getPhones().contains(this) or by adding to the beginning of
* the method body:
* if(this.employee != null)
* this.employee.removePhone(this);
*/
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);
}
}
...
}
有些人希望 JPA 提供商拥有自动维护关系的“魔法”。 这实际上是 EJB CMP 2 规范的一部分。 但是问题是,如果对象被分离或序列化到另一个 VM,或者在被管理之前将新对象关联起来,或者在 JPA 范围之外使用对象模型,那么“魔法”就会消失,应用程序将不得不自己解决问题,因此通常最好将代码添加到对象模型中。 但是,一些 JPA 提供商确实支持自动维护关系。
在某些情况下,在添加子对象时实例化大型集合是不可取的。 一个解决方案是不映射双向关系,而是根据需要查询它。 此外,一些 JPA 提供商优化了他们的延迟集合对象来处理这种情况,因此您仍然可以向集合添加内容而无需实例化它。
对象和关系表之间的一个常见不匹配是,OneToMany
在 Java 中不需要反向引用,但在数据库中需要反向引用外键。 通常最好在 Java 中定义 ManyToOne
反向引用,如果您不能或不想这样做,那么可以使用中间联接表来存储关系。 这类似于 ManyToMany
关系,但是如果您向目标外键添加唯一约束,则可以强制它是 OneToMany
。
JPA 使用 @JoinTable
注解和 <join-table>
XML 元素定义联接表。 JoinTable
可以用于 ManyToMany
或 OneToMany
映射。
另请参阅,单向 OneToMany
EMPLOYEE (表)
EMP_ID | FIRSTNAME | LASTNAME |
1 | Bob | 可以 |
2 | Sarah | Smith |
3 | Sarah | Smith |
EMP_PHONE (表)
EMP_ID | PHONE_ID |
1 | 1 |
1 | 2 |
2 | 3 |
PHONE (表)
ID | TYPE | PHONE_ID | P_NUMBER |
1 | home | 1 | 792-0000 |
2 | work | 1 | 896-1234 |
3 | work | 2 | 123-4444 |
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToMany
@JoinTable
(
name="EMP_PHONE",
joinColumns={ @JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID") },
inverseJoinColumns={ @JoinColumn(name="PHONE_ID", referencedColumnName="ID", unique=true) }
)
// While Update this will also insert collection row another insert
private List<Phone> phones;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id">
<column name="EMP_ID"/>
</id>
<one-to-many name="phones">
<join-table name="EMP_PHONE">
<join-column name="EMP_ID" referenced-column-name="EMP_ID"/>
<inverse-join-column name="PHONE_ID" referenced-column-name="ID" unique="true" />
</join-table>
</one-to-many>
</attributes>
</entity>
- 参见对象损坏。
JPA 1.0 不支持没有 JoinTable
的单向 OneToMany
关系。从 JPA 2.0 开始,支持单向 OneToMany
。在 JPA 2.x 中,可以在 OneToMany
上使用 @JoinColumn
来定义外键,一些 JPA 提供程序可能已经支持这一点。
单向 OneToMany
的主要问题是外键由目标对象的表拥有,因此如果目标对象不知道该外键,则插入和更新该值很困难。在单向 OneToMany
中,源对象拥有外键字段,并负责更新其值。
单向 OneToMany
中的目标对象是一个独立的对象,因此它不应该以任何方式依赖于外键,即外键不能是其主键的一部分,也不能通常对其设置非空约束。您可以建模一个对象集合,其中目标没有映射外键,但使用它作为其主键,或者使用 Embeddable
集合映射没有主键,请参见嵌入式集合。
如果您的 JPA 提供程序不支持单向 OneToMany
关系,那么您需要添加一个反向引用 ManyToOne
或 JoinTable
。通常,如果您确实想要在数据库中建模单向 OneToMany
,最好使用 JoinTable
。
有一些创新的解决方法来定义单向 OneToMany
。一种是使用 JoinTable
来映射它,但使目标表成为 JoinTable
。这将导致额外的连接,但对于大多数读取来说可以正常工作,写入当然不会正常工作,因此这只是一个只读解决方案,而且是一个很糟糕的解决方案。
EMPLOYEE (表)
EMP_ID | FIRSTNAME | LASTNAME |
1 | Bob | 可以 |
2 | Sarah | Smith |
PHONE (表)
ID | TYPE | AREA_CODE | P_NUMBER | OWNER_ID |
1 | home | 613 | 792-0000 | 1 |
2 | work | 613 | 896-1234 | 1 |
3 | work | 416 | 123-4444 | 2 |
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToMany
@JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
private List<Phone> phones;
...
}
@Entity
public class Phone {
...
@Column(name="OWNER_ID")
private long ownerId;
...
}
如果您的连接列是非空的,则需要指定 @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID", nullable = false)
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToMany
@JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID", nullable = false)
private List<Phone> phones;
...
}