跳转到内容

Java 持久化/持久化

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

持久化

[编辑 | 编辑源代码]

JPA 使用 EntityManager API 进行运行时使用。EntityManager 代表应用程序会话或与数据库的对话。每个请求或每个客户端都将使用自己的 EntityManager 来访问数据库。EntityManager 还代表一个事务上下文,在典型的无状态模型中,每个事务都会创建一个新的 EntityManager。在有状态模型中,EntityManager 可能与客户端会话的生命周期相匹配。

EntityManager 提供了所有必需的持久化操作的 API。这些包括以下 CRUD 操作

EntityManager 是一个面向对象的 API,因此不会直接映射到数据库 SQL 或 DML 操作。例如,要更新一个对象,您只需要读取该对象并通过其 set 方法更改其状态,然后在事务上调用 commitEntityManager 会找出您更改了哪些对象并对数据库执行正确的更新,JPA 中没有显式的更新操作。

分离的与托管的

[编辑 | 编辑源代码]

JPA 为给定持久化上下文的某个对象定义了两种主要状态,即 托管的分离的

托管对象是在当前持久化上下文(EntityManager/JTA 事务)中读取的对象。托管对象在持久化上下文中注册,持久化上下文将跟踪对该对象的更改并维护其对象标识。如果在同一个持久化上下文中再次读取同一个对象,或者通过另一个托管对象的关联关系进行遍历,则将返回同一个相同(==)的对象。在新的对象上调用 persist 也将使其成为托管对象。在分离对象上调用 merge 将返回该对象的托管副本。一个对象永远不应该由多个持久化上下文进行托管。一个对象将由其持久化上下文进行托管,直到持久化上下文通过 clear 被清除,或者该对象通过 detach 被强制分离。删除的对象在 flushcommit 之后将不再被托管。在 rollback 上,所有托管对象将变为分离对象。在 JTA 托管的 EntityManager 中,所有托管对象在任何 JTA commit 或 rollback 上都将变为分离对象。

分离对象是在当前持久化上下文不被托管的对象。这可能是通过不同的持久化上下文读取的对象,或者是被克隆或序列化后的对象。新对象在对它调用 persist 之前也被认为是分离的。被删除并刷新或提交的对象将变为分离对象。一个对象可以在一个持久化上下文中被认为是托管的,在另一个持久化上下文中被认为是分离的。

托管对象应该只引用其他托管对象,而分离对象应该只引用其他分离对象。避免关联或混合分离的和托管的对象,这通常会导致问题,因为您的应用程序可能会访问同一对象的两个副本,从而导致更改丢失或数据陈旧。错误地关联托管的和分离的对象可能是用户在 JPA 中遇到的最常见问题之一。

持久化

[编辑 | 编辑源代码]

EntityManager.persist() 操作用于将新对象插入数据库。persist 不会直接将对象插入数据库:它只是在持久化上下文(事务)中将其注册为新的。当事务被提交,或者持久化上下文被刷新时,该对象将被插入数据库。

如果该对象使用生成的 Id,则 Id 通常会在调用 persist 时被分配给该对象,因此 persist 也可以用于分配对象的 Id。唯一的例外是如果使用 IDENTITY 顺序,在这种情况下,Id 仅在 commitflush 时分配,因为数据库仅在 INSERT 时分配 Id。如果该对象不使用生成的 Id,则通常应该在调用 persist 之前分配其 Id

persist 操作只能在事务内调用,在事务之外调用会抛出异常。persist 操作是就地进行的,也就是说,要持久化的对象将成为持久化上下文的一部分。事务提交时对象的 state 将被持久化,而不是 persist 调用时的 state。

persist 通常只应该在新对象上调用。如果对象是持久化上下文的一部分,则允许在现有对象上调用它,这仅用于将级联持久化操作应用于任何可能的相关新对象。如果在不是持久化上下文一部分的现有对象上调用 persist,则可能会抛出异常,或者可能会尝试插入并发生数据库约束错误,或者如果未定义约束,则可能能够插入重复数据。

persist 只能在 Entity 对象上调用,不能在 Embeddable 对象、集合或非持久化对象上调用。Embeddable 对象将自动作为其所属 Entity 的一部分进行持久化。

并非总是需要调用 persist。如果您将一个新对象关联到持久化上下文的一部分的现有对象,并且关联关系是级联持久化的,那么当事务被提交或持久化上下文被刷新时,它将被自动插入。

示例持久化

[编辑 | 编辑源代码]
EntityManager em = getEntityManager();
em.getTransaction().begin();

Employee employee = new Employee();
employee.setFirstName("Bob");
Address address = new Address();
address.setCity("Ottawa");
employee.setAddress(address);

em.persist(employee);

em.getTransaction().commit();

级联持久化

[编辑 | 编辑源代码]

在对象上调用 persist 还将在所有标记为级联持久化的关联关系上级联 persist 操作。如果关联关系不是级联持久化的,并且关联对象是新的,那么如果您没有首先在关联对象上调用 persist,则可能会抛出异常。从直觉上说,您可能会考虑将所有关联关系标记为级联持久化,以避免必须在每个对象上调用 persist,但这也会导致问题。

将所有关系标记为级联持久化会导致性能问题。在每次持久化调用时,都需要遍历所有相关对象并检查它们是否引用了任何新对象。如果将所有关系标记为级联持久化,并持久化一个大型的新对象图,这实际上会导致 `O(n²) ` 性能问题。如果仅对根对象调用 `persist`,则可以。但是,如果对图中的每个对象调用 `persist`,则会遍历图中的每个对象,这会导致严重的性能问题。JPA 规范可能应该将 `persist` 定义为仅适用于新对象,而不是已存在的持久化上下文的一部分,但它要求 `persist` 应用于所有对象,无论是新的、现有的还是已持久化的,因此会出现此问题。

第二个问题是,如果调用 `remove` 删除一个对象,然后调用该对象的 `persist`,它将恢复该对象,并且它将再次变为持久化状态。如果这是故意的,这可能是需要的,但 JPA 规范也要求级联持久化具有此行为。因此,如果调用 `remove` 删除了一个对象,但忘记从级联持久化关系中删除对它的引用,则 `remove` 将被忽略。

建议只将复合关系或私有关系标记为级联持久化。

合并

[edit | edit source]

EntityManager.merge() 操作用于将对分离对象的更改合并到持久化上下文。merge 不会直接将对象更新到数据库,而是将更改合并到持久化上下文(事务)中。当事务提交或持久化上下文被 *刷新* 时,对象将在数据库中被更新。

通常不需要 merge,尽管它经常被误用。要更新一个对象,只需读取它,然后通过它的 `set` 方法更改它的状态,然后提交事务。EntityManager 将找出所有更改并更新数据库。只有在具有持久化对象的脱机副本时,才需要 merge。*脱机* 对象是指通过不同的 EntityManager(或在 JEE 管理的 EntityManager 中的不同事务)读取的对象,或已克隆或序列化的对象。一个常见的情况是 stateless `SessionBean`,其中对象在一个事务中读取,然后在另一个事务中更新。由于更新是在不同的事务中处理的,并且具有不同的 EntityManager,因此必须先进行合并。merge 操作将查找/找到脱机对象的托管对象,并将脱机对象中发生更改的每个属性复制到托管对象中,以及级联任何标记为级联合并的相关对象。

merge 操作只能在事务中调用,在事务之外调用会抛出异常。merge 操作不是原位操作,也就是说,要合并的对象永远不会成为持久化上下文的一部分。任何进一步的更改都必须对 merge 返回的托管对象进行,而不是对脱机对象进行。

merge 通常用于现有的对象,但也可以用于新的对象。如果对象是新的,将创建该对象的副本并将其注册到持久化上下文,脱机对象本身不会被持久化。

merge 只能用于 `Entity` 对象,不能用于 `Embeddable` 对象、集合或非持久化对象。Embeddable 对象作为其拥有 `Entity` 的一部分自动合并。

合并示例

[edit | edit source]
EntityManager em = createEntityManager();
Employee detached = em.find(Employee.class, id);
em.close();
...
em = createEntityManager();
em.getTransaction().begin();
Employee managed = em.merge(detached);
em.getTransaction().commit();

级联合并

[edit | edit source]

对对象调用 merge 也会级联 merge 操作到任何标记为级联合并的关系中。即使关系不是级联合并,引用也会被合并。如果关系是级联合并,关系和每个相关对象都将被合并。直觉上,你可能会考虑将每个关系标记为级联合并,以避免不得不担心对每个对象调用合并,但这通常不是一个好主意。

将所有关系标记为级联合并会导致性能问题。如果一个对象具有很多关系,那么每次 merge 调用都需要遍历一个大型的对象图。

另一个问题是,如果脱机对象在某种程度上已损坏。例如,假设有一个 Employee,他有一个 manager,但该 manager 有一个不同的脱机 Employee 对象副本作为其 managedEmployee。这会导致同一个对象被合并两次,或者至少可能无法一致地确定哪个对象将被合并,因此你可能无法获得预期的合并更改。如果你没有更改对象,但其他用户更改了,如果 merge 级联到此未更改的对象,它将还原其他用户的更改,或者抛出 OptimisticLockException(取决于你的锁定策略)。这通常不可取。

建议只将复合关系或私有关系标记为级联合并。

瞬态变量

[edit | edit source]

merge 的另一个问题是瞬态变量。由于 merge 通常与对象序列化一起使用,如果关系被标记为 transient(Java 瞬态,而不是 JPA 瞬态),那么脱机对象将包含 null,并且 null 将被合并到对象中,即使这是不希望的。即使关系不是级联合并,也会发生这种情况,因为 merge 始终合并对相关对象的引用。通常,在使用序列化时,需要瞬态,以避免在只要求单个对象或一小部分对象的情况下序列化整个数据库。

一种解决方案是避免将任何内容标记为 transient,而是使用 JPA 中的 `LAZY` 关系来限制要序列化的内容(未访问的延迟关系通常不会被序列化)。另一种解决方案是在自己的代码中手动合并。

一些 JPA 提供程序提供扩展的 merge 操作,例如允许 *浅* 合并或 *深* 合并,或不合并引用的合并。

删除

[edit | edit source]

EntityManager.remove() 操作用于从数据库中删除对象。remove 不会直接从数据库中删除对象,而是标记对象在持久化上下文(事务)中被删除。当事务提交或持久化上下文被 *刷新* 时,对象将从数据库中被删除。

remove 操作只能在事务中调用,在事务之外调用会抛出异常。remove 操作必须在托管对象上调用,而不是在脱机对象上调用。通常,必须先 `find` 对象才能删除它,尽管可以对对象的 `Id` 调用 EntityManager.getReference(),然后对引用调用删除。根据 JPA 提供程序如何优化 getReferenceremove,它可能不需要从数据库中读取对象。

remove 只能用于 `Entity` 对象,不能用于 `Embeddable` 对象、集合或非持久化对象。Embeddable 对象作为其拥有 `Entity` 的一部分自动删除。

删除示例

[edit | edit source]
EntityManager em = getEntityManager();
em.getTransaction().begin();
Employee employee = em.find(Employee.class, id);
em.remove(employee);
em.getTransaction().commit();

级联删除

[edit | edit source]

对对象调用 remove 也会级联 remove 操作到任何标记为级联删除的关系中。

注意,级联删除只影响 remove 调用。如果有一个级联删除的关系,并且从集合中删除了一个对象,或取消引用了一个对象,它 *不会* 被删除。必须明确调用 remove 才能删除对象。一些 JPA 提供程序提供扩展来提供此行为,在 JPA 2.0 中,`OneToMany` 和 `OneToOne` 映射将有一个 `orphanRemoval` 选项来提供此行为。

转世

[edit | edit source]

通常情况下,被移除的对象将保持移除状态,但在某些情况下,您可能需要将对象恢复。这种情况通常发生在使用自然 ID(而非生成的 ID)时,因为新对象始终会获得一个新的 ID。通常,恢复对象的愿望源于糟糕的对象模型设计,通常是想要更改对象的类类型(这在 Java 中无法做到,因此必须创建新的对象)。通常情况下,最佳解决方案是更改对象模型,使对象包含一个类型对象来定义其类型,而不是使用继承。但有时恢复对象也是可取的。

如果在两个单独的事务中执行此操作,通常情况下是没问题的,首先您remove对象,然后您persist它。如果您想在同一事务中removepersist具有相同Id的对象,则操作会更加复杂。如果您对某个对象调用remove,然后对同一对象调用persist,那么它将不再被移除。如果您对某个对象调用remove,然后对具有相同Id的另一个对象调用persist,那么行为可能取决于您的 JPA 提供程序,而且可能无法正常工作。如果您在调用remove后调用flush,然后调用persist,那么该对象应该可以成功恢复。请注意,它将是一行新的数据,现有数据行将被删除,并且将插入一行新的数据。如果您希望更新同一行数据,则可能需要使用本机 SQL 更新查询。

高级

[edit | edit source]

刷新

[edit | edit source]

EntityManager.refresh() 操作用于从数据库中刷新对象的状态。这将恢复当前事务中对对象进行的任何未刷新的更改,并将对象的状态刷新为当前在数据库中定义的状态。如果发生了flush,它将刷新为已刷新的状态。刷新必须在受管对象上调用,因此如果您有非受管实例,则可能需要使用活动的EntityManagerfind该对象。

刷新将级联到任何标记为cascade刷新的关系,尽管它可能会根据您的获取类型以延迟方式进行,因此您可能需要访问该关系以触发刷新。refresh只能在Entity对象上调用,不能在Embeddable对象、集合或非持久对象上调用。Embeddable对象会作为其拥有Entity的一部分自动刷新。

刷新可用于恢复更改,或者如果您的 JPA 提供程序支持缓存,则可用于刷新过时的缓存数据。有时,希望Queryfind操作刷新结果。不幸的是,JPA 1.0 没有定义如何执行此操作。一些 JPA 提供程序提供了查询提示,允许在查询上启用刷新。

TopLink / EclipseLink : 定义查询提示"eclipselink.refresh"以允许在查询上启用刷新。

JPA 2.0 定义了一组用于刷新的标准查询提示,请参阅JPA 2.0 缓存 API

刷新示例

[edit | edit source]
EntityManager em = getEntityManager();
em.refresh(employee);

锁定

[edit | edit source]

请参阅读写锁定

获取引用

[edit | edit source]

EntityManager.getReference() 操作用于获取对对象的句柄,而无需加载该对象。它类似于find操作,但可能返回代理未提取对象。JPA 不要求getReference避免加载对象,因此某些 JPA 提供程序可能不支持它,而只是执行正常的查找操作。getReference返回的对象应该看起来像一个普通对象,如果您访问除Id以外的任何方法或属性,它将触发从数据库中刷新自身。

getReference的目的是,如果只有对象的Id并且想要避免加载对象,则可以在插入或更新操作中使用它作为相关对象的替代对象。请注意,getReference不会像find那样验证对象的是否存在。如果对象不存在,并且您尝试在插入或更新中使用未提取对象,则可能会发生外键约束冲突,或者如果您访问该对象,则可能会触发异常。

获取引用示例

[edit | edit source]
EntityManager em = getEntityManager();
Employee manager = em.getReference(Employee.class, managerId);
Employee employee = new Employee();
...
em.persist(employee);
employee.setManager(manager);
em.commit();

刷新

[edit | edit source]

EntityManager.flush() 操作可用于在事务提交之前将所有更改写入数据库。默认情况下,JPA 通常不会在事务提交之前将更改写入数据库。这通常是可取的,因为它可以避免在需要之前访问数据库、资源和锁。它还允许数据库写入按顺序进行,并以最佳方式进行批处理以访问数据库,并维护完整性约束,避免死锁。这意味着,当您调用persistmergeremove时,数据库 DML INSERT, UPDATE, DELETE不会执行,直到提交或触发刷新。

flush()不会执行实际的commitcommit仍然会在请求显式commit()时发生(在资源本地事务的情况下),或者在容器管理(JTA)事务完成时发生。

刷新有多种用途

  • 在执行查询之前刷新更改,以使查询能够返回新对象和持久化单元中进行的更改。
  • 插入持久对象,以确保其Id被分配并可供应用程序访问(如果使用IDENTITY排序)。
  • 将所有更改写入数据库,以便对任何数据库错误进行错误处理(在使用 JTA 或 SessionBean 时非常有用)。
  • 刷新和清除批处理,以便在单个事务中进行批处理。
  • 避免约束错误或恢复对象。

刷新示例

[edit | edit source]
public long createOrder(Order order) throws ACMEException {
  EntityManager em = getEntityManager();
  em.persist(order);
  try {
    em.flush();
  } catch (PersistenceException exception) {
    throw new ACMEException(exception);
  }
  return order.getId();
}

清除

[edit | edit source]

EntityManager.clear() 操作可用于清除持久化上下文。这将清除当前EntityManager或事务中读取、更改、持久化或移除的所有对象。已经通过flush写入数据库的更改,或对数据库进行的任何更改都不会被清除。通过EntityManager读取或持久化的任何对象都将被分离,这意味着对该对象进行的任何更改都不会被跟踪,并且在合并到新的持久化上下文中之前,不应再使用该对象。

clear可用于类似于回滚的操作,以放弃更改并重新启动持久化上下文。如果事务提交失败或执行回滚,则持久化上下文将自动被清除。

clear类似于关闭EntityManager并创建一个新的EntityManager,主要区别在于clear可以在事务正在进行时调用。clear还可用于释放EntityManager消耗的对象和内存。重要的是要注意,EntityManager负责跟踪和管理在其持久化上下文中读取的所有对象。在应用程序管理的EntityManager中,这包括自创建EntityManager以来读取的每个对象,包括EntityManager使用的每个事务。如果使用长期存在的EntityManager,这将是一个内在的内存泄漏,因此调用clear或关闭EntityManager并创建一个新的EntityManager是重要的应用程序设计考量。对于 JTA 管理的EntityManager,持久化上下文会在每个 JTA 事务边界自动被清除。

清除对于大型批处理作业也很重要,即使它们在一个事务中发生。批处理作业可以在同一事务中拆分为更小的批次,并且可以在每个批次之间调用clear以避免持久化上下文变得过大。

清除示例

[编辑 | 编辑源代码]
public void processAllOpenOrders() {
  EntityManager em = getEntityManager();
  List<Long> openOrderIds = em.createQuery("SELECT o.id from Order o where o.isOpen = true");
  em.getTransaction().begin();
  try {
    for (int batch = 0; batch < openOrderIds.size(); batch += 100) {
      for (int index = 0; index < 100 && (batch + index) < openOrderIds.size(); index++) {
        Long id = openOrderIds.get(batch + index);
        Order order = em.find(Order.class, id);
        order.process(em);
      }
      em.flush();
      em.clear();
    }
    em.getTransaction().commit();
  } catch (RuntimeException error) {
    if (em.getTransaction().isActive()) {
      em.getTransaction().rollback();
    }
  }
}

EntityManager.close() 操作用于释放应用程序管理的 EntityManager 的资源。JEE JTA 管理的 EntityManager 无法关闭,因为它们由 JTA 事务和 JEE 服务器管理。

EntityManager 的生命周期可以持续一个事务、请求或用户会话。通常,生命周期为每个请求,并且 EntityManager 在请求结束时关闭。从 EntityManager 获取的对象在 EntityManager 关闭时变为分离状态,并且如果在 EntityManager 关闭之前未访问过任何 LAZY 关系,则可能无法再访问它们。一些 JPA 提供商允许在关闭后访问 LAZY 关系。

示例关闭

[编辑 | 编辑源代码]
public Order findOrder(long id) {
  EntityManager em = factory.createEntityManager();
  Order order = em.find(Order.class, id);
  order.getOrderLines().size();
  em.close();
  return order;
}

获取委托

[编辑 | 编辑源代码]

EntityManager.getDelegate() 操作用于访问 JPA 提供商的 EntityManager 实现类,该类位于 JEE 管理的 EntityManager 中。JEE 管理的 EntityManager 将由 JEE 服务器中的一个代理 EntityManager 包裹,该服务器将请求转发到当前 JTA 事务中活动的 EntityManager。如果需要 JPA 提供商特定的 API,则 getDelegate() API 允许访问 JPA 实现以调用该 API。

在 JEE 中,管理的 EntityManager 通常会为每个 JTA 事务创建一个新的 EntityManager。此外,在 JTA 事务上下文之外的行为是有些不明确的。在 JTA 事务上下文之外,JEE 管理的 EntityManager 可能会为每个方法创建一个新的 EntityManager,因此 getDelegate() 可能会返回一个临时的 EntityManager 甚至 null。访问 JPA 实现的另一种方法是通过 EntityManagerFactory,该工厂通常不会被代理包裹,但可能在某些服务器中被包裹。

在 JPA 2.0 中,getDelegate() API 已被更通用的 unwrap() API 替换。

示例获取委托

[编辑 | 编辑源代码]
public void clearCache() {
  EntityManager em = getEntityManager();
  ((JpaEntityManager)em.getDelegate()).getServerSession().getIdentityMapAccessor().initializeAllIdentityMaps();
}

解包 (JPA 2.0)

[编辑 | 编辑源代码]

EntityManager.unwrap() 操作用于访问 JPA 提供商的 EntityManager 实现类,该类位于 JEE 管理的 EntityManager 中。JEE 管理的 EntityManager 将由 JEE 服务器中的一个代理 EntityManager 包裹,该服务器将请求转发到当前 JTA 事务中活动的 EntityManager。如果需要 JPA 提供商特定的 API,则 unwrap() API 允许访问 JPA 实现以调用该 API。

在 JEE 中,管理的 EntityManager 通常会为每个 JTA 事务创建一个新的 EntityManager。此外,在 JTA 事务上下文之外的行为是有些不明确的。在 JTA 事务上下文之外,JEE 管理的 EntityManager 可能会为每个方法创建一个新的 EntityManager,因此 getDelegate() 可能会返回一个临时的 EntityManager 甚至 null。访问 JPA 实现的另一种方法是通过 EntityManagerFactory,该工厂通常不会被代理包裹,但可能在某些服务器中被包裹。

示例解包

[编辑 | 编辑源代码]
public void clearCache() {
  EntityManager em = getEntityManager();
  em.unwrap(JpaEntityManager.class).getServerSession().getIdentityMapAccessor().initializeAllIdentityMaps();
}
华夏公益教科书