背景
実装目的
エンティティの内容が変更された際に、変更前後のデータをJSON形式で変更履歴テーブルに格納したい。これにより、データの変更履歴を明確に管理し、監査やトラブルシューティングに活用できる。
Hibernate Event Listenerの仕組み
Hibernate の Event Listener を使用すると、エンティティの生成や更新を監視し、変更が発生した際に自動的に処理を実行できる。
これにより、データの変更をリアルタイムで検知し、履歴を記録できる。
メリット
・追加の処理を記述せずに、変更が発生したタイミングで履歴を記録可能。
・Hibernate の仕組みを利用するため、ビジネスロジック側で特別な処理を追加する必要がない。
デメリット
・エンティティにはさまざまなデータ型が含まれるため、JSON への変換時に失敗する可能性がある。
・リスナーはエンティティの変更と同時に動作するため、大量の更新が発生すると業務処理全体のパフォーマンスに影響を及ぼす可能性がある。
実際の実装
参考にした実装
HibernateConfig.java (Hibernate のイベントリスナーを設定する)
@Configuration
public class HibernateConfig {
@PersistenceUnit
private EntityManagerFactory emf;
@Autowired
public RootAwareUpdateAndDeleteEventListener rootAwareUpdateAndDeleteEventListener;
@PostConstruct
protected void init() {
SessionFactoryImpl sessionFactory = emf.unwrap(SessionFactoryImpl.class);
EventListenerRegistry registry = sessionFactory.getServiceRegistry()
.getService(EventListenerRegistry.class);
registry.getEventListenerGroup(EventType.PERSIST)
.appendListener(rootAwareUpdateAndDeleteEventListener);
registry.getEventListenerGroup(EventType.FLUSH_ENTITY)
.appendListener(rootAwareUpdateAndDeleteEventListener);
}
}
JacksonConfig.java (HibernateのLazy Loadingに関連する問題を解決するためのJackson設定)
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
Hibernate5Module hibernate5Module = new Hibernate5Module();
hibernate5Module.enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING);
SimpleModule module = new SimpleModule();
module.addSerializer(HibernateProxy.class, new JsonSerializer<HibernateProxy>() {
@Override
public void serialize(HibernateProxy value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
Object unproxiedValue = Hibernate.unproxy(value);
gen.writeObject(unproxiedValue);
}
});
builder.modulesToInstall(hibernate5Module);
builder.modules(module);
builder.featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
return builder;
}
}
RootAwareEventListenerIntegrator.java (Hibernate のカスタムイベントリスナーを統合する)
public class RootAwareEventListenerIntegrator implements Integrator {
@Autowired
public RootAwareInsertEventListener rootAwareInsertEventListener;
@Autowired
public RootAwareUpdateAndDeleteEventListener rootAwareUpdateAndDeleteEventListener;
@Override
public void disintegrate(
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
}
@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
final EventListenerRegistry eventListenerRegistry = sessionFactory
.getServiceRegistry()
.getService(EventListenerRegistry.class);
eventListenerRegistry.appendListeners(
EventType.PERSIST,
rootAwareInsertEventListener);
eventListenerRegistry.appendListeners(
EventType.FLUSH_ENTITY,
rootAwareUpdateAndDeleteEventListener);
}
}
RootAwareInsertEventListener.java (エンティティの新規作成イベントをフックするリスナー)
@Component
public class RootAwareInsertEventListener implements PersistEventListener {
@Autowired
private RevisionInfoRepository revisionInfoRepository;
@Autowired
private ObjectMapper objectMapper;
// 変更履歴を保存しないエンティティを追加
private static final Set<Class<?>> excludedEntities = new HashSet<>();
static {
excludedEntities.add(RevisionInfo.class);
}
@Override
public void onPersist(PersistEvent event, Map transientEntities) throws HibernateException {
// 挿入されたエンティティを取得
final Object entity = event.getObject();
// 排除エンティティ判定
if (excludedEntities.contains(entity.getClass())) {
return;
}
// セッション情報を取得
EventSource session = event.getSession();
SessionFactoryImplementor sessionFactory = session.getFactory();
MetamodelImplementor metamodel = (MetamodelImplementor) sessionFactory.getMetamodel();
// エンティティに関連するエンティティのメタデータを取得
EntityPersister entityPersister = metamodel.entityPersister(entity.getClass());
// テーブルにあるすべてのキー(プロパティ名)を取得
String[] propertyNames = entityPersister.getPropertyNames();
// 変更履歴 RevisionInfoオブジェクトを作成し、値を設定
RevisionInfo revisionInfo = new RevisionInfo();
revisionInfo.setChangeDateTime(LocalDateTime.now());
revisionInfo.setTargetTable(entityPersister.getEntityName());
// プライムキー(複数ある場合は"_"でつなげる)
Type idType = entityPersister.getIdentifierType();
StringBuilder keyName = new StringBuilder();
if (idType.isComponentType()) {
for (int i = 0; i < propertyNames.length; i++) {
if (i > 0) {
keyName.append("_");
}
keyName.append(propertyNames[i]);
}
} else {
keyName.append(entityPersister.getIdentifierPropertyName());
}
revisionInfo.setPrimaryKey(keyName.toString());
// プライムキー値(複数ある場合は"_"でつなげる)
StringBuilder keyValue = new StringBuilder();
if (idType.isComponentType()) {
Object primaryKey = session.getIdentifier(entity);
Object[] compositeValues = (Object[]) primaryKey;
for (int i = 0; i < compositeValues.length; i++) {
if (i > 0) {
keyValue.append("_");
}
keyValue.append(compositeValues[i].toString());
}
} else {
Object primaryKey = session.getIdentifier(entity);
keyValue.append(primaryKey.toString());
}
revisionInfo.setPrimaryValue(keyValue.toString());
// 変更ユーザID
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUserId = authentication.getName();
revisionInfo.setChangeUserId(currentUserId);
// 変更種類
revisionInfo.setChangeType(ChangeType.INSERT.getValue());
// 変更前後のデータをJSONに変換
try {
// 変更前データは空
revisionInfo.setOldData(new JSONObject().toString());
// 変更後データ
String newData = objectMapper.writeValueAsString(entity);
revisionInfo.setNewData(newData);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// リビジョン情報を保存
revisionInfoRepository.save(revisionInfo);
}
@Override
public void onPersist(PersistEvent event) {
onPersist(event);
}
}
RootAwareUpdateAndDeleteEventListener.java (エンティティの更新/削除イベントをフックするリスナー)
@Component
public class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener {
@Autowired
private RevisionInfoRepository revisionInfoRepository;
@Autowired
private ObjectMapper objectMapper;
// 変更履歴を保存しないエンティティを追加
private static final Set<Class<?>> excludedEntities = new HashSet<>();
static {
excludedEntities.add(RevisionInfo.class);
}
@Override
public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
// エンティティのエントリー情報を取得
final MutableEntityEntry entry = (MutableEntityEntry) event.getEntityEntry();
// 保存または更新されたエンティティを取得
final Object entity = event.getEntity();
final boolean mightBeDirty = entry.requiresDirtyCheck(entity);
final Object[] loadedState = entry.getLoadedState();
// 排除エンティティ判定
if (excludedEntities.contains(entity.getClass())) {
return;
}
// セッション情報を取得
EventSource session = event.getSession();
SessionFactoryImplementor sessionFactory = session.getFactory();
MetamodelImplementor metamodel = (MetamodelImplementor) sessionFactory.getMetamodel();
// エンティティに関連するエンティティのメタデータを取得
EntityPersister entityPersister = metamodel.entityPersister(entity.getClass());
// 変更前のエンティティ
Object oldEntity = entityPersister.instantiate(entry.getId(), event.getSession());
entityPersister.setPropertyValues(oldEntity, loadedState);
// 現在のエンティティ
Object currentEntity = event.getEntity();
// テーブルにあるすべてのキー(プロパティ名)を取得
String[] propertyNames = entityPersister.getPropertyNames();
// 変更履歴 RevisionInfoオブジェクトを作成し、値を設定
RevisionInfo revisionInfo = new RevisionInfo();
revisionInfo.setChangeDateTime(LocalDateTime.now());
revisionInfo.setTargetTable(entityPersister.getEntityName());
// プライムキー(複数ある場合は"_"でつなげる)
Type idType = entityPersister.getIdentifierType();
StringBuilder keyName = new StringBuilder();
if (idType.isComponentType()) {
for (int i = 0; i < propertyNames.length; i++) {
if (i > 0) {
keyName.append("_");
}
keyName.append(propertyNames[i]);
}
} else {
keyName.append(entityPersister.getIdentifierPropertyName());
}
revisionInfo.setPrimaryKey(keyName.toString());
// プライムキー値(複数ある場合は"_"でつなげる)
StringBuilder keyValue = new StringBuilder();
if (idType.isComponentType()) {
Object primaryKey = session.getIdentifier(entity);
Object[] compositeValues = (Object[]) primaryKey;
for (int i = 0; i < compositeValues.length; i++) {
if (i > 0) {
keyValue.append("_");
}
keyValue.append(compositeValues[i].toString());
}
} else {
Object primaryKey = session.getIdentifier(entity);
keyValue.append(primaryKey.toString());
}
revisionInfo.setPrimaryValue(keyValue.toString());
// 変更ユーザID
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUserId = authentication.getName();
revisionInfo.setChangeUserId(currentUserId);
// 更新/削除判定
if (mightBeDirty) {
// 変更種類
revisionInfo.setChangeType(ChangeType.UPDATE.getValue());
// 変更前後のデータをJSONに変換
try {
// 変更前データ
String oldData = objectMapper.writeValueAsString(oldEntity);
revisionInfo.setOldData(oldData.toString());
// 変更後データ
String newData = objectMapper.writeValueAsString(currentEntity);
revisionInfo.setNewData(newData);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
} else {
// 変更種類
revisionInfo.setChangeType(ChangeType.DELETE.getValue());
// 変更前後のデータをJSONに変換
try {
// 変更前データ
String oldData = objectMapper.writeValueAsString(loadedState);
revisionInfo.setOldData(oldData);
// 変更後データは空
revisionInfo.setNewData(new JSONObject().toString());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// リビジョン情報を保存
revisionInfoRepository.save(revisionInfo);
}
}
これでエンティティ変更の情報を取得して、変更履歴テーブルに保存することができる。
同じ目的の異なる実装方法
Debezium Listener を用いた変更履歴テーブルの構築
https://qiita.com/teaco/items/245e1b18ea758db8f578