Background
在一些数据敏感的项目, 特别是配置表, 我们需要记录每一次对表的甚至每个值的改动, 并把改动的数据存放到另一张表中。
以前我们可能会用数据库trigger来实现, 但trigger实现方式也有如下限制
- 需要用sql编写trigger, 一但表结构更改, 需要找人修改trigger
- 手动创建Audit 表
- 对删除(hard delete)难以detect是谁删除的。
当今世界, 更加偏向用java框架去实现,例如JPA Audit 和 Hiberate Envers
JPA Audit
如果对Audit 的要求不高, 只需要在表中记录是谁创建的, 谁最后修改,创建时间和最后修改时间。
这种情况下我们使用JPA Audit就足够了。
下面来看是怎么实现的, 我们会用user service 作为例子.
step 1, 对Entity类添加注解 @EntityListeners(AuditingEntityListener.class)
@Entity // to be pojo class of Hibernate
@Data
@Table(name = "tb_user")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User {
step 2, 对Entity类添加 下面4个关于audit的属性
@CreatedDate
@JsonIgnore
private Date createDate;
@LastModifiedDate
@JsonIgnore
private Date lastModifiedDate;
@CreatedBy
@JsonIgnore
private String createdBy;
@LastModifiedBy
@JsonIgnore
private String lastModifiedBy;
step 3, 添加1个bean,实现AuditorAware接口告诉JPA当前的用户是谁
正常呢来讲, username should be get from Sprint security API, 为了测试方便这边我就hardcode了
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component("myAuditorAware")
public class MyAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("JasonPoon");
}
}
step 4, 在启动类添加注解 @EnableJpaAuditing(auditorAwareRef = “myAuditorAware”)
@EnableJpaRepositories
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "myAuditorAware")
@ComponentScan({"cn.home.user.config","cn.home.user.web","cn.home.user.service","cn.home.user.pojo","cn.home.user.dao"})
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
step 5, 接下来我们就可以测试了
@Slf4j
@DataJpaTest //default will auto rollback
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserDaoTest {
@Autowired
private UserDao userDao; //springboot will create an instance of the interface while starting
@Test
@DisplayName("test add User")
@Rollback(value = false)
public void testAddUser(){
User user = new User();
user.setUsername("Alice13");
user.setAddress("The 5th School");
userDao.save(user);
log.info("User id: {}",user.getId());
assertTrue(0 < user.getId(),"id should be generated");
userDao.delete(user);
}
@Test
@DisplayName("test add User")
@Rollback(value = false)
public void updateUser(){
User user = userDao.findByUsername("Alice12");
log.info("User id: {}",user.getId());
user.setAddress("The 5th School3");
userDao.save(user);
assertTrue( "The 5th School3".equals(user.getAddress()),"address should be updated");
}
@Test
@DisplayName("find one User")
@Rollback(value = false)
public void findUserTest1(){
User user = userDao.findByUsername("Alice12");
log.info(String.valueOf(user));
assertNotNull(user);
}
}
如果enable了 spring.jpa.hibernate.ddl-auto=true, 启动时, 会自动为tb_user表添加create_date等4个列。
测试pass时检查下record
可以看见audit 信息已经被加入!
Hibernate Envers
如果对某张表的要求比较高, 要记录所有的change History在另1 张table, 这时我们需要用到Hibernate Envers
下面也是对user service 的 user table作一个例子.
首先我们Enver需要一张公共表 revinfo
默认情况下,它包含两列
REV- 主键列 - 修订版ID/修订版号
REVTSTMP- 修订版创建的时间戳。
而且Envers 会建立hard 外键 在其他Audit表上, 所以db 账号必须具有创建外键 REFERENCES 的权限。
Mysql 中, grant语句是:
grant select,insert,update,delete,create,alter,drop,REFERENCES on demo_cloud_user.* to cloud_user;
step 1, 添加hibernate-envers 依赖
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
step 2, 在springboot 配置文件中, 添加关于envers的属性
spring:
datasource:
url: jdbc:mysql://43.138.222.61:3306/demo_cloud_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: cloud_user
password: '{cipher}323e2265acd321eaec76a88bfa710f5f3673c58f8e6e1bbe2944f08b9518ac0c'
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maxLifeTime: 30000
jpa:
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
org:
hibernate:
envers:
audit_strategy: org.hibernate.envers.strategy.internal.DefaultAuditStrategy
audit_strategy_validity_store_revend_timestamp: true
其中audit_strategy, org.hibernate.envers.strategy.internal.DefaultAuditStrategy 的意思为
hibernate提供了两种审计策略,分别是
org.hibernate.envers.strategy.internal.DefaultAuditStrategy
org.hibernate.envers.strategy.internal.ValidityAuditStrategy
如果使用DefaultAuditStrategy,USER_AUD表中不会有REVEND,REVEND_TSTMP两个字段,只会单纯的记录变更与版本
而使用ValidityAuditStrategy,在新增一条变更记录时,会更新上一条变更记录的REVEND,REVEND_TSTMP为当前的版本号以及变更时间
因为ValidityAuditStrategy除了插入新纪录还要更新旧的记录,所以插入速度会慢一点,但是因为提供了额外的信息,对于数据查询,速度则较DefaultAuditStrategy更快一些
具体的Envers 设置, 请参考
https://docs.jboss.org/hibernate/orm/5.1/userguide/html_single/Hibernate_User_Guide.html#envers
step 3, 在springboot 配置文件中, 添加关于envers的属性
在Entity类上加上@Audited 注解
@Entity // to be pojo class of Hibernate
@Data
@Table(name = "tb_user")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Audited
public class User {
接下来就可以测试了
执行上面的都测试用例, 可以见到1个新的AUD表被创建