视频

持久化上下文

对 Ebean 的持久化上下文和它支持的各种范围的说明。

定义

虽然 Ebean 没有 JPA 实体管理器,但它确实有持久化上下文。事实上,你可以进一步说,任何值得使用的 ORM 都需要一个持久化上下文才能构建一致的对象图。

JPA v1.0 规范 - 第 5.1 节

“持久化上下文是一组受管实体实例,对于任何持久实体标识,都有一个唯一的实体实例。在持久化上下文中,实体实例及其生命周期由实体管理器管理。”

Ebean 有一个持久化上下文来确保唯一的实体实例,但 Ebean 对生命周期管理的方法与 JPA 不同。

也就是说,Ebean 有一个持久化上下文来确保唯一的实体实例。但对生命周期管理的方法与 JPA 不同。Ebean 没有实体管理器和持久化/合并/刷新生命周期方法,并且持久化上下文不参与持久化。

唯一的实体实例

Ebean 将持久化上下文用于查询和延迟加载(当它构建对象图时)。这样做的目的是确保构建一致的对象图(每个标识符 1 个唯一实例)。

确保唯一实例等同于构建一致的对象图

例如,在获取订单列表及其客户时……持久化上下文确保你只为其给定的 ID 获取 1 个客户实例(例如,你不允许有 2 个或更多个“客户 ID=7”的实例)。

你甚至可以说,由于关系结果集的性质,任何值得使用的 ORM 在从关系结果集构建对象图时都需要一个持久化上下文。

例如,如果你没有持久化上下文,并且允许“客户 7”有 2 个或更多个实例……并且修改了一个实例但没有修改另一个实例……事情会变得非常糟糕。“持久化上下文”确保用户/应用程序代码使用唯一的实体实例。

持久化

Ebean 对生命周期管理有不同的方法。核心区别在于,使用 Ebean 时,每个 Bean 本身都有自己的脏检查(检测何时修改了 Bean,并保留其旧/原始值以进行乐观并发检查)。

这意味着,对于 Ebean,在持久化 Bean 时,不需要或不使用持久化上下文。

Ebean 的持久化上下文不参与持久化。

使用 JPA 实现时,通常由实体管理器执行脏检查。实体管理器通常保留旧/原始值以进行乐观并发检查,并且需要将 Bean “附加”到实体管理器才能“刷新”(作为插入/更新/删除)。[注意:基于 JDO 的 JPA 实现对此操作的方式略有不同]。

Ebean 方法的优点

  • 无需管理实体管理器
  • 使用持久化/合并/刷新等方式保存/删除比附加/分离 Bean 更简单

Ebean 方法的缺点

  • Ebean 假设标量类型是不可变的。大多数标量类型(String、Integer、Double、Float、BigDecimal 等)是不可变的,但有些类型(例如 java.util.Date)不是。这意味着,如果您使用 Ebean 更改了 java.util.Date,Ebean 将不会检测到更改——相反,您必须设置不同的 java.util.Date 实例。

范围

Ebean 为持久化上下文提供了 3 个范围——事务范围查询范围每个对象图范围

事务范围

使用 Ebean 时,持久化上下文默认采用事务范围。这意味着,当您开始一个新事务(隐式或显式)时,Ebean 将启动一个新的持久化上下文。

持久化上下文在事务结束后仍然存在。这使得在事务结束后发生的任何延迟加载都可以使用与创建实例时相同的持久化上下文。

这意味着,持久化上下文

  • 在事务开始时启动
  • 在事务范围内用于构建所有对象图(查询)
  • 在事务结束后仍然存在,以便在该对象图上发生的任何延迟加载也使用相同的持久化上下文

事务范围的持久化上下文作为“一级缓存”

持久化上下文有时被称为“一级缓存”或 L1 缓存。我还看到它被描述为“事务缓存”,因为它最常被限定为一个事务。

// a new persistence context started with the transaction
Ebean.beginTransaction();
try {
  // find "order 72" results in that instance being put
  // into the persistence context
  Order order = Ebean.find(Order.class, 72);

  // finds an existing "order 72" in the persistence context
  // ... so just returns that instance
  Order o2 = Ebean.find(Order.class, 72);
  Order o3 = Ebean.getReference(Order.class, 72);

  // all the same instance
  Assert.assertTrue(order == o2);
  Assert.assertTrue(order == o3);
} finally {
  Ebean.endTransaction();
}

上面的代码显示只有 1 个“订单 72”实例。当我们尝试再次获取它(在单个持久化上下文的范围内)时,最终会得到相同的实例。

但是,通常您不会编写在单个事务中多次获取相同订单的代码。上面的代码不是您通常会编写的代码。

一个更实际的示例是在使用持久化上下文时

// a new persistence context started with the transaction
Ebean.beginTransaction();

try {
  // find "customer 1" results in this instance being
  // put into the persistence context
  Customer customer = Ebean.find(Customer.class, 1);

  // for this example … "customer 1" placed "order 72"
  // when "order 72" is fetched/built it has a foreign
  // key value customer_id = 1...
  // As customer 1 is already in the persistence context
  // this same instance of customer 1 is used
  Order order = Ebean.find(Order.class, 72);
  Customer customerB = order.getCustomer();

  // they are the same instance
  Assert.assertTrue(customer == customerB);
} finally {
  Ebean.endTransaction();
}

从这些示例中,您应该能够看到持久化上下文在某种程度上充当了缓存。当您获取对象图并浏览它们时,它有时可以减少所需的数据库查询数量。

但是,持久化上下文的首要功能是确保……给定标识符的唯一实例(以便以一致的方式构建对象图)。它有时看起来/表现得像缓存,这更像是一种副作用。

查询范围

查询范围的目的是有效绕过事务范围的持久性上下文(l1 缓存),以便从数据库读取最新数据。

一般情况下,您正在进行事务,并且您希望确保特定查询命中数据库以获取最新数据。在这样做时,我们希望避免使用当前事务范围的持久性上下文。

// transaction with a persistence context
Transaction transaction = ...

// Fetch customer 42 hitting the database
// ... ignoring the transaction persistence context
Customer customer = Ebean.find(Customer.class)
      .setId(42)
      // ignore transaction persistence context
      .setPersistenceContextScope(QUERY)
      .findOne();

对象图范围 - 大查询

当您使用 findEach()findStream()findIterate() 流式查询时,Ebean 会为持久性上下文使用特殊范围。这些查询预计会遍历任意大的查询请求,因此我们不想在内存中保存所有对象图。

对于这些流式查询,Ebean 会在发生迭代时创建一个新的 PersistenceContext,并且范围实际上会变成每个对象图。