0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-19

背景

実装目的

エンティティの内容が変更された際に、変更前後のデータを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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?