我try 仅在表具有审计字段时才 for each 插入或更新查询设置审计字段.经过大量的试验和错误,我得出了这个逻辑,使用VisitListener:


public class AuditVisitListener implements VisitListener {

    private static final Field<Timestamp> createdAt = field("created_at", Timestamp.class);
    private static final Field<String> createdBy = field("created_by", String.class);
    private static final Field<Timestamp> updatedAt = field("updated_at", Timestamp.class);
    private static final Field<String> updatedBy = field("updated_by", String.class);
    private Set<Field<?>> auditFields = new HashSet<>();

    private UserContextHolder userContextHolder;

    public AuditVisitListener(UserContextHolder userContextHolder) {
        this.userContextHolder = userContextHolder;
    }

    @Override
    public void visitStart(VisitContext context) {
        QueryPart topLevelQuery = context.context().topLevel();
        if(topLevelQuery instanceof SelectQuery<?>) {
            // No need to process further since we don't set audit fields for select queries
            return;
        }
        collectAuditFields(context.queryParts());
        if(topLevelQuery instanceof InsertQuery<?> insertQuery) {
            addAuditFieldsForInsert(insertQuery);
        }
        if(topLevelQuery instanceof UpdateQuery<?> updateQuery) {
            addAuditFieldsForUpdate(updateQuery);
        }
    }

    @Override
    public void visitEnd(VisitContext context) {
        // Without this line, audit fields are added
        // even if there is no audit fields in a table. 
        auditFields.clear();
    }

    private void collectAuditFields(QueryPart[] queryParts) {
        for(QueryPart queryPart : queryParts) {
           if(queryPart instanceof Table<?> table) {
               if(table.field(createdAt) != null){
                   auditFields.add(createdAt);
               }
               if(table.field(createdBy) != null) {
                  auditFields.add(createdBy);
               }
               if(table.field(updatedAt) != null) {
                  auditFields.add(updatedAt);
               }
               if(table.field(updatedBy) != null) {
                   auditFields.add(updatedBy);
               }
           }
        }
    }

    private void addAuditFieldsForInsert(StoreQuery<?> storeQuery) {
        if(auditFields.contains(createdAt)) {
            storeQuery.addValue(createdAt, now());
        }
        if(auditFields.contains(createdBy)) {
            storeQuery.addValue(createdBy, userId());
        }
        addAuditFieldsForUpdate(storeQuery);
    }

    private void addAuditFieldsForUpdate(StoreQuery<?> storeQuery) {
        if(auditFields.contains(updatedAt)) {
            storeQuery.addValue(updatedAt, now());
        }
        if(auditFields.contains(updatedBy)) {
            storeQuery.addValue(updatedBy, userId());
        }
    }

    private String userId() {
        return userContextHolder.getUserId()
                .orElseThrow(() -> new BusinessException(ErrorCode.EMPTY_USER_CONTEXT));
    }

}

第一个问题是没有auditFields.clear(),即使表中没有审计字段,也会添加审计字段.为什么会发生这种事呢? 第二个问题是,它适用于单行插入,但当INSERT语句有多行时,它只将审计字段设置为最后一行.例如,INSERT语句:

    ctx.insertInto(BRAND, BRAND.NAME_EN, BRAND.NAME_KR, BRAND.STATUS)
            .values("Heinz", "하인즈", (short)1)
            .values("Hunts", "헌츠", (short)1)
            .execute();

生成以下查询:

insert into `pms`.`brand` (`name_en`, `name_kr`, `status`, created_at, created_by, updated_at, updated_by) 
values ('Heinz', '하인즈', 1, null, null, null, null),
       ('Hunts', '헌츠', 1, current_timestamp(), 'test@test.com', current_timestamp(), 'test@test.com')

但是,如果我插入多行,则Batch Insert再次起作用:

    BrandRecord heinz = ctx.newRecord(BRAND);
    heinz.setNameEn("Heinz");
    heinz.setNameKr("하인즈");
    heinz.setStatus((short)1);

    BrandRecord hunts = ctx.newRecord(BRAND);
    hunts.setNameEn("Hunts");
    hunts.setNameKr("헌츠");
    hunts.setStatus((short)1);

    ctx.batchInsert(heinz, hunts).execute();

如何为多行INSERT或UPDATE语句设置审计字段?

作为参考,AuditVisitListener使用VisitListenerProvider进行配置:

@Configuration
public class JooqConfig {

    @Bean
    public DefaultConfigurationCustomizer customizer(UserContextHolder userContextHolder) {
        return configuration -> {
            configuration.setVisitListenerProvider(() -> new AuditVisitListener(userContextHolder));
        };
    }

}

jOOQ: 3.18
Java: 17
Spring Boot: 3.0.5

推荐答案

Why is auditFields.clear() necessary

这指向了您的设计中的一个更大的问题.VisitListener的生命周期可能不是针对每个查询呈现,而是针对Configuration,这意味着您的各种查询共享相同的实例.这意味着clear()似乎"修复"了这个问题,但实际上隐藏了一个更大的并发问题.

或者,您应该使用VisitListenerProvider来每次实例化新的VisitListener实例,或者将您的上下文放在VisitContext中,而VisitContext的生命周期是单次呈现遍历.

为什么它只在最后一行起作用

(非常有历史意义的)InsertQueryUpdateQuery API只允许将数据追加到最后一行.要添加新行,调用InsertQuery.newRecord().从那时起,InsertQuery中没有公共API可以将值追加到前面的行.

对于批处理查询则不是这样,批处理查询是多个查询,而不是每个查询多行.

较新的版本(截至jOOQ3.18仍处于实验阶段)model API将允许您访问VALUES子句中的所有行.它还允许编写不需要VisitListener SPI的简单得多的SQL转换逻辑.

关于备选方案的注解

请注意,jOOQ具有:

如果只使用它,或者使用触发器填充这些列值,就会简单much倍.

Java相关问答推荐

从头开始使用Jgit初始化InMemoryRepository

是否可以从@ TrustMapping中删除特定方法的基路径?

如何从片段请求数据到活动?在主要活动中单击按钮请求数据?

使用Java Streams API比较两个不同的Java集合对象和一个公共属性

如何以干净的方式访问深度嵌套的对象S属性?

Java中如何根据Font.canDisplay方法对字符串进行分段

Spring Boot@Cachebale批注未按预期工作

使用传递的参数构造异常的Mockito-doThrow(或thenThrow)

Mapstruct不能正确/完全映射属性

试着做一个2x2的魔方求解算法,我如何找到解路径(DFS)?

GSON期间的Java类型擦除

基于接口的投影、原生查询和枚举

如何只修改父类ChroniclerView位置0处的第一个嵌套ChroniclerView(child)元素?

Spring Framework6.1中引入的新RestClient是否有适合于测试的变体,就像RestTemplate和TestRestTemplate一样?

Java在操作多个属性和锁定锁对象时使用同步和易失性

对角线填充二维数组

为什么相同的数据条码在视觉上看起来不同?

如何在Maven Central上部署?

无法在Java中获取ElastiCache的AWS CloudWatch指标

如果c不为null,Arrays.sort(T[]a,Comparator<;?super T>;c)是否会引发ClassCastException?