跳转到内容

Java 持久性/OneToMany

来自 Wikibooks,开放世界的开放书籍

OneToMany

[编辑 | 编辑源代码]

Java 中的 OneToMany 关系是指源对象有一个属性存储目标对象的集合,并且如果这些目标对象具有反向关系回到源对象,则它将是 ManyToOne 关系。 Java 和 JPA 中的所有关系都是单向的,也就是说,如果源对象引用目标对象,则不能保证目标对象也与源对象存在关系。 这与关系数据库不同,在关系数据库中,关系通过外键定义,并进行查询,以便始终存在反向查询。

JPA 还定义了 ManyToMany 关系,它类似于 OneToMany 关系,只是反向关系(如果定义)是 ManyToMany 关系。 JPA 中 OneToManyManyToMany 关系的主要区别在于 ManyToMany 始终使用中间关系联接表来存储关系,而 OneToMany 可以使用联接表,也可以使用目标对象表中的外键来引用源对象表的主键。 如果 OneToMany 使用目标对象表中的外键,JPA 要求关系是双向的(必须在目标对象中定义反向 ManyToOne 关系),并且源对象必须使用 mappedBy 属性来定义映射。

在 JPA 中,OneToMany 关系通过 @OneToMany 注解或 <one-to-many> 元素定义。

OneToMany 关系数据库示例

[编辑 | 编辑源代码]

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

OneToMany 关系和反向 ManyToOne 注解示例

[编辑 | 编辑源代码]
@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;
  ...
}

OneToMany 关系和反向 ManyToOne XML 示例

[编辑 | 编辑源代码]
<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

Getter 和 Setter

[编辑 | 编辑源代码]

关系是双向的,因此当应用程序更新关系的一端时,另一端也应该更新,并保持同步。 在 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 可以用于 ManyToManyOneToMany 映射。

另请参阅,单向 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

使用联接表注解的 OneToMany 示例

[编辑 | 编辑源代码]
@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;
  ...
}

使用 JoinTable XML 的 OneToMany 示例

[编辑 | 编辑源代码]
<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>

另请参阅

[编辑 | 编辑源代码]

常见问题

[编辑 | 编辑源代码]
刷新后对象不在集合中。
[编辑 | 编辑源代码]
参见对象损坏

单向 OneToMany,无反向 ManyToOne,无 Join Table (仅限 JPA 2.x)

[编辑 | 编辑源代码]

JPA 1.0 不支持没有 JoinTable 的单向 OneToMany 关系。从 JPA 2.0 开始,支持单向 OneToMany。在 JPA 2.x 中,可以在 OneToMany 上使用 @JoinColumn 来定义外键,一些 JPA 提供程序可能已经支持这一点。

单向 OneToMany 的主要问题是外键由目标对象的表拥有,因此如果目标对象不知道该外键,则插入和更新该值很困难。在单向 OneToMany 中,源对象拥有外键字段,并负责更新其值。

单向 OneToMany 中的目标对象是一个独立的对象,因此它不应该以任何方式依赖于外键,即外键不能是其主键的一部分,也不能通常对其设置非空约束。您可以建模一个对象集合,其中目标没有映射外键,但使用它作为其主键,或者使用 Embeddable 集合映射没有主键,请参见嵌入式集合

如果您的 JPA 提供程序不支持单向 OneToMany 关系,那么您需要添加一个反向引用 ManyToOneJoinTable。通常,如果您确实想要在数据库中建模单向 OneToMany,最好使用 JoinTable

有一些创新的解决方法来定义单向 OneToMany。一种是使用 JoinTable 来映射它,但使目标表成为 JoinTable。这将导致额外的连接,但对于大多数读取来说可以正常工作,写入当然不会正常工作,因此这只是一个只读解决方案,而且是一个很糟糕的解决方案。

JPA 2.x 单向 OneToMany 关系数据库示例

[编辑 | 编辑源代码]

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

JPA 2.x 单向 OneToMany 关系注释示例

[编辑 | 编辑源代码]
@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;
  ...
}
华夏公益教科书