视频

简介

使用 Oracle 和 Postgres 演练 @History / SQL2011 支持

与 Hibernate Envers 的比较

与 Hibernate Envers 采取的方法的比较

概述

SQL2011 引入了 SQL 的时间扩展,包括 AS OF SYSTEM TIMEVERSIONS BETWEEN SYSTEM TIME

Ebean 支持此 SQL 扩展。此支持分为 2 种一般情况,其中一些数据库内置了 SQL2011 支持(Oracle、DB2、MS SQL Server 2016),或者没有内置(Postgres、MySql),Ebean 会生成历史表/触发器/视图来支持此功能。

Ebean 中的当前支持

  • 通过 Total recall 的Oracle
  • 通过触发器/历史表的Postgres
  • 通过触发器/历史表的MySql
  • 通过触发器/历史表的H2

历史表

对于 Postgres、MySql 和 H2(以及通常没有内置 SQL2011 支持的数据库),Ebean 会为历史表和关联的触发器和视图生成 DDL。这是一种常见的“时间数据库设计”方法,已经使用了一段时间,并且不特定于 Ebean。例如,此方法已作为 Postgres 扩展实现 - arkhipov/temporal_tables

通常,历史表方法会导致

  • 创建的“历史”表与“基本表”具有相同(或非常相似)的结构
  • 默认情况下,“历史”表没有约束(没有主键、外键或索引)
  • 在基本表和历史表中添加了 2 个额外的列,用于“生效开始”和“生效结束”时间戳范围。Postgres 有一个时间戳范围类型,因此对于 Postgres 实现来说,它比 2 个单独的列更合适。
  • 视图用于union all组合“基本表”和“历史表”,以简化历史查询
  • 在更新和删除时使用触发器,将现有行数据从“基本表”复制到关联的“历史表”
  • 对“基本表”的架构更改(添加列等)会导致对“历史表”的镜像更改,并可能更新关联的触发器

另请参见

与变更日志相比

事务性

使用 @History 时,更改是事务性的,并具有关联的保证 - 更改不会被遗漏或丢失。

未绕过

使用 @History 时,数据库处理所有更改。如果使用其他框架(非 Ebean)或存储过程或原始 JDBC 来更新数据库,则所有这些更改都将被数据库触发器捕获。尽管 Ebean 使查询和生成适当的 DDL 变得更加容易,但捕获更改是数据库功能,并且不严格绑定到 Ebean 或应用程序代码。

易于查询

使用 @History,可以轻松查询和查看给定时间戳“截至”的数据,并查看给定 bean/行的版本和历史更改。使用变更日志也可以做到这一点,但需要更多工作(将变更日志加载到 ElasticSearch 中,并创建适当的查询以返回截至和版本类型数据)。

数据库开销

与使用变更日志相比,@History 的一个缺点是它确实会给数据库带来额外的成本,包括存储(历史表占用存储空间)和对响应时间的的影响,因为更新和删除现在也会将数据复制到历史表中。

使用 Oracle Total Recall 的方法通过在后台和批处理中处理从重做中复制的数据到历史表来减少对响应时间的的影响,这可能意味着它通常可以比传统的基于触发器的方法更广泛地使用(在更多基本表上)。

架构更改

与使用变更日志相比,@History 的一个缺点是架构更改(如添加列)可能意味着更多工作/复杂性,因为更改还需要反映在关联的历史表和触发器中。Ebean 中的数据库迁移支持通过生成对历史表和触发器等的所有适当更改来缓解此问题。

@History

@History 注解添加到您希望获得历史支持的实体 Bean。对于 Postgres 和 MySql,这意味着 Ebean 将生成 DDL(触发器、视图等)以支持底层表上的历史记录。对于 Oracle,添加 @History 意味着该表已将闪回存档分配给该表。

@HistoryExclude

@HistoryExclude 注解添加到应从历史记录中排除的实体 Bean 属性。预计这将用于大型文本或 blob 列,其中可能存在与保留历史记录值相关的相对较高的存储成本,并且希望排除这些列。

ManyToMany 交集

对于 ManyToMany 关系的交集(桥接)表,默认情况下交集表也具有历史记录。也就是说,如果将 @History 放在实体 Bean 上,则默认情况下 @ManyToMany 属性的交集表具有历史记录支持(关联的历史记录表、视图和触发器)。

要从交集表中排除历史记录,应将 @HistoryExclude 放在 @ManyToMany 属性上。

查询时间

时间戳可与 Query.asOf(Timestamp) 一起使用,Ebean 将生成查询,以便返回的对象图表示给定时间戳的状态。更准确地说,对于具有历史记录的表,查询返回表示给定时间戳的表的行,对于查询中包含的其他表,返回的行表示“当前值”。

// asOf some time in the past like 1 hour ago, 1 week ago, 1 month ago etc
Timestamp asOf = ...;

Customer customer =
    Customer.find.query()
        .asOf(asOf)
        .fetch("billingAddress")
        .where().eq("name", "jim")
        .findOne();

场景:客户和地址都有历史记录

在上述查询中,如果客户地址都有历史记录,则客户数据和地址数据都将返回“截至”给定时间戳。

场景:客户有历史记录,但地址没有

在上述查询中,如果客户有历史记录,但地址没有,则客户数据将返回“截至”给定时间戳,但地址数据将使用“当前数据”返回。

场景:地址延迟加载

AS OF 时间戳传播到所有二级查询,包括延迟加载查询。也就是说,AS OF 时间戳适用于由其加载的整个对象图,而不仅仅是“原始查询”。如果地址延迟加载并且地址有历史记录,则地址数据也将“截至”原始时间戳。

版本之间

Query.findVersionsBetween() 用于返回一段时间内给定对象的版本列表。返回的版本 Bean 包含与先前版本之间的“差异”以及生效的开始和结束时间戳。

Timestamp start = ...;
Timestamp end = ...;

List<Version<Customer>> customerVersions =
    Customer.find.query()
      .where()
      .idEq(42)
      .findVersionsBetween(start, end);

for (Version<Customer> customerVersion : customerVersions) {
  Customer bean = customerVersion.getBean();
  Map<String, ValuePair> diff = customerVersion.getDiff();
  Timestamp effectiveStart = customerVersion.getStart();
  Timestamp effectiveEnd = customerVersion.getEnd();
}

谁和何时

通常情况下,实体 Bean 应包含 @WhoCreated@WhoModified@WhenCreated@WhenModified 属性。

/**
 * Common properties used by many entity beans.
 */
@MappedSuperclass
public class BaseModel {

  @Id
  Long id;

  @Version
  Long version;

  @WhenCreated
  Timestamp whenCreated;

  @WhenModified
  Timestamp whenModified;

  @WhoCreated
  String whoCreated;

  @WhoModified
  String whoModified;
  ...
@History
@Entity
@Table(name="customer")
public class Customer extends BaseModel {

  @Size(max = 100)
  String name;
  ...

Postgres

Postgres 有一个现有的 arkhipov/temporal_tables 扩展,Postgres/Ebean 用户可以使用此扩展。默认情况下,Ebean 不使用此扩展,而是生成类似的触发器。使用或不使用 arkhipov/temporal_tables 的区别归结为 DDL 生成。最终,Ebean 能够支持 arkhipov/temporal_tables 的 DDL 生成会很好,但这尚未内置到 Ebean 中。

Postgres 中历史支持的另一个值得注意的方面是,我们可以利用时间戳范围类型,相对于使用 2 个单独的时间戳列,这在索引性能方面有望带来好处。

MySql

在需要对生产数据库应用 DDL 更改的情况下,MySql 存在一个问题。在由于架构更改(如添加列)需要更改触发器的情况下,MySql 没有“创建或替换触发器”,而是需要持有表锁来删除并创建触发器。在生产数据库中持有表锁可能是一个问题,并限制架构更改。

Oracle

Rob 的观点:Oracle Total recall 在显式触发器/历史表方法方面有一些不错的优势。主要优势是

  • 性能:低前台成本 - 历史表填充在后台发生,并且导致前台响应时间开销/影响大大降低
  • 性能:批量处理 - 历史表填充可以在批量中处理,从而降低许多小更新的开销/影响
  • 管理:减少 DDL - 表更改(添加列等)会自动处理,从而降低维护触发器/视图/历史表的管理成本。