Skip to main content

Jpa添加Audit

huhxOriginalAbout 3 minspringJavaSpring

Spring中添加Audit

Spring中内置了一些跟Audit相关的注解,

  • @CreatedBy: 表的这条记录是谁创建的,可以是人也可以是系统
  • @CreatedDate: 记录的创建时间
  • @LastModifiedBy: 表记录被谁更新
  • @LastModifiedDate: 表记录的更新时间,默认情况下创建的记录,该字段也会插入值

下面我们具体看下它们的SpringBoot中的使用,首先数据库和jpa的依赖是首要条件。这里不多赘述

定义Audit的基类

@Setter
@Getter
@MappedSuperclass
@NoArgsConstructor
@SuperBuilder
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    // -1表示未删除,0表示物理删除, 1708874319这种表示逻辑删除
    public static final String SOFT_DELETED_CLAUSE = "deleted_at = -1";

    @Id
    private Long id;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @Version
    private Integer version;

    private Long deletedAt = -1L;
}
  • @MappedSuperclass: 表示这个类是非持久类,并且hibernate将不会创建/查找此类的表映射
  • @EntityListeners: 定义Jpa事件的回调,比如当实体上发生指定事件(例如实体创建、更新或删除)时,将自动调用这些方法
  • AuditingEntityListener: Jpa内置的类,提供了对@CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate这些注解的支持
  • @Version: 会在第一次插件的时候设置为0,后续的每次更新会叠加。主要是辅助实现乐观锁

添加deletedAt字段,主要是标记记录的删除状态。未删除、物理删除、逻辑删除

定义继承BaseEntity的Entity类

@Entity
@Getter
@NoArgsConstructor
@SuperBuilder
@Table(name = "baby")
@SQLDelete(sql = "UPDATE baby SET deleted_at = extract(epoch from now()), "
    + "version = version + 1, updated_at = current_timestamp WHERE id = ? AND version = ?",
    check = ResultCheckStyle.COUNT)
@Where(clause = BaseEntity.SOFT_DELETED_CLAUSE)
public class Baby extends BaseEntity {

    private String idCard;
}
  • @SQLDelete: 我们复写delete操作的逻辑,所以当jpa里面的例如deleteById或者我们自定义的deleteByIdCard的方法时,我们复写的sql语句被执行,而不是默认的delete语句
  • @Where: 当我们在查询当前表的时候,会添加指定的过滤条件。上述就是在查询的时候,会额外添加deleted_at = -1的过滤条件

实现AuditorAware类

上述我们添加了@CreatedDate@LastModifiedBy注解表明这条记录是谁在操作。Jpa是从哪里得到当前操作人的信息呢?那就是AuditorAware了,我们通过实现AuditorAware类,比如实现中从token里面解析得到当前用户的信息username。下面只是简单的实现以了解原理就行

public class AuditAwareImpl implements AuditorAware<String> {

    @Override
    public @Nonnull Optional<String> getCurrentAuditor() {
        return Optional.of("huhx");
    }
}

开启Spirng对Audit的开关

@Configuration
@EnableJpaAuditing
public class JpaConfig {

    @Bean
    public AuditAwareImpl auditorProvider() {
        return new AuditAwareImpl();
    }
}

原理

@CreatedBy和@LastModifiedBy的原理

@CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate这些注解生效的原因在于Spring内置的实现AuditingEntityListener
AuditingEntityListener里面定义了Entity在实际创建和更新前的回调函数。

	@PrePersist
	public void touchForCreate(Object target) {
		if (handler != null) {
			AuditingHandler object = handler.getObject();
			if (object != null) {
				object.markCreated(target);
			}
		}
	}

	@PreUpdate
	public void touchForUpdate(Object target) {
		if (handler != null) {
			AuditingHandler object = handler.getObject();
			if (object != null) {
				object.markModified(target);
			}
		}
	}

我们以创建为例,来深入看下touchForCreate内部的源码。我们直接跟进markCreated方法。

public <T> T markCreated(T source) {
    return markCreated(getAuditor(), source);
}

// 这个方法就是得到审计数据中的CreateBy或者是LastModifiedBy,返回的值是:uditorAware实现类的getCurrentAuditor方法
Auditor<?> getAuditor() {
    return auditorAware.map(AuditorAware::getCurrentAuditor).map(Auditor::ofOptional)
            .orElse(Auditor.none());
}    

<T> T markCreated(Auditor<?> auditor, T source) {
    return touch(auditor, source, true);
}

上述得到audit的值,接着作为参数进入到了touch函数。

private <T> T touch(Auditor<?> auditor, T target, boolean isNew) {
    Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
    return wrapper.map(it -> {
        touchAuditor(auditor, it, isNew);
        Optional<TemporalAccessor> now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty();
        ....

        return it.getBean();
    }).orElse(target);
}



 
 





在第四行,默认情况下setCreatedBysetLastModifiedBy就是上述audit的值。

private void touchAuditor(Auditor<?> auditor, AuditableBeanWrapper<?> wrapper, boolean isNew) {
    if(!auditor.isPresent()) return;
    
    if (isNew) {
        wrapper.setCreatedBy(auditor.getValue());
    }

    if (!isNew || modifyOnCreation) {
        wrapper.setLastModifiedBy(auditor.getValue());
    }
}

接下来,我们看下第五行touchDate的代码,关于审计时间的设置。

private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
    Optional<TemporalAccessor> now = dateTimeProvider.getNow();
    now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
    now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);

    return now;
}

@Version的原理

总结

参考