【问题标题】:Hooking into start of JDBC transaction挂钩到 JDBC 事务的开始
【发布时间】:2018-06-15 05:07:50
【问题描述】:

我有一个连接到 Postgres 9.6 数据库的 Spring Boot webapp。
我使用 Spring 的JdbcTemplate 来执行 SQL 语句。 我数据库中的每个表都有 INSERT、CREATE 和 DELETE 语句的触发器。这些触发器将受影响的行复制到历史表中。

我希望触发器还保存进行更改的用户的应用程序用户 ID。

根据https://stackoverflow.com/a/13172964/2591231,我可以通过让应用程序在每个事务开始时将当前用户ID插入临时表并从临时表中读取触发器来实现我的目标。 在其他几个地方提到的类似方法正在执行: SET LOCAL application_name = "my_application_user",然后在触发器内读取 application_name。同样,这必须在每笔交易开始时完成。

我正在寻找一种与业务代码正交的方法(我不希望每个 DAO 都显式设置用户 ID),以挂钩到每个事务的开始以运行特定的 SQL 语句 同一事务中的任何其他语句之前。

我需要它同时适用于隐式事务(JdbcTemplate 的单个调用)和使用 Spring 的 @Transactional 注释以声明方式划分的事务。

【问题讨论】:

  • 你想过使用 AOP 吗?还是 TransactionalEventListener?
  • TransactionalEventListener 在事务开始时不通知。最早的事件(或TransactionPhase)是BEFORE_COMMIT,在我的情况下为时已晚,因为触发器已经执行。我正在考虑 AOP,即在 everyJdbcTemplate 可以写入数据库的方法上放置一个切入点。在这些切入点上运行的建议将获取当前连接,将autoComit 设置为false,运行我的特殊语句,然后仅之后继续使用joinPoint。但这看起来会很混乱,我将把它作为最后的手段。
  • 这可以在数据源级别实现。只需要装饰返回的连接。一些数据源提供了这样做的方法。否则很容易覆盖 getConnection() 方法。可以使用 Spring DelegatingDataSource 来做到这一点。
  • 你在重新发明轮子吗? javers.org
  • 也许您可以通过扩展 DataSourceTransactionManager 并覆盖正确的方法(doBegindoCommit 等)来使用自己的 PlatformTransactionManager,但我不知道如何以及是否要正确管理临时表上的事务和 CRUD 操作

标签: java spring postgresql jdbc spring-jdbc


【解决方案1】:

首先,JdbcTemplate 不提供开箱即用的事务支持(请参阅here)。因此,为了拦截所有 @Transaction 注释代码 AND 每次调用 JdbcTemplate,这可以在 DataSource 级别完成,正如 Serge Bogatyrev 之前评论的那样。

我有一个 Spring Web 项目,我在其中测试了这种方法。我定义了一个替换 DataSource @Bean,称为 MyDataSource,它扩展了 BasicDataSource,并覆盖了它的 getConnection() 方法,以便它创建临时表并在返回连接之前插入 user_id。

它适用于@Transaction 调用和纯JdbcTemplate 调用。

如果您想在每个事务的开始严格绑定此临时表更新,请执行相同的策略来定义PlatformTransactionManager @Bean。您只需要覆盖doBegin() 方法。并且不要忘记使用 @Transaction 注释所有调用 JdbcTemplate 的方法。

PS1:确保在创建临时表之前调用DROP TABLE IF EXISTS temp_table_name,以便在连接返回池时替换DISCARD ALL,如here 所述。

PS2:创建临时表的整个解决方案并不好闻。我不会自己实现它。我宁愿深呼吸,将 created_by 和 updated_by 列添加到我的所有表中。

【讨论】:

  • 我并不完全反对在我的每个表中添加 created_byupdated_by 列。我要避免的主要事情是用设置和更新这些列的样板代码乱扔我的 DAO 方法。我认为这些数据与业务逻辑正交,因此我不想将我的 DAO 与这些问题完全结合起来。
【解决方案2】:

您可以利用 Spring AOP 来设置用户。方面将调用数据库来设置应用程序用户。

在我的示例中,存储过程用于设置负责创建、修改或删除记录的应用程序用户。您可以根据自己的要求对其进行自定义。这是从 HTTP 请求中检索user 然后调用存储过程的示例方面,

@Component
@Aspect
@Slf4j
public class SetUserAspect {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public SetUserAspect(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Before("execution(* com.basaki.example.service.BookService.*(..) )")
    public void setUser(JoinPoint jp) {
        log.info("In class: " + jp.getSignature().getDeclaringTypeName() +
                " - before method: " + jp.getSignature().getName());

        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        if (request != null) {
            String user = request.getHeader("user");
            if (user != null) {
                log.info("Setting user " + user);
                SimpleJdbcCall
                        jdbcCall = new SimpleJdbcCall(jdbcTemplate)
                        .withSchemaName("example_book_schema")
                        .withFunctionName("set_user");
                SqlParameterSource
                        in =
                        new MapSqlParameterSource().addValue("audit_user",
                                user);
                jdbcCall.executeFunction(String.class, in);
            }
        }
    }
}

所有的 CRUD 操作都在 BookService 中执行,本质上是一个 DAO。

这是用于设置用户的存储过程,

CREATE OR REPLACE FUNCTION example_book_schema.set_user(
    audit_user TEXT
) RETURNS BOOLEAN STABLE LANGUAGE SQL AS $$
    SELECT set_config('book.audit_user', audit_user, true);
    SELECT TRUE;

将切入点限制为仅事务方法

您可以通过在Before 建议中添加一个附加子句,将积分削减限制为仅限BookService 中的事务方法。

@Before("execution(* com.basaki.example.service.BookService.*(..) ) " +
            "&& @annotation(annotation)")
    public void setUser(final JoinPoint jp, final Transactional annotation) {
    ...
}

【讨论】:

  • Aspect 看起来可行。然而,这假设 DAO 中的所有方法都包装在一个事务中(可能用@Transactional 注释)。我试图避免这种情况。但是,似乎没有 simple 解决方法。
  • @DoronGold 请在上面查看我的更新答案。您可以通过添加一个附加子句来修改方面,以使用 @Transactional 注释捕获方法。
【解决方案3】:

您可以使用@EntityListeners 来监听应用程序上下文中实体的变化,然后收集任何信息(实体值、身份验证用户等),然后插入到您的历史记录表中。您可以在此处遵循示例:http://www.baeldung.com/database-auditing-jpa

【讨论】:

  • 是的,但我猜他没有使用 JPA 和/或 Hibernate,而是使用纯 SQL,因为他谈到了 JdbcTemplate
  • 正如@AngeloImmediata 正确指出的那样,我不使用JPA。所以没有“实体”。
【解决方案4】:

您可以创建视图、添加用户 ID 列,并使用您的触发器来处理更新。因此,这是在 DB 端对其进行编码的另一种方式。这样你应该每次都通过它,所以不需要其他更改。

转到 Java/Spring 方面。

有点过时的风格:TransactionTemplate - 这样您就可以完全控制,但您的 dao 需要更多代码,因为需要在那里完成事务管理。

其他选项是创建代理 org.springframework.jdbc.datasource.DataSourceTransactionManager 并在 doBegin 完成您的工作,然后您的代理需要传递给事务管理器。这就是我要走的路。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-12-23
    • 2019-12-24
    • 2011-12-25
    • 2018-02-03
    • 1970-01-01
    • 2013-04-01
    • 2011-05-16
    • 1970-01-01
    相关资源
    最近更新 更多