最近,我帮助一位客户将遗留数据库迁移到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_ID和VERSION列。我们的目标是让每个实体实例在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中外部化...
祝好