跳到主要内容

保存前拦截器

基本概念

任何实体对象在被保存指令保存 (无论插入还是更新) 前,都会被拦截器拦截。

在此,用户有一次修改被保存数据的机会,尤其是为某些缺失的属性赋值。

提示

如果使用拦截器为缺失的属性赋值*(这也是推荐用法)*,就和数据库级别的默认值有点类似,但是存在如下差异

  • 数据库默认值只能提供业务无关的默认值规则。

  • 拦截器可以根据业务上下文相关信息提供默认值,比如,当前用户在权限系统中的身份信息。

    用户可以根据这类业务上下问信息提供和业务紧密结合的默认值,这是数据库级别默认值无法实现的。

定义被拦截数据格式

Draft拦截器和Save指令配合使用,在对象被保存之前调整数据。

假如大部分实体表都具备created_time、modified_time、created_by和modified_by四个字段,可以提供如下超类

@MappedSuperclass
public interface BaseEntity {

LocalDateTime createdTime();

LocalDateTime modifiedTime();

@Nullable
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
User creator();

@Nullable
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
User editor();
}

所有需要这些字段的实体都从此超类派生即可。

备注

这里的@OnDissociate(DissociateAction.SET_NULL)是为了防止因这两个外键导致相关User数据的删除操作被阻止。当相关User被删除后,这两个外键自动清空。

提示

当然,用户可以直接拦截实体类型 (被@Entity修饰),而非抽象类型 (被@MappedSupperClass) 修饰。

然而,如果选择拦截抽象类型,那么所有派生实体类型的保存操作都将会被拦截,这可以极大地提高系统的灵活性,尤其是抽象类型支持多继承时。

所以,本文的例子选择拦截抽象类型,而非实体类型。

定义拦截器

假设有一个叫做UserService的服务类,其java方法getCurrentUserId()或kotlin属性currentUserId返回当前登录用户的id。

拦截器需要实现org.babyfish.jimmer.sql.DraftInterceptor接口。

如果使用Spring托管 (下文会介绍两种使用拦截器的方式),请用@Component修饰拦截器实现类,代码代码如下:

@Component
public class BaseEntityDraftInterceptor
implements DraftInterceptor<BaseEntity, BaseEntityDraft> {

private final UserService userService;

public BaseEntityDraftInterceptor(UserService userService) {
this.userService = userService;
}

@Override
public void beforeSave(BaseEntityDraft draft, @Nullable BaseEntity original) {
if (!ImmutableObjects.isLoaded(draft, BaseEntityProps.MODIFIED_TIME)) {
draft.setModifiedTime(LocalDateTime.now());
}
if (!ImmutableObjects.isLoaded(draft, BaseEntityProps.EDITOR)) {
draft.applyModifiedBy(user - > {
user.setId(userService.getCurrentUserId());
});
}
if (original == null) {
if (!ImmutableObjects.isLoaded(draft, BaseEntityProps.CREATED_TIME)) {
draft.setCreatedTime(LocalDateTime.now());
}
if (!ImmutableObjects.isLoaded(draft, BaseEntityProps.CREATOR)) {
draft.applyCreatedBy(user - > {
user.setId(userService.getCurrentUserId());
});
}
}
}
}

其中,beforeSave方法在某个对象被保存之前被调用,用户可以对即将保存的数据draft做出最后调整。该方法有两个参数

  • draft: 即将被保存的对象,你可以修改它

  • original: 如果非null,则表示数据库中现有的数据,只可读取,不可修改

    • 对于INSERT操作而言,original为null

    • 对于UPDATE操作而言,original非null

    所以,可以通过original是否为null判断当前操作是INSERT还是UPDATE。

    original对象是一个Jimmer动态对象,其哪些些属性就绪可以访问而哪些缺失不可访问,受到另外一个方法dependencies的控制。

注意

请不要在beforeSave方法中,修改被@Id@Key修饰的属性。

控制original参数的格式

上文谈到,如果当前操作为UPDATEbeforeSave方法的original参数非null,表示数据库中的旧值。

original是Jimmer动态对象,默认情况下,只有idkey属性是已加载和可访问的。然而,是否能够控制original对象的格式让跟多的属性可以被访问呢?

DraftInterceptor接口提供了另外一个default方法dependencies,返回一个属性集合,以表示除了id属性和key属性外,original对象还有那些属性需要被加载。

@Component
public class BaseEntityDraftInterceptor
implements DraftInterceptor<BaseEntity, BaseEntityDraft> {

@Override
public void beforeSave(
BaseEntityDraft draft,

// The format of `original` is controlled by `dependencies()`
@Nullable BaseEntity original
) {
...implementation is omitted...
}

@Override
public Collection<TypedProp<BaseEntity, ?>> dependencies() {
return Arrays.asList(
BaseEntityProps.CREATED_BY,
BaseEntityProps.MODIFIED_BY
);
}
}
提示

返回的属性集合无需包含id属性和key属性,因为它们总是被加载。

应用拦截器

使用Jimmer Spring Starter

上文中,我们定义的类BaseEntityDraftInterceptor@Component修饰,很明显这是一个Spring托管对象。

信息

如果使用SpringBoot Starter且保证拦截器被Spring托管,那么Jimmer就会将注册它,无需额外的配置。

否则,必需手动注册

不使用Jimmer Spring Starter

未使用SpringBoot时,将拦截器挂接到SqlClient对象上,即可生效

@Bean
public JSqlClient sqlClient(
List<DraftInterceptor<?>> interceptors,
...省略其他参数...
) {
return JSqlClient
.newBuilder()
.addDraftInterceptors(interceptors)
...省略其他配置...
.build();
}
提示

虽然在本文仅示范了一个DraftInterceptor,实际项目中可能有很多个。

所以,这里使用集合,让Spring注入所有的DraftInterceptor

最终使用

假如Book继承了BaseEntity,则可以这么使用

Book book = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("59"));
draft.applyStore(store -> store.setId(2L));
});
sqlClient.getEntities().save(book);
  • 如果上面的保存指令最终导致了insert操作,生成的SQL如下

    insert into BOOK(
    /* highlight-start */
    CREATED_TIME,
    MODIFIED_TIME,
    CREATED_BY,
    MODIFIED_BY,
    /* highlight-end */
    NAME,
    EDITION,
    PRICE,
    STORE_ID
    ) values(
    /* highlight-next-line */
    ?, ?, ?, ?,
    ?, ?, ?, ?
    )

    其中,为CREATED_TIMEMODIFIED_TIMECREATED_BYMODIFIED_BY赋值的行为由拦截器自动添加

  • 如果上面的保存指令最终导致了update操作,生成的SQL如下

    update BOOK set 
    /* highlight-start */
    MODIFIED_TIME = ?,
    MODIFIED_TIME,
    /* highlight-end */
    PRICE = ?,
    STORE_ID = ?
    where ID = ?

其中,为MODIFIED_TIMEMODIFIED_BY赋值的行为由拦截器自动添加