H2-无法在“插入后”触发器中写入数据库

时间:2019-02-07 12:36:15

标签: triggers h2 sql-insert

我300%确信id字段是自动生成的,因为在添加触发器之前这是完美的工作。

我有一个实体,该实体扩展了带有自动生成的id字段的基本实体:

@MappedSuperclass
public abstract class BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

我已经在H2中为该实体注册了一个触发器:

create trigger after_subtest_result_insert after insert on subtest_result
for each row call "package.path.SubtestResultTrigger";

触发器本身:

public class SubtestResultTrigger implements Trigger {

  private static final int EQUIPMENT_ID = 5;

  @Override
  public void init(Connection conn, String schemaName, String triggerName,
      String tableName, boolean before, int type) {
  }

  @Override
  public void fire(Connection conn, Object[] oldRow, Object[] newRow)
      throws SQLException {
    try (PreparedStatement ps = conn.prepareStatement(
        "update equipment e set " +
        (...)
       )
    ) {
      ps.setObject(1, newRow[EQUIPMENT_ID]);
      ps.setObject(2, newRow[EQUIPMENT_ID]);

      ps.executeUpdate();
    }
  }

  @Override
  public void close() throws SQLException {
  }

  @Override
  public void remove() throws SQLException {
  }
}

取消注释ps.executeUpdate();时,它会以HibernateException: The database returned no natively generated identity value开头。

似乎H2幕后某个地方从执行的最后一个准备好的语句(而不是第一个准备好的语句)获取生成的键,因为当不运行触发器中的更新时,一切都很好。有任何解决方法或解决方法吗?

编辑:我知道发生了什么,但我仍然不知道如何解决它。深入研究代码,H2驱动程序具有一个会话范围的GeneratedKeys对象,该对象在每次请求键时都会清除,并且可能每次在同一会话中执行PreparedStatement时都会被覆盖。这实际上使后插入触发器根本无法将任何内容写入数据库。我将编写一个错误报告,与此同时,我要么不必使用触发器(在这种情况下很难,我已经只能诉诸触发器,因为每种选择都更糟),或者完全放弃H2,我不确定我能做得到,因为堆栈不在我手中。

2 个答案:

答案 0 :(得分:1)

我遇到了同样的问题,但是发现了不同的解决方法,您可能会更喜欢。像您一样,我正在使用Hibernate和Spring。

我能够使H2触发器完全正常工作,但是关键是要在触发器中使用Hibernate进行所有数据库操作。这是我的操作方式:

  1. 已使用this technique to create a static method from which to access the Spring ApplicationContext。我是这样做的:

    @Service
    public class SpringContextHook {
    
        private static ApplicationContext context;
    
        @Autowired
        public SpringContextHook(ApplicationContext inContext) {
            context = inContext;
        }
    
        public static ApplicationContext getContext() {
            return context;
        }
    }
    
  2. 在H2触发器中,我通过在Spring JPA中进行一些挖掘来获得当前活动的Session:

    private Session getHibernateSession() {
        ApplicationContext context = SpringContextHook.getContext();
        EntityManagerFactory emf = (EntityManagerFactory) context.getBean("entityManagerFactory");
        EntityManager entityManager = SharedEntityManagerCreator.createSharedEntityManager(emf, null, true);
        return entityManager.unwrap(Session.class);
    }
    
  3. 使用Hibernate @Entity对象执行更新,并使用getHibernateSession().save(entity)保存它们。
  4. 我必须确保不要对Hibernate对象执行操作,这就是触发触发器的原因。

如果您有兴趣执行此策略并需要更多帮助,我可以提供更多详细信息。

答案 1 :(得分:0)

编辑:之后,我选择了arogos的解决方案。我将其保留在此处,以防有人需要进行更深奥的更新,而Hibernate不会削减它。

尽管我从未设法找到真正的解决方案,但我在Spring Boot的上下文中找到了一种解决方法,只要您需要或不介意在更新时也使用触发器,该方法就可以工作。它有很多缺点,迫使我不得不首先使用触发器,但是至少在必须支持触发器的其他两个DBMS项目中,这至少将问题的范围缩小到仅H2和仅插入问题方法就可以了。

我基本上创建了一个更新触发器,一个后插入处理程序,如果DBMS为H2,则强制伪造更新,以及一个注释提醒every insert operation that does not come from a REST endpoint request must invoke the handler manually。实际上,这种行为是我尝试使用JPA的@PostInsert@PostUpdate并收到由于尝试从刚写入的表中读取而引起的错误后才求助于触发器的原因-我的理解是有一个读锁,因此您的@PostInsert / @PostUpdate不能以任何方式从表中读取。

我的src/main/resources/data-h2.sql中的触发器:

-- creating schema changes in data file because schema.sql overwrites JPA schema intialization
-- https://github.com/spring-projects/spring-boot/issues/9048
create trigger after_subtest_result_update after update on subtest_result
for each row call "com.siemens.mftool.dialects.H2SubtestResultTrigger";

处理程序:

package com.siemens.mftool.entity.handlers;

import com.siemens.mftool.entity.SubtestResult;
import com.siemens.mftool.repositories.SubtestResultRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.core.annotation.HandleAfterCreate;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.stereotype.Component;

/** workaround for H2 post insert triggers not working because of improperly
 *  handled generated keys
 *  must be called manually if the repo invocation does not come from a REST
 *  request
 */
@Component
@RepositoryEventHandler(SubtestResult.class)
public class H2WorkaroundSubtestResultHandler {

  private final SubtestResultRepository subtestResultRepository;

  @Value("${spring.datasource.platform}")
  private String platform;

  @Autowired
  public H2WorkaroundSubtestResultHandler(SubtestResultRepository subtestResultRepository) {
    this.subtestResultRepository = subtestResultRepository;
  }


  @HandleAfterCreate
  public void handleAfterCreate(final SubtestResult subtestResult) {
    if("h2".equals(platform)) {
      subtestResultRepository.h2Workaround(subtestResult);
    }
  }
}

存储库方法:

  // force an update to the newly inserted subtestResult so the
  // after-update trigger is triggered
  @Modifying
  @Query(nativeQuery = true, value =
      "update subtest_result " +
      "set id = :#{ #subtestResult.id } " +
      "where id = :#{ #subtestResult.id } ")
  void h2Workaround(SubtestResult subtestResult);

以编程方式完成呼叫后的外观:

h2WorkaroundSubtestResultHandler.handleAfterCreate(subtestResult);

仍然是一个痛点,但这至少是一个痛点,而不是整个痛点。