跳转到内容

Java 持久化/关系

来自维基教科书,开放世界中的开放书籍

关系是从一个对象到另一个对象的引用。在 Java 中,关系通过从源对象到目标对象的 对象引用(指针)来定义。从技术上讲,在 Java 中,对另一个对象的关系与对 StringDate 等数据属性的“关系”之间没有区别(基本类型有所不同),因为两者都是指针;但是,在逻辑上和出于持久化的目的,数据属性被认为是对象的一部分,而对其他持久对象的引用被认为是关系。

在关系数据库中,关系通过外键来定义。源行包含目标行的主键以定义关系(有时还有反向关系)。必须执行查询以使用外键和主键信息读取关系的目标对象。

在 Java 中,如果关系是到其他对象的集合,则 Collection 或数组类型用于在 Java 中保存关系的内容。在关系数据库中,集合关系要么通过目标对象具有指向源对象主键的反向外键来定义,要么通过具有中间联接表来存储关系(两个对象的主键)。

Java 和 JPA 中的所有关系都是单向的,这意味着如果源对象引用目标对象,则不能保证目标对象也与源对象有关系。这与关系数据库不同,在关系数据库中,关系通过外键和查询来定义,这样反向查询总是存在的。

JPA 关系类型

[编辑 | 编辑源代码]
  • 一对一 - 从一个对象到另一个对象的唯一引用,是 OneToOne 的反向关系。
  • 多对一 - 从一个对象到另一个对象的引用,是 OneToMany 的反向关系。
  • 一对多 - 对象的 CollectionMap,是 ManyToOne 的反向关系。
  • 多对多 - 对象的 CollectionMap,是 ManyToMany 的反向关系。
  • 嵌入式 - 对共享父级相同表的对象的引用。
  • 元素集合 - JPA 2.0, BasicEmbeddable 对象的 CollectionMap,存储在单独的表中。

这涵盖了大多数对象模型中存在的绝大多数类型的关系。每种关系类型也涵盖多种不同的实现,例如 OneToMany 允许联接表或目标中的外键,而集合映射也允许 Collection 类型和 Map 类型。还有一些其他可能的复杂关系类型,请参阅 高级关系

延迟加载

[编辑 | 编辑源代码]

检索和构建对象关系的成本远远超过选择对象的成本。对于像 managermanagedEmployees 这样的关系来说尤其如此,如果选择任何员工,它将通过关系层次结构触发所有员工的加载。显然,这是一件坏事,但是对象中拥有关系是非常可取的。

解决这个问题的办法是延迟加载(延迟加载)。延迟加载允许延迟加载关系,直到访问它为止。这不仅对于避免数据库访问很重要,而且对于避免在不需要时构建对象的成本也很重要。

在 JPA 中,可以使用 `fetch` 属性在任何关系上设置延迟加载。`fetch` 可以设置为 `LAZY` 或 `EAGER`,如 FetchType 枚举中定义的那样。除了 `OneToOne` 和 `ManyToOne` 之外,所有关系的默认获取类型都是 `LAZY`,但通常最好将所有关系都设置为 `LAZY`。`OneToOne` 和 `ManyToOne` 的 `EAGER` 默认值是出于实现原因(更难实现),而不是因为它是一个好主意。从技术上讲,在 JPA 中 `LAZY` 只是一个提示,JPA 提供者不需要支持它,但在现实中,所有主要的 JPA 提供者都支持它,如果它们不支持它,它们将毫无用处。

延迟一对一关系注解示例

[edit | edit source]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="ADDR_ID")
  private Address address;
  ...
}

延迟一对一关系 XML 示例

[edit | edit source]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-one name="address" fetch="LAZY">
            <join-column name="ADDR_ID"/>
        </one-to-one>
    </attributes>
</entity>

魔法

[edit | edit source]

延迟加载通常涉及 JPA 提供者中的某种“魔法”,以透明地将关系加载到内存中,当它们被访问时。对于集合关系,典型的魔法是 JPA 提供者将其关系设置为自己的 `Collection`、`List`、`Set` 或 `Map` 实现。当在这个集合代理上访问任何(或大多数)方法时,它将加载真实的集合并转发该方法。这就是为什么 JPA 要求所有集合关系使用其中一个集合接口(尽管一些 JPA 提供者也支持集合实现)。

对于 `OneToOne` 和 `ManyToOne` 关系,魔法通常涉及对实体类的某种字节码操作,或创建子类。这允许访问字段或 get/set 方法被拦截,并在允许访问值之前先检索关系。一些 JPA 提供者使用不同的方法,例如将引用包装在代理对象中,尽管这可能导致 `null` 值和原始方法的问题。为了执行字节码魔法,通常需要一个代理或后处理器。确保您正确使用提供者的代理或后处理器,否则延迟可能无法正常工作。您还可能在调试器中注意到额外的变量,但总的来说,调试仍然可以正常工作。

基础

[edit | edit source]

`Basic` 属性也可以设置为 `LAZY`,但这通常与延迟关系不同,应该避免,除非该属性很少被访问。

参见 基本属性:延迟加载

序列化和分离

[edit | edit source]

延迟关系的一个主要问题是确保对象被分离或序列化后,关系仍然可用。对于大多数 JPA 提供者,在序列化之后,任何没有实例化的延迟关系都会被破坏,并且在被访问时要么抛出错误,要么返回 null。

一个简单的解决方案是将所有关系都设置为 eager。序列化与持久化面临着相同的问题,如果您没有延迟关系,很容易序列化整个数据库。因此,延迟关系对于序列化来说和对于数据库访问一样必要;但是,您需要确保您在序列化之前实例化了您在序列化之后需要的所有内容。您可以只将您认为在序列化之后需要的关系标记为 `EAGER`;这将起作用,但可能存在许多情况下,您不需要这些关系。

第二个解决方案是在返回对象以进行序列化之前,访问您需要的任何关系。这具有使用案例特定的优点,因此不同的使用案例可以实例化不同的关系。对于集合关系,发送 `size()` 通常是确保延迟关系被实例化的最佳方法。对于 `OneToOne` 和 `ManyToOne` 关系,通常只要访问关系就足够了(例如 `employee.getAddress()`),尽管对于使用代理的一些 JPA 提供者,您可能需要向对象发送一条消息(例如 `employee.getAddress().hashCode()`)。

第三个解决方案是使用 JPQL `JOIN FETCH` 在查询对象时查询关系。`JOIN FETCH` 通常可以确保关系已被实例化。但是,在使用 `JOIN FETCH` 时应该谨慎,因为它在集合关系上使用时会变得效率低下,尤其是在多个集合关系上使用时,因为它需要在数据库上进行 n^2 连接。

一些 JPA 提供者还可能提供某些查询提示或其他序列化选项。

在没有序列化的情况下,也会出现相同的问题,如果在事务结束之后访问一个分离的对象。一些 JPA 提供者允许在事务结束之后或在 `EntityManager` 关闭之后访问延迟关系,但有些提供者不允许。如果您的 JPA 提供者不允许这样做,那么您可能需要确保在结束事务之前,您已经实例化了您将需要的所有延迟关系。

急切连接获取

[edit | edit source]

一个常见的误解是 `EAGER` 意味着应该连接获取关系,即在与源对象相同的 SQL `SELECT` 语句中检索关系。一些 JPA 提供者确实以这种方式实现了 eager。但是,仅仅因为某件事需要被加载,并不意味着它应该被连接获取。考虑 `Employee` - `Phone`,`Phone` 的员工引用被设置为 `EAGER`,因为员工几乎总是先于电话被加载。但是,在加载电话时,您不想连接员工,员工已经被读取并且已经存在于缓存或持久化上下文中。同样,仅仅因为您想要加载两个集合关系,并不意味着您想要连接获取它们,这将导致一个非常低效的连接,它将返回 n^2 数据。

连接获取是 JPA 目前只通过 JPQL 提供的功能,这通常是正确的地方,因为每个使用案例都有不同的关系要求。一些 JPA 提供者还在映射级别提供了一个连接获取选项,以便始终连接获取关系,但这通常与 `EAGER` 不一样。连接获取通常不是加载关系最有效的方式,通常批处理读取关系在您的 JPA 提供者支持的情况下效率更高。

参见 连接获取

参见 批处理读取

级联

[edit | edit source]

关系映射有一个 `cascade` 选项,允许关系级联到常见的操作。`cascade` 通常用于模拟依赖关系,例如 `Order` -> `OrderLine`。级联 `orderLines` 关系允许 `Order` -> `OrderLine` 与它们的父级一起被持久化、删除、合并。

以下操作可以级联,如 CascadeType 枚举中定义的那样

  • PERSIST - 级联 `EntityManager.persist()` 操作。如果对父级调用 `persist()`,并且子级也是新的,它也将被持久化。如果它已经存在,将不会发生任何事情,尽管对现有对象调用 `persist()` 仍然会将持久化操作级联到其依赖项。如果您持久化一个对象,并且它与一个新对象相关,并且关系没有级联持久化,那么将发生异常。这可能需要您先对相关对象调用持久化,然后再将其与父级相关联。总的来说,它可能看起来很奇怪,或者希望始终级联持久化操作,如果一个新对象与另一个对象相关,那么它可能应该被持久化。在每个关系上始终级联持久化可能没有重大问题,尽管它可能会影响性能。不需要在相关对象上调用持久化,在提交时,任何其关系是级联持久化的相关对象都将自动持久化。预先调用持久化的优点是,任何生成的 id 都将(除非使用标识)被分配,并且将引发 `prePersist` 事件。
  • REMOVE - 级联 EntityManager.remove() 操作。如果在父级上调用 remove(),则子级也将被删除。这应该只用于依赖关系。请注意,只有 remove() 操作是级联的,如果您从 OneToMany 集合中删除一个依赖对象,它不会被删除,JPA 要求您显式调用 remove()。一些 JPA 提供者可能支持一个选项,即从依赖集合中删除的对象将被删除,JPA 2.0 也为此定义了一个选项。
  • MERGE - 级联 EntityManager.merge() 操作。如果在父级上调用 merge(),则子级也将被合并。这通常应该用于依赖关系。请注意,这只会影响合并的级联,关系引用本身始终会被合并。如果使用 transient 变量来限制序列化,这可能会成为一个主要问题,在这种情况下,您可能需要手动合并或重置 transient 关系。一些 JPA 提供者提供额外的 merge 操作。
  • REFRESH - 级联 EntityManager.refresh() 操作。如果在父级上调用 refresh(),则子级也将被刷新。这通常应该用于依赖关系。在为所有关系启用此功能时要小心,因为它会导致对其他对象的更改被重置。
  • ALL - 级联所有上述操作。

级联一对一关系注释的示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToOne(cascade={CascadeType.ALL})
  @JoinColumn(name="ADDR_ID")
  private Address address;
  ...
}

级联一对一关系 XML 的示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-one name="address">
            <join-column name="ADDR_ID"/>
            <cascade>
                <cascade-all/>
            </cascade>
        </one-to-one>
    </attributes>
</entity>

孤儿删除 (JPA 2.0)

[编辑 | 编辑源代码]

remove 操作的级联仅在对对象调用 remove 时发生。这通常不是依赖关系中想要的。如果相关对象不能在没有源对象的情况下存在,那么通常希望在源对象被删除时删除它们,而且也希望在它们不再被源对象引用时删除它们。JPA 1.0 没有为此提供选项,因此当从源关系中删除依赖对象时,必须从 EntityManager 中显式删除它。JPA 2.0 在 OneToManyOneToOne 注释和 XML 中提供了一个 orphanRemoval 选项。孤儿删除将确保从关系中不再引用的任何对象都将从数据库中删除。

孤儿删除一对多关系注释的示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(orphanRemoval=true, cascade={CascadeType.ALL})
  private List<PhoneNumbers> phones;
  ...
}

孤儿删除一对多关系 XML 的示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones" orphan-removal="true">
            <cascade>
                <cascade-all/>
            </cascade>
        </one-to-many>
    </attributes>
</entity>

目标实体

[编辑 | 编辑源代码]

关系映射有一个 targetEntity 属性,它允许指定关系的引用类(目标)。这通常不需要设置,因为它从字段类型、get 方法返回值类型或集合的泛型类型中默认得出。

如果您的字段使用公共接口类型,也可以使用它,例如字段是接口 Address,但映射需要映射到实现类 AddressImpl。另一种用法是,如果您的字段是超类类型,但您想将关系映射到子类。

目标实体关系注释的示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(targetEntity=Phone.class)
  @JoinColumn(name="OWNER_ID")
  private List phones;
  ...
}

目标实体关系 XML 的示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones" target-entity="org.acme.Phone">
            <join-column name="OWNER_ID"/>
        </one-to-many>
    </attributes>
</entity>

集合映射包括 OneToManyManyToMany,以及 JPA 2.0 中的 ElementCollection。JPA 要求集合字段或 get/set 方法的类型是 Java 集合接口之一,CollectionListSetMap

集合实现

[编辑 | 编辑源代码]

您的字段不应该为集合实现类型,例如 ArrayList。一些 JPA 提供者可能支持使用集合实现,许多提供者支持 EAGER 集合关系来使用实现类。您可以将任何实现设置为集合的实例值,但在从数据库中读取对象时,如果它是 LAZY,JPA 提供者通常会放入一个特殊的 LAZY 集合。

重复项

[编辑 | 编辑源代码]

Java 中的 List 支持重复条目,而 Set 不支持。在数据库中,通常不支持重复项。从技术上讲,如果使用 JoinTable,这可能是可能的,但 JPA 不要求支持重复项,大多数提供者也不支持。

如果您需要重复支持,您可能需要创建一个代表并映射到连接表的对象。该对象仍然需要一个唯一的 Id,例如 GeneratedValue。参见 映射带有附加列的连接表

JPA 允许在检索时按数据库对集合值进行排序。这是通过 @OrderBy 注释或 <order-by> XML 元素完成的。

OrderBy 的值是 JPQL ORDER BY 字符串。这可以是一个属性名称,后面跟着 ASCDESC,表示升序或降序排序。您还可以使用路径或嵌套属性,或使用 "," 表示多个属性。如果未给出 OrderBy 值,则假设为目标对象的 Id

OrderBy 值必须是目标对象的映射属性。如果您想要一个有序的 List,您需要在目标对象中添加一个 index 属性,并在其表中添加一个 index 列。您还必须确保设置索引值。JPA 2.0 将扩展支持使用 OrderColumn 的有序 List

请注意,使用OrderBy不会保证集合在内存中排序。您有责任以正确的顺序添加到集合中。Java 定义了SortedSet 接口和TreeSet 集合实现,它们确实会维护一个顺序。JPA 并不特别支持SortedSet,但一些 JPA 提供者可能会允许您将SortedSetTreeSet 用于您的集合类型,并维护正确的排序。默认情况下,这些要求您的目标对象实现Comparable 接口或设置Comparator。您还可以使用Collections.sort() 方法在需要时对List 进行排序。在内存中进行排序的一种选择是使用属性访问,并在您的 set 和 add 方法中调用Collections.sort()

集合排序注解示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany
  @OrderBy("areaCode")
  private List<Phone> phones;
  ...
}

集合排序 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones">
            <order-by>areaCode</order-by>
        </one-to-many>
    </attributes>
</entity>

排序列 (JPA 2.0)

[编辑 | 编辑源代码]

JPA 2.0 添加了对OrderColumn 的支持。OrderColumn 可用于定义任何集合映射上的排序List。它通过@OrderColumn 注解或<order-column> XML 元素定义。

OrderColumn 由映射维护,不应该作为目标对象的属性。OrderColumn 的表取决于映射。对于OneToMany 映射,它将在目标对象的表中。对于ManyToMany 映射或使用JoinTableOneToMany,它将在联接表中。对于ElementCollection 映射,它将在目标表中。

集合排序列数据库示例

[编辑 | 编辑源代码]

EMPLOYEE (表)

ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

EMPLOYEE_PHONE (表)

EMPLOYEE_ID PHONE_ID INDEX
1 1 0
1 3 1
2 2 0
2 4 1

PHONE(表)

ID AREACODE NUMBER
1 613 792-7777
2 416 798-6666
3 613 792-9999
4 416 798-5555

集合排序列注解示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany
  @OrderColumn(name="INDEX")
  private List<Phone> phones;
  ...
}

集合排序列 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones">
            <order-column name="INDEX"/>
        </one-to-many>
    </attributes>
</entity>

常见问题

[编辑 | 编辑源代码]

对象损坏,更新一方后另一方未更新

[编辑 | 编辑源代码]

双向关系中的一个常见问题是应用程序更新关系的一方,但另一方没有更新,并且变得不同步。在 JPA 中,就像在一般 Java 中一样,应用程序或对象模型负责维护关系。如果您的应用程序向关系的一方添加内容,则必须向另一方添加内容。

这通常通过对象模型中的addset 方法来解决,这些方法处理关系的双方,因此应用程序代码不必担心它。解决此问题的方法有两种:您可以将关系维护代码添加到关系的一方,并且只使用该方的 setter(例如,使另一方受保护),或者将其添加到双方并确保避免无限循环。

例如

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;
    ...
    public void setOwner(Employee employee) {
        this.owner = employee;
        if (!employee.getPhones().contains(this)) {
            employee.getPhones().add(this);
        }
    }
    ...
}

双向OneToOneManyToMany 关系的代码类似。

有些人期望 JPA 提供者具有自动维护关系的魔力。这实际上是 EJB CMP 2 规范的一部分。但是问题是,如果对象被分离或序列化到另一个 VM,或者在对象被管理之前建立了新的关系,或者在 JPA 范围之外使用对象模型,那么魔力就会消失,应用程序就必须自己解决问题,因此总的来说,最好将代码添加到对象模型中。但是一些 JPA 提供者确实支持自动维护关系。

在某些情况下,在添加子对象时不希望实例化大型集合。一种解决方案是不映射双向关系,而是根据需要查询它。一些 JPA 提供者还优化了它们的延迟集合对象以处理这种情况,因此您仍然可以向集合添加内容而不实例化它。

性能低下,查询过多

[编辑 | 编辑源代码]

导致性能低下的最常见问题是使用EAGER 关系。这要求在读取源对象时读取相关对象。例如,使用EAGER managedEmployees 读取公司总裁会导致读取公司的所有Employee。解决方案是始终将所有关系设为LAZY。默认情况下,OneToManyManyToManyLAZY,但OneToOneManyToOne 不是,因此请确保将其配置为LAZY。请参阅延迟获取。有时您配置了LAZY,但它不起作用,请参阅延迟不起作用

另一个常见问题是n+1 问题。例如,假设您读取所有Employee 对象,然后访问它们的Address。由于每个Address 都被单独访问,因此这会导致 n+1 个查询,这会成为一个主要的性能问题。这可以通过联接获取批量读取 来解决。

延迟不起作用

[编辑 | 编辑源代码]

延迟OneToOneManyToOne 关系通常需要某种形式的编织或字节码生成。通常在 JSE 中运行时,需要agent 选项来允许字节码编织,因此请确保您已正确配置代理。一些 JPA 提供者执行动态子类生成,因此不需要代理。

代理示例

   java -javaagent:eclipselink.jar ...

一些 JPA 提供者还提供静态编织,或者除了动态编织之外。对于静态编织,必须在您的 JPA 类上运行一些预处理器。

在 JEE 中运行时,延迟通常可以正常工作,因为 EJB 规范需要类加载器钩子。但是一些 JEE 提供者可能不支持此功能,因此可能需要静态编织。

另外,请确保您不要在不应该访问关系时访问关系。例如,如果您使用属性访问,并在您的 set 方法中访问相关的延迟值,这会导致它被加载。要么删除 set 方法的副作用,要么使用字段访问。

序列化后关系断裂

[编辑 | 编辑源代码]

如果您的关系被标记为lazy,那么如果它在对象被序列化之前没有被实例化,那么它可能不会被序列化。这可能会导致错误,或者在反序列化后访问时返回null

请参阅序列化和分离

从 OneToMany 集合中删除的依赖对象不会被删除

[编辑 | 编辑源代码]

从集合中删除对象时,如果也希望从数据库中删除该对象,则必须在该对象上调用 remove()。在 JPA 1.0 中,即使关系是 cascade REMOVE,也仍然必须调用 remove(),只有父对象的删除会被级联,而不会从集合中删除。

JPA 2.0 将提供一个选项,允许从集合中删除触发删除。某些 JPA 提供商在 JPA 1.0 中支持此选项。

参见,级联

@Entity
@Table(name ="Comment")
public class Comment {
	
	@Id
	@Column(name="Id")
	@GeneratedValue(strategy=GenerationType.AUTO)
	private int Id;
	
	
	private int vehicleId;
	private int userId;
	private String post;
	private Date timeStamp;
	private double amountOffered = 0.0;
	private boolean acceptOffer;

        ...
}

我的关系目标是一个接口

[编辑 | 编辑源代码]

如果关系字段的类型是类的公共接口,并且只有一个实现者,那么解决方法很简单,您只需要在映射中设置一个 targetEntity。参见,目标实体

如果接口有多个实现者,那么情况会更复杂。JPA 不直接支持映射接口。一个解决方案是将接口转换为抽象类,并使用继承来映射它。您也可以保留接口,创建抽象类,并确保每个实现者都扩展它,并将 targetEntity 设置为抽象类。

另一种解决方案是使用 get/set 方法为每个可能的实现者定义虚拟属性,并分别映射它们,并将接口 get/set 标记为 transient。您也可以不映射属性,而是在需要时查询它。

参见,可变和异构关系

一些 JPA 提供商支持接口和可变关系。

TopLinkEclipseLink : 通过他们的 @VariableOneToOne 注释和 XML 支持可变关系。映射到接口和查询接口也通过他们的 ClassDescriptorInterfacePolicy API 支持。

高级关系

[编辑 | 编辑源代码]

JPA 2.0 关系增强

[编辑 | 编辑源代码]
  • ElementCollection - CollectionMapEmbeddableBasic 值。
  • 映射列 - OneToManyManyToManyElementCollection,其中包含一个 BasicEmbeddableEntity 密钥,该密钥不是目标对象的一部分。
  • 顺序列 - OneToManyManyToManyElementCollection 现在可以包含一个 OrderColumn,该列定义在使用 List 时集合的顺序。
  • 单向 OneToMany - OneToMany 不再需要定义 ManyToOne 反向关系。

其他类型的关系

[编辑 | 编辑源代码]
  • 可变 OneToOne,ManyToOne - 对接口或具有多个不同实现者的通用未映射继承类的引用。
  • 可变 OneToMany,ManyToMany - CollectionMap 的异构对象,这些对象共享一个接口或具有多个不同实现者的通用未映射继承类。
  • 嵌套集合关系,例如数组的数组,ListList,或 MapMap,或其他此类组合。
  • 对象关系数据类型 - 使用 STRUCTVARRAYREFNESTEDTABLE 类型存储在数据库中的关系。
  • XML 关系 - 作为 XML 文档存储的关系。

Java 定义了 Map 接口来表示其值为按密钥索引的集合。有多种 Map 实现,最常见的是 HashMap,但也包括 HashtableTreeMap

JPA 允许 Map 用于任何集合映射,包括 OneToManyManyToManyElementCollection。JPA 要求使用 Map 接口作为属性类型,尽管一些 JPA 提供商也可能支持使用 Map 实现。

在 JPA 1.0 中,映射密钥必须是集合值的映射属性。可以使用 @MapKey 注释或 <map-key> XML 元素定义映射关系。如果未指定 MapKey,则默认为目标对象的 Id

映射密钥关系注释示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(mappedBy="owner")
  @MapKey(name="type")
  private Map<String, PhoneNumber> phoneNumbers;
  ...
}

@Entity
public class PhoneNumber {
  @Id
  private long id;
  @Basic
  private String type;  // Either "home", "work", or "fax".
  ...
  @ManyToOne
  private Employee owner;
  ...
}

映射密钥关系 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phoneNumbers" mapped-by="owner">
            <map-key name="type"/>
        </one-to-many>
    </attributes>
</entity>
<entity name="PhoneNumber" class="org.acme.PhoneNumber" access="FIELD">
    <attributes>
        <id name="id"/>
        <basic name="type"/>
        <many-to-one name="owner"/>
    </attributes>
</entity>

映射列 (JPA 2.0)

[编辑 | 编辑源代码]

JPA 2.0 允许使用一个 Map,其中密钥不是目标对象的一部分,可以被持久化。Map 密钥可以是以下任何一个

  • 一个 Basic 值,存储在目标表或连接表中。
  • 一个 Embedded 对象,存储在目标表或连接表中。
  • 另一个 Entity 的外键,存储在目标表或连接表中。

映射列可用于任何集合映射,包括 OneToManyManyToManyElementCollection

这使得可以灵活地使用复杂的数量,以便能够映射不同的模型。使用的映射类型始终由 Map 的值决定,而不是密钥。因此,如果密钥是 Basic,但值是 Entity,则仍然使用 OneToMany 映射。但如果值是 Basic,但密钥是 Entity,则使用 ElementCollection 映射。

这使得可以映射一些非常复杂的数据库模式。例如,使用具有 MapKeyJoinColumnManyToMany 映射三方连接表,用于第三个外键。对于 ManyToMany,密钥始终存储在 JoinTable 中。对于 OneToMany,如果定义了 JoinTable,则将其存储在 JoinTable 中,否则将其存储在目标 Entity 的表中,即使目标 Entity 不映射此列。对于 ElementCollection,密钥存储在元素表中。

可以使用 @MapKeyColumn 注释或 <map-key-column> XML 元素定义密钥为 Basic 值的映射关系,@MapKeyEnumerated@MapKeyTemporal 也可用于 EnumCalendar 类型。可以使用 @MapKeyJoinColumn 注释或 <map-key-join-column> XML 元素定义密钥为 Entity 值的映射关系,@MapKeyJoinColumns 也可用于复合外键。注释 @MapKeyClass<map-key-class> XML 元素可在密钥为 Embeddable 时使用,或用于指定目标类或类型(如果未使用泛型)。

映射密钥列关系数据库示例

[编辑 | 编辑源代码]

EMPLOYEE (表)

ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

PHONE(表)

ID OWNER_ID PHONE_TYPE AREACODE NUMBER
1 1 home 613 792-7777
2 1 cell 613 798-6666
3 2 home 416 792-9999
4 2 fax 416 798-5555

地图键列关系注释示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(mappedBy="owner")
  @MapKeyColumn(name="PHONE_TYPE")
  private Map<String, Phone> phones;
  ...
}

@Entity
public class Phone {
  @Id
  private long id;
  ...
  @ManyToOne
  private Employee owner;
  ...
}

地图键列关系 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones" mapped-by="owner">
            <map-key-column name="PHONE_TYPE"/>
        </one-to-many>
    </attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
    <attributes>
        <id name="id"/>
        <many-to-one name="owner"/>
    </attributes>
</entity>

地图键连接列关系数据库示例

[编辑 | 编辑源代码]

EMPLOYEE (表)

ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

PHONE(表)

ID OWNER_ID PHONE_TYPE_ID AREACODE NUMBER
1 1 1 613 792-7777
2 1 2 613 798-6666
3 2 1 416 792-9999
4 2 3 416 798-5555

PHONETYPE(表)

ID TYPE
1 home
2 cell
3 fax
4 work

地图键连接列关系注释示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(mappedBy="owner")
  @MapKeyJoinColumn(name="PHONE_TYPE_ID")
  private Map<PhoneType, Phone> phones;
  ...
}

@Entity
public class Phone {
  @Id
  private long id;
  ...
  @ManyToOne
  private Employee owner;
  ...
}

@Entity
public class PhoneType {
  @Id
  private long id;
  ...
  @Basic
  private String type;
  ...
}

地图键连接列关系 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones" mapped-by="owner">
            <map-key-join-column name="PHONE_TYPE_ID"/>
        </one-to-many>
    </attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
    <attributes>
        <id name="id"/>
        <many-to-one name="owner"/>
    </attributes>
</entity>
<entity name="PhoneType" class="org.acme.PhoneType" access="FIELD">
    <attributes>
        <id name="id"/>
        <basic name="type"/>
    </attributes>
</entity>


地图键类嵌入式关系数据库示例

[编辑 | 编辑源代码]

EMPLOYEE (表)

ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

EMPLOYEE_PHONE (表)

EMPLOYEE_ID PHONE_ID TYPE
1 1 home
1 2 cell
2 3 home
2 4 fax

PHONE (表)

ID AREACODE NUMBER
1 613 792-7777
2 613 798-6666
3 416 792-9999
4 416 798-5555

地图键类嵌入式关系注释示例

[编辑 | 编辑源代码]
@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany
  @MapKeyClass(PhoneType.class)
  private Map<PhoneType, Phone> phones;
  ...
}

@Entity
public class Phone {
  @Id
  private long id;
  ...
}

@Embeddable
public class PhoneType {
  @Basic
  private String type;
  ...
}

地图键类嵌入式关系 XML 示例

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones">
            <map-key-class>PhoneType</map-key-class>
        </one-to-many>
    </attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
    <attributes>
        <id name="id"/>
        <many-to-one name="owner"/>
    </attributes>
</entity>
<embeddable name="PhoneType" class="org.acme.PhoneType" access="FIELD">
    <attributes>
        <basic name="type"/>
    </attributes>
</embeddable>

连接提取

[编辑 | 编辑源代码]

连接提取是一种用于在一个数据库查询中读取多个对象的查询优化技术。它涉及在 SQL 中连接两个对象的表并选择两个对象的数据。连接提取通常用于 OneToOne 关系,但也可用于任何关系,包括 OneToManyManyToMany

连接提取是解决经典 ORM n+1 性能问题的解决方案之一。问题是,如果你选择了 nEmployee 对象,并访问了每个对象的地址,在基本的 ORM(包括 JPA)中,你会得到 1 个用于 Employee 对象的数据库选择,然后 n 个数据库选择,每个 Address 对象一个。连接提取通过只要求一个选择并选择 Employee 及其 Address 来解决这个问题。

JPA 通过使用 JOIN FETCH 语法的 JPQL 支持连接提取。

JPQL 连接提取示例

[编辑 | 编辑源代码]
SELECT emp FROM Employee emp JOIN FETCH emp.address

这会导致 EmployeeAddress 数据在一个查询中被选择。

外部连接

[编辑 | 编辑源代码]

使用 JPQL JOIN FETCH 语法,执行一个正常的 INNER 连接。这具有从结果集中过滤任何没有地址的 Employee 的副作用。SQL 中的 OUTER 连接是一个不过滤连接上的缺失行,而是连接所有 null 值行的连接。如果你的关系允许 null 或集合关系的空集合,那么你可以使用 OUTER 连接提取,这在 JPQL 中使用 LEFT 语法完成。

请注意,OUTER 连接在某些数据库上效率可能较低,因此如果不需要,请避免使用 OUTER

JPQL 外部连接提取示例

[编辑 | 编辑源代码]
SELECT emp FROM Employee emp LEFT JOIN FETCH emp.address

映射级别连接提取和 EAGER

[编辑 | 编辑源代码]

JPA 没有办法指定始终对关系使用连接提取。通常,最好在查询级别指定连接提取,因为某些用例可能需要相关对象,而其他用例可能不需要。JPA 在映射上支持 EAGER 选项,但这意味着将加载关系,而不是将它连接。将所有关系标记为 EAGER 可能是可取的,因为所有内容都需要加载,但将所有内容在一个巨大的选择中连接提取可能会导致数据库上的低效、过于复杂或无效的连接。

一些 JPA 提供程序将 EAGER 解释为连接提取,因此这在某些 JPA 提供程序上可能有效。一些 JPA 提供程序支持一个单独的选项来始终连接提取关系。

TopLinkEclipseLink : 在映射上支持 @JoinFetch 注释和 XML 来定义始终连接提取关系。

嵌套连接

[编辑 | 编辑源代码]

JPA 1.0 不允许在 JPQL 中嵌套连接提取,尽管这可能受某些 JPA 提供程序支持。你可以连接提取多个关系,但不能连接嵌套关系。

多个 JPQL 连接提取示例

[编辑 | 编辑源代码]
SELECT emp FROM Employee emp LEFT JOIN FETCH emp.address LEFT JOIN FETCH emp.phoneNumbers

重复数据和大型连接

[编辑 | 编辑源代码]

使用连接获取的一个问题是可能会返回重复数据。例如,考虑连接获取一个EmployeephoneNumbers关系。如果每个Employee在其phoneNumbers集合中拥有 3 个Phone对象,连接需要检索回n*3 行。由于每行员工数据对应 3 行电话数据,因此员工数据行会被重复 3 次。所以,你读取的数据比你在n+1个查询中选择对象时要多。通常,执行较少查询这一事实弥补了可能读取重复数据这一事实,但如果你考虑连接多个集合关系,你可能会开始检索j*i个重复数据,这可能会成为一个问题。即使使用ManyToOne关系,你也可能正在选择重复数据。考虑连接获取一个Employee的经理:如果所有或大多数员工都有相同的经理,你最终会多次选择此经理的数据。在这种情况下,你不使用连接获取,而是允许对经理执行单个查询会更好。

如果你开始连接获取每个关系,你可能会得到一些非常大的连接。这对数据库来说有时可能是一个问题,尤其是对于大型外连接。

连接获取的一种替代解决方案,它不会遇到重复数据的问题,是使用批量获取.

批量获取

[编辑 | 编辑源代码]

批量获取是一种查询优化技术,用于在有限的数据库查询集中读取多个相关对象。它涉及以正常方式执行根对象的查询。但是对于相关对象,原始查询将与相关对象的查询连接起来,允许所有相关对象在一个数据库查询中读取。批量获取可用于任何类型的关系。

批量获取是解决经典 ORM n+1 性能问题的解决方案之一。问题是,如果你选择nEmployee对象,并访问它们的每个地址,在基本的 ORM(包括 JPA)中,你将获得1个用于Employee对象的数据库选择,然后n个数据库选择,每个Address对象一个。批量获取通过仅要求对Employee对象进行一次选择,对Address对象进行一次选择来解决此问题。

对于读取集合关系和多个关系,批量获取更有效,因为它不需要像连接获取那样选择重复数据。

JPA 不支持批量读取,但一些 JPA 提供商支持。

TopLinkEclipseLink  : 支持@BatchFetch注释和 xml 元素,以及"eclipselink.batch"查询提示以启用批量读取。支持批量获取的三种形式:JOINEXISTSIN

另请参阅,

过滤,复杂连接

[编辑 | 编辑源代码]

通常,关系基于数据库中的外键,但有时它始终基于其他条件。例如,Employee拥有许多PhoneNumber,但也拥有一个单独的家庭电话,或者是他电话中的一个带有"home"类型的电话,或者是一组“活跃”项目,或者其他类似的条件。

JPA 不支持映射这些类型的关系,因为它只支持由外键定义的映射,而不是基于其他列、常量值或函数。一些 JPA 提供商可能支持此功能。解决方法包括映射关系的外键部分,然后在对象的 get/set 方法中过滤结果。你也可以查询结果,而不是定义关系。

TopLinkEclipseLink  : 通过多种机制支持过滤和复杂关系。你可以使用DescriptorCustomizer在任何映射上定义selectionCriteria,使用Expression criteria API。这允许应用任何条件,包括常量、函数或复杂连接。你也可以使用DescriptorCustomizer定义 SQL 或为映射的selectionQuery定义StoredProcedureCall

可变关系和异构关系

[编辑 | 编辑源代码]

有时需要定义一种关系,其中关系的类型可以是几个不相关、异构值的其中之一。这可以是 OneToOne、ManyToOne、OneToMany 或 ManyToMany 关系。相关值可能共享一个公共接口,或者除了子类化Object之外,可能没有其他共同点。也可以设想一种关系,它也可以是任何Basic值,甚至Embeddable

通常,JPA 不支持可变、接口或异构关系。JPA 支持与继承类的关系,因此最简单的解决方法通常是为相关值定义一个公共超类。

另一种解决方案是使用 get/set 方法为每个可能的实现者定义虚拟属性,并分别映射它们,并将异构 get/set 标记为transient。你也可以不映射属性,而是根据需要查询它。

对于异构BasicEmbeddable关系,一种解决方案是将值序列化为二进制字段。你也可以将值转换为可以从中恢复值的String表示,或者将值存储到两列中,一列存储String值,另一列存储类名或类型。

一些 JPA 提供商支持接口、可变关系和/或异构关系。

TopLinkEclipseLink : 通过他们的 @VariableOneToOne 注释和 XML 支持可变关系。映射到接口和查询接口也通过他们的 ClassDescriptorInterfacePolicy API 支持。

嵌套集合、映射和矩阵

[编辑 | 编辑源代码]

在对象模型中,拥有复杂的集合关系(例如ListList(即矩阵),或MapMap,或MapList,等等)是比较常见的。不幸的是,这些类型的集合映射到关系数据库的效果很差。

JPA 不支持嵌套集合关系,通常最好更改你的对象模型以避免它们,以便更轻松地进行持久性和查询。一个解决方案是创建一个包装嵌套集合的对象。

例如,如果一个Employee有一个MapProject,其键是String project-type,值为ListProject。为了映射它,可以创建一个新的ProjectType类来存储 project-type,以及一个OneToManyProject

嵌套集合模型示例(原始)

[编辑 | 编辑源代码]
public class Employee {
  private long id;
  private Map<String, List<Project>> projects;
}

嵌套集合模型示例(修改后)

[编辑 | 编辑源代码]
public class Employee {
  @Id
  @GeneratedValue
  private long id;
  ...
  @OneToMany(mappedBy="employee")
  @MapKey(name="type")
  private Map<String, ProjectType> projects;
}
public class ProjectType {
  @Id
  @GeneratedValue
  private long id;
  @ManyToOne
  private Employee employee;
  @Column(name="PROJ_TYPE")
  private String type;
  @ManyToMany
  private List<Project> projects;
}

嵌套集合数据库示例

[编辑 | 编辑源代码]

EMPLOYEE (表)

ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

PROJECTTYPE(表)

ID EMPLOYEE_ID PROJ_TYPE
1 1 small
2 1 medium
3 2 large

PROJECTTYPE_PROJECT(表)

PROJECTTYPE_ID PROJECT_ID
1 1
1 2
2 3
3 4
华夏公益教科书