前文《Ebean ORM框架介绍-1.增强注解》介绍了一些特性注解,本文继续介绍一些注解的高级功能

一、@Encrypted字段加密

使用@Encrypted注解简单实现对数据库字段进行加密解密,以达到保护重要数据的作用,如下phone字段

rider emmylua 注解 @encrypted注解_java

1. 使用数据库加密

public class User extends BaseModel {

    @DbComment("the name")
    private String name;

    @Encrypted
    private String phone;

    private Integer loginCount = 0;
}

只需要设置@Encrypted就可以使用对指定字段进行加密解密,此设置对应用程序是完全透明的

2. 使用应用程序加密

public class User extends BaseModel {

    @DbComment("the name")
    private String name;

    @Encrypted
    private String phone;

    private Integer loginCount = 0;

    @Encrypted(dbEncryption=false)
    String description;
}
(1) 注解设置

@Encrypted(dbEncryption=false)

(2) 自定义加密程序
public class BasicEncryptKeyManager implements EncryptKeyManager {

    @Override
    public EncryptKey getEncryptKey(String tableName, String columnName) {
        return new BasicEncryptKey(tableName, columnName);
    }
}

public class BasicEncryptKey implements EncryptKey {

    private String tableName;
    private String columnName;
    private String key = "0123456";

    public BasicEncryptKey(String tableName, String columnName){
        this.tableName = tableName;
        this.columnName = columnName;
    }

    @Override
    public String getStringValue() {
        return tableName.concat(columnName).concat(key);
    }
}
(3) 配置注解程序
ebean:
  encryptKeyManager: fun.barryhome.ebean.encrypt.BasicEncryptKeyManager

application.yaml中设置

3. 性能分析

10:29:06.167 [main] DEBUG io.ebean.SQL - txn[1001] select t0.id, t0.name, CONVERT(AES_DECRYPT(t0.phone,?) USING UTF8) _e_t0_phone, t0.login_count, t0.description, t0.version, t0.when_created, t0.when_modified from user t0 where CONVERT(AES_DECRYPT(t0.phone,?) USING UTF8) = ? and t0.description = ?; --bind(****,130000000000,****)

从日志中的SQL执行语句可以看出,使用了加密的字段在SQL上会做函数转换,如果做为查询条件的话可能会有较大的性能开销,故谨慎使用

二、 @ChangeLog 更新日志

使用@ChangeLog注解将数据更新日志记录到日志中,主要用于日志查询

1. 设置注解

@ChangeLog
public class User extends BaseModel {
  ...
}

2. 日志配置

这里必须是logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">

    <appender name="CHANGE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/changeLog.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>log/changeLog.log.%d{yyyy-MM-dd}</FileNamePattern>
            <MaxHistory>90</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{HH:mm:ss.SSS} %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{80}) - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="io.ebean.ChangeLog" level="INFO" additivity="false">
        <appender-ref ref="CHANGE_LOG"/>
        <appender-ref ref="STDOUT"/>
    </logger>

    <!--默认使用console和file-->
    <root level="INFO" >
        <appender-ref ref="CHANGE_LOG" />
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

3. 日志结果

15:01:07.975 [ebean-db1] INFO  io.ebean.ChangeLog - {"ts":1622962867966,"change":"U","type":"User","id":"1","data":{"version":24,"whenModified":"2021-06-06T07:01:07.936Z","description":"Sun Jun 06 15:01:07 CST 2021"},"oldData":{"version":23,"whenModified":"2021-06-06T06:02:49.253Z","description":"Sun Jun 06 14:02:49 CST 2021"}}

三、@History 历史记录

使用@history注解可以实现在数据库表中记录所有数据的update和del操作的前后快照

1. 设置

@History
@Table(name = "customer")
public class Customer extends BaseModel {

    public static final CustomerFinder find = new CustomerFinder();

    private String name;

    @HistoryExclude
    private Integer age;

}

Entity上设置**@History**,不需要记录的字段可设置**@HistoryExclude**

2. 数据结构变化

rider emmylua 注解 @encrypted注解_加密解密_02

设置注解后数据库结构会发生一些变化

  • customer表会增加"sys_period_start"和"sys_period_end"两个字段,以及"customer_history_del"和"customer_history_upd"两个触发器
  • 增加"customer_history"历史数据表
  • 增加"customer_with_history"视图

3. 数据变化

1)新增记录变化
@Test
public void create() {
  Customer customer = Customer.builder()
    .name("abc")
    .age(0)
    .build();
  customer.save();
}

rider emmylua 注解 @encrypted注解_rider emmylua 注解_03

源表中sys_period_start字段更新为最后更新时间,由于时区原因,比当前时间小8小时

2)修改记录变化
@Test
public void update() {
  Customer customer = DB.find(Customer.class, 1L);
  customer.setName(UUID.randomUUID().toString());
  customer.setAge(customer.getAge() + 1);
  customer.update();
}

rider emmylua 注解 @encrypted注解_java_04

源表中原字段已更新, sys_period_start字段更新为最后更新时间

rider emmylua 注解 @encrypted注解_spring boot_05

历史表增加一行数据,记录了原始数据

  • sys_period_start为新增时的时间
  • sys_period_end为更新后的时间
  • 两个时间形成本条记录的保持时间

rider emmylua 注解 @encrypted注解_jpa_06

多次更新后会发现规律,后一条记录的start时间就是前一条记录的end时间,也就是当前记录数据的保持时间

4. 历史记录查询

@Test
public void query() {
  Timestamp date = Timestamp.valueOf("2021-06-07 04:20:00");
  Customer customer = Customer.find.query().asOf(date).findOne();
  System.err.println(customer);
}
13:58:44.485 [main] DEBUG io.ebean.SQL - txn[1001] select t0.id, t0.name, t0.age, t0.version, t0.when_created, t0.when_modified from customer_with_history t0 where (t0.sys_period_start <= ? and (t0.sys_period_end is null or t0.sys_period_end > ?)); --bind(asOf 2021-06-07 06:20:00.0, )
  • date可为历史任意一个时间,根据前面的时间规律,可以查询到唯一一条记录,如果前于新增时间则回返为空,如果晚于最后更新时间则返回为最新的记录
  • customer_with_history为自动创建的视图

5. 历史数据版本比较

@Test
public void queryList() {
    List<Version<Customer>> customerVersions = Customer.find.query()
            .where().idEq(1L)
            .findVersions();

    for (Version<Customer> customerVersion : customerVersions) {
        Customer bean = customerVersion.getBean();
        Map<String, ValuePair> diff = customerVersion.getDiff();
        Timestamp effectiveStart = customerVersion.getStart();
        Timestamp effectiveEnd = customerVersion.getEnd();

        System.err.println(diff);
    }
}
{name=a6d6ca0e-c266-4efd-95c5-9a47e94706d2,711d365c-9a40-44ad-be6a-535879c81c12, age=4,null, version=5,4, whenModified=2021-06-07T05:49:06.570Z,2021-06-07T04:29:48.570Z}
{name=711d365c-9a40-44ad-be6a-535879c81c12,3b1da201-e600-4d94-ba86-89679edfde4e, version=4,3, whenModified=2021-06-07T04:29:48.570Z,2021-06-07T04:29:23.179Z}
{name=3b1da201-e600-4d94-ba86-89679edfde4e,520874da-816c-482f-a3b9-d94be47477ba, version=3,2, whenModified=2021-06-07T04:29:23.179Z,2021-06-07T04:19:45.068Z}
{name=520874da-816c-482f-a3b9-d94be47477ba,abc, version=2,1, whenModified=2021-06-07T04:19:45.068Z,2021-06-07T04:18:58.203Z}

每次输出都可以看出前后两个值的变化情况

6. 注意事项

  • 事务性,可以保证更新内容是一致性的
  • History是通过数据库的触发器实现的,故直接修改数据库也可以产生历史数据
  • 相对于@ChangeLog来讲,@History给数据库带来了额外的存储成本和性能开销
  • 对于表结构的修改,原有的触发器和history表不会自动更新,结构同步将带来一些麻烦,可使用ebean提供的db迁移来解决这一问题

四、综述

Ebean还有很多JPA没有的高级功能,如草稿、复合查询、多数据支持、多租户等等功能,后续期待更新。

文中代码由于篇幅原因有一定省略并不是完整逻辑,如有兴趣请Fork源代码 https://gitee.com/hypier/barry-ebean/tree/master/ebean-section-2