Hibernate Event Listener を用いた変更履歴テーブルの構築

## 背景
### 実装目的

エンティティの内容が変更された際に、変更前後のデータをJSON形式で変更履歴テーブルに格納したい。これにより、データの変更履歴を明確に管理し、監査やトラブルシューティングに活用できる。

### Hibernate Event Listenerの仕組み
Hibernate の Event Listener を使用すると、エンティティの生成や更新を監視し、変更が発生した際に自動的に処理を実行できる。
これにより、データの変更をリアルタイムで検知し、履歴を記録できる。

### メリット
・追加の処理を記述せずに、変更が発生したタイミングで履歴を記録可能。
・Hibernate の仕組みを利用するため、ビジネスロジック側で特別な処理を追加する必要がない。

### デメリット
・エンティティにはさまざまなデータ型が含まれるため、JSON への変換時に失敗する可能性がある。
・リスナーはエンティティの変更と同時に動作するため、大量の更新が発生すると業務処理全体のパフォーマンスに影響を及ぼす可能性がある。

## 実際の実装
##### 参考にした実装
https://vladmihalcea.com/hibernate-event-listeners/

##### 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