历史触发器和Hibernate

发布者    |      

最近,我帮助一位客户将遗留数据库迁移到Hibernate;其中一个更有趣的话题是版本控制和审计日志。实际上,在过去几个月里,历史数据的问题被多次提出。无论是一个遗留的SQL模式还是从损坏的对象数据库迁移,每个人都有自己的数据变更记录方式。

在本条博客中,我将介绍一种干净且不错的解决方案。我的建议自然与Hibernate集成。让我们使用数据库触发器和视图,而不是在应用层中用代码。

虽然实际上写一个Hibernate拦截器进行审计日志记录非常简单(示例可以在 Hibernate in Action 或在 Hibernate Wiki 中找到),但我们总是喜欢使用数据库系统的功能。如果多个应用程序共享相同的模式和数据,则数据库中的审计日志记录是最佳选择,并且从长远来看通常更容易维护。

首先,让我们创建一个我们想要实现变更历史的实体,一个简单的Item。在Java中,这个实体作为Item类实现。对于一个使用分离对象和自动乐观并发控制的Hibernate应用程序,我们通常会给它一个id和一个version属性

public class Item {

    private Long id = null
    private int version;
    private String description;
    private BigDecimal price;

    Item() {}
    
    ... // Accessor and business methods    
}

这个类然后使用Hibernate元数据映射到一个表

<hibernate-mapping>
<class name="Item" table="ITEM_VERSIONED>
    <id name="id" column="ITEM_ID">
        <generator class="native"/>
    </id>
    <version name="version" column="VERSION"/>
    <property name="description" column="DESC"/>
    <property name="price" column="PRICE"/>
</class>
</hibernate-mapping>

映射表的名称是ITEM_VERSIONED。这实际上不是一个正常的基表,而是一个数据库视图,它将两个基表中的数据连接起来。让我们看看Oracle中的这两个表

create table ITEM (
    ITEM_ID    NUMBER(19) NOT NULL,
    DESC       VARCHAR(255) NOT NULL,
    PRICE      NUMBER(19,2) NOT NULL,
    PRIMARY KEY(ITEM_ID)
)

create table ITEM_HISTORY (
    ITEM_ID    NUMBER(19) NOT NULL,
    DESC       VARCHAR(255) NOT NULL,
    PRICE      NUMBER(19,2) NOT NULL,
    VERSION    NUMBER(10) NOT NULL,
    PRIMARY KEY(ITEM_ID, VERSION)
)

ITEM是我们的真实实体关系。表ITEM_HISTORY有一个不同的主键,使用ITEM_IDVERSION列。我们的目标是让每个实体实例在ITEM(我们数据的最新版本)以及每个项目版本一行ITEM_HISTORY:

ITEM_ID   DESC            PRICE
1         A nice Item.    123,99
2         Another one.     34,44

ITEM_ID   DESC            PRICE      VERSION
1         The original.   123,99     0
1         An update.      123,99     1
1         A nice Item.    123,99     2
2         Another one.     34,44     0

因此,我们不是将我们的Java实体映射到任意两个表,而是映射到一个新的虚拟表,ITEM_VERSIONED。此视图合并了两个基本表的数据

create or replace view ITEM_VERSIONED (ITEM_ID, VERSION, DESC, PRICE) as
    select I.ITEM_ID as ITEM_ID,
        (select max(IH.VERSION)
            from ITEM_HISTORY HI
            where HI.ITEM_ID = I.ITEM_ID) as VERSION,
        I.DESC as DESC,
        I.PRICE as PRICE
    from   ITEM I

ITEM_VERSIONED视图使用相关子查询和theta风格的连接来获取历史表中特定项目的最高版本号,同时选择来自ITEM行的当前值。ITEM_HISTORY当然,我们也可以直接从

中读取所有数据,但这个查询更灵活,例如,如果您不想在历史中包含所有原始列。

Hibernate现在可以读取实体,并且它有一个版本号用于自动乐观锁定。然而,我们不能保存实体,因为这个视图是只读的。(在Oracle和大多数其他数据库中,使用连接创建的视图不能更新。)如果您尝试更新实体,您将收到异常。

create or replace trigger ITEM_INSERT
    instead of insert on ITEM_VERSIONED begin
    
    insert into ITEM(ITEM_ID, DESC, PRICE)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE);
           
    insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;

我们通过编写数据库触发器来解决这个问题。触发器将拦截对视图的所有更新和插入,并将数据重定向到基本表。这种触发器称为/INSTEAD OF/触发器。让我们首先处理插入

create or replace trigger ITEM_UPDATE
    instead of update on ITEM_VERSIONED begin
    
    update ITEM set
            DESC = :n.DESC,
            PRICE = :n.PRICE,
           where
            ITEM_ID = :n.ITEM_ID;
           
    insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;

此触发器将执行两个插入,并将数据在实体和实体历史表之间分割。接下来是更新操作ITEM_HISTORY表。

实际上,这就是实现基本历史功能所需的所有内容,只需检查您的数据库管理系统中的/INSTEAD OF/触发器支持即可。您甚至可以增强此模式并使其更加灵活:编写一个新的审计信息值类型类,包含用户和时间戳信息,并将审计信息属性添加到Java实体类中。使用Hibernate自定义UserType将此映射到视图中的某些新列,并通过设置属性在Hibernate中跟踪信息,在更新和插入发生时拦截器使用AOP将此方面从您的POJO中外部化...

祝好


回到顶部