BullよりElk

洋服と食をこよなく愛するWebプログラマ。

Hibernate Enversで履歴管理

エンティティが登録、更新、削除された履歴を管理します。

Hibernate Enversは、履歴管理テーブル全体を管理するエンティティ(デフォルトでRevinfo)をいて、自動採番されるid(プロパティ名はrev)と履歴を永続化した日時(プロパティ名はrevtstmp)の2プロパティを持っています。
エンティティ(後述のClient)の登録、更新、削除された履歴を管理するエンティティ(後述のClientHistory。デフォルトのクラス名はClientAud)が履歴管理するプロパティに加えて、Revinfoのid(デフォルトのプロパティ名はrev)とどのようなイベントが行われたかのフィールド(デフォルトのプロパティ名はrevtype)を持っています。ちなみにrevは、Revisionの略です。

環境

やること

hibernate-enverを依存関係に追加する
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
</dependency>
@Auditedをエンティティクラス(またはエンティティの特定のフィールド)に付与する

今回は、Clientエンティティを使って説明します。

Client.java

@Entity
@Audited
public class Client implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Getter
	private Integer id;

	@Getter
	@Setter
	private String name;

	@Embedded
	@Getter
	@Setter
	private Address address;
}

Address.java

@Embeddable
@Getter
@Setter
@Audited
public class Address implements Serializable {

	private int postalCode;

	private String address1;

	private String address2;
}

ここでClientエンティティの履歴を管理したいのでクラスに対して@Auditedを付与します。

Clientエンティティの履歴を管理するエンティティは、デフォルトでClientAudとなります。Audは、Auditの略ですがわかりずらいのでClientHistoryにします。また、ClientHistoryのプロパティ名も、それぞれrevをauditId、revtypeをauditTypeに変更します。application.propertiesに次のように書けば変更できます。

spring.jpa.properties.org.hibernate.envers.audit_table_suffix=_history
spring.jpa.properties.org.hibernate.envers.revision_field_name=audit_id
spring.jpa.properties.org.hibernate.envers.revision_type_field_name=audit_type
テーブルを準備する

Flywayを使うので、sqlファイルに次のように書きます。
これで、Clientと対になるclientテーブル、ClientHistoryと対になるclient_historyテーブル、履歴管理テーブル全体の管理をするrevinfoが作られます。

create table client (
 id integer not null auto_increment,
 name varchar(255),
 postal_code integer not null,
 address1 varchar(255),
 address2 varchar(255),
 primary key (id)
)
 engine=InnoDB;

create table client_history (
 id integer not null,
 name varchar(255),
 postal_code integer,
 address1 varchar(255),
 address2 varchar(255),
 audit_id integer not null,
 audit_type tinyint,
 primary key (id, audit_id)
) engine=InnoDB;

create table revinfo (
 rev integer not null auto_increment,
 revtstmp bigint,
 primary key (rev)
)
engine=InnoDB;

alter table client_history add constraint FK_client_history_revinfo foreign key (audit_id) references revinfo (rev);

できたテーブルがこれです。

https://d2l930y2yx77uc.cloudfront.net/production/uploads/images/6744813/picture_pc_e03a99bea17d7c41c2c71ac4ad27b504.jpg

登録、更新、削除やってみる

それぞれ、Hibernateが生成したSQLです。

登録
Hibernate: insert into client (address1, address2, postal_code, name) values (?, ?, ?, ?)
Hibernate: insert into revinfo (revtstmp) values (?)
Hibernate: insert into client_history (audit_type, address1, address2, postal_code, name, id, audit_id) values (?, ?, ?, ?, ?, ?, ?)

作られたデータ

"client" : {
    "id": 1,
    "name": "hey,inc",
    "postalCode": 1500011,
    "address1": "東京都",
    "address2": "渋谷区東3丁目16番3号 エフ・ニッセイ恵比寿ビル4階"
}

"clientHistory": {
    "id": 1,
    "name": "hey,inc",
    "postalCode": 1500011,
    "address1": "東京都",
    "address2": "渋谷区東3丁目16番3号 エフ・ニッセイ恵比寿ビル4階",
    "audit_id": 1,
    "audit_type": 0
}

"revinfo": {
    "rev": 1,
    "revtstmp": 1528214457258
}
更新
Hibernate: update client set address1=?, address2=?, postal_code=?, name=? where id=?
Hibernate: insert into revinfo (revtstmp) values (?)
Hibernate: insert into client_history (audit_type, address1, address2, postal_code, name, id, audit_id) values (?, ?, ?, ?, ?, ?, ?)

作られたデータ

"client" : {
   "id": 1,
   "name": "Coiney,inc",
   "postalCode": 1500011,
   "address1": "東京都",
   "address2": "渋谷区東3丁目16番3号 エフ・ニッセイ恵比寿ビル4階"
}

"clientHistory": {
   "id": 1,
   "name": "Coiney,inc",
   "postalCode": 1500011,
   "address1": "東京都",
   "address2": "渋谷区東3丁目16番3号 エフ・ニッセイ恵比寿ビル4階",
   "audit_id": 2,
   "audit_type": 1
}

"revinfo": {
   "rev": 2,
   "revtstmp": 1528214465266
}
削除
Hibernate: delete from client where id=?
Hibernate: insert into revinfo (revtstmp) values (?)
Hibernate: insert into client_history (audit_type, address1, address2, postal_code, name, id, audit_id) values (?, ?, ?, ?, ?, ?, ?)

作られたデータ

"clientHistory": {
   "id": 1,
   "name": null,
   "postalCode": null,
   "address1": null,
   "address2": null,
   "audit_id": 3,
   "audit_type": 2
}
"revinfo": {
   "rev": 3,
   "revtstmp": 1528214465681
}

ここでauditTypeの0、1、2は何ですか?という疑問が残ってるかと思いますが、それぞれ次の通りです。
0 → 登録
1 → 更新
2 → 削除
イベントにあったタイプが与えられます。

Hibernate Enversで履歴管理が楽になりました。現場からは以上でした。