保存前拦截器
基本概念
任何实体对象在被保存指令保存 (无论插入还是更新) 前,都会被拦截器拦截。
在此,用户有一次修改被保存数据的机会 ,尤其是为某些缺失的属性赋值。
如果使用拦截器为缺失的属性赋值*(这也是推荐用法)*,就和数据库级别的默认值有点类似,但是存在如下差异
-
数据库默认值只能提供业务无关的默认值规则。
-
拦截器可以根据业务上下文相关信息提供默认值,比如,当前用户在权限系统中的身份信息。
用户可以根据这类业务上下问信息提供和业务紧密结合的默认值,这是数据库级别默认值无法实现的。
定义被拦截数据格式
Draft拦截器和Save指令配合使用,在对象被保存之前调整数据。
假如大部分实体表都具备created_time、modified_time、created_by和modified_by四个字段,可以提供如下超类
- Java
- Kotlin
@MappedSuperclass
public interface BaseEntity {
LocalDateTime createdTime();
LocalDateTime modifiedTime();
@Nullable
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
User creator();
@Nullable
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
User editor();
}
@MappedSuperclass
interface BaseEntity {
val createdTime: LocalDateTime
val modifiedTime: LocalDateTime
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
val createdBy: User?
@ManyToOne
@OnDissociate(DissociateAction.SET_NULL)
val modifiedBy: User?
}
所有需要这些字段的实体都从此超类派生即可。
这里的@OnDissociate(DissociateAction.SET_NULL)
是为了防止因这两个外键导致相关User
数据的删除操作被阻止。当相关User
被删除后,这两个外键自动清空。
当然,用户可以直接拦截实体类型 (被@Entity修饰),而非抽象类型 (被@MappedSupperClass) 修饰。
然而,如果选择拦截抽象类型,那么所有派生实体类型的保存操作都将会被拦截,这可以极大地提高系统的灵活性,尤其是抽象类型支持多继承时。
所以,本文的例子选择拦截抽象类型,而非实体类型。
定义拦截器
假设有一个叫做UserService
的服务类,其java方法getCurrentUserId()
或kotlin属性currentUserId
返回当前登录用户的id。
拦截器需要实现org.babyfish.jimmer.sql.DraftInterceptor
接口。
如果使用Spring托管 (下文会介绍两种使用拦截器的方式),请用@Component
修饰拦截器实现类,代码代码如下:
- Java
- Kotlin
@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());
});
}
}
}
}
@Component
class BaseEntityDraftInterceptor(
private val userService: UserService
) : DraftInterceptor<BaseEntity, BaseEntityDraft> {
override fun beforeSave(draft: BaseEntityDraft, original: BaseEntity?) {
if (!isLoaded(draft, BaseEntity::modifiedTime)) {
draft.modifiedTime = LocalDateTime.now()
}
if (!isLoaded(draft, BaseEntity::modifiedBy)) {
draft.modifiedBy {
id = userService.currentUserId
}
}
if (original === null) {
if (!isLoaded(draft, BaseEntity::createdTime)) {
draft.createdTime = LocalDateTime.now()
}
if (!isLoaded(draft, BaseEntity::createdBy)) {
draft.createdBy {
id = userService.currentUserId
}
}
}
}
}
其中,beforeSave
方法在某个对象被保存之前被调用,用户可以对即将保存的数据draft
做出最后调整。该方法有两个参数
-
draft
: 即将被保存的对象,你可以修改它 -
original
: 如果非null,则表示数据库中现有的数据,只可读取,不可修改-
对于INSERT操作而言,
original
为null -
对于UPDATE操作而言,
original
非null
所以,可以通过
original
是否为null判断当前操作是INSERT还是UPDATE。original
对象是一个Jimmer动态对象,其哪些些属性就绪可以访问而哪些缺失不可访问,受到另外一 个方法dependencies
的控制。 -
请不要在beforeSave
方法中,修改被@Id
或@Key
修饰的属性。
控制original参数的格式
上文谈到,如果当前操作为UPDATE
,beforeSave
方法的original
参数非null,表示数据库中的旧值。
original
是Jimmer动态对象,默认情况下,只有id
和key
属性是已加载和可访问的。然而,是否能够控制original
对象的格式让跟多的属性可以被访问呢?
DraftInterceptor
接口提供了另外一个default方法dependencies
,返回一个属性集合,以表示除了id属性和key属性外,original
对象还有那些属性需要被加载。
- Java
- Kotlin
@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
);
}
}
@Component
class BaseEntityDraftInterceptor(
private val userService: UserService
) : DraftInterceptor<BaseEntity, BaseEntityDraft> {
override fun beforeSave(
draft: BaseEntityDraft,
// The format of `original` is controlled by `dependencies()`
original: BaseEntity?
) {
...implementation is omitted...
}
override fun dependencies(): Collection<TypedProp<BaseEntity, *>> =
listOf(
BaseEntityProps.CREATED_BY,
BaseEntityProps.MODIFIED_BY
)
}
返回的属性集合无需包含id
属性和key
属性,因为它们总是被加载。
应用拦截器
使用Jimmer Spring Starter
上文中,我们定义的类BaseEntityDraftInterceptor
被@Component
修饰,很明显这是一个Spring托管对象。
如果使用SpringBoot Starter且保证拦截器被Spring托管,那么Jimmer就会将注册它,无需额外的配置。
否则,必需手动注册
不使用Jimmer Spring Starter
未使用SpringBoot时,将拦截器挂接到SqlClient对象上,即可生效
- Java
- Kotlin
@Bean
public JSqlClient sqlClient(
List<DraftInterceptor<?>> interceptors,
...省略其他参数...
) {
return JSqlClient
.newBuilder()
.addDraftInterceptors(interceptors)
...省略其他配置...
.build();
}
@Bean
fun sqlClient(
interceptors: List<DraftInterceptor<?>>,
...省略其他参数...
): KSqlClient =
newKSqlClient {
addDraftInterceptors(interceptors)
...省略其他配置...
}
虽然在本文仅示范了一个DraftInterceptor
,实际项目中可能有很多个。
所以,这里使用集合,让Spring注入所有的DraftInterceptor
。
最终使用
假如Book
继承了BaseEntity
,则可以这么使用
- Java
- Kotlin
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);
val book = Book {
name = "SQL in Action"
edition = 1
price = BigDecimal("59")
store().id = 2
}
sqlClient.entities.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_TIME
、MODIFIED_TIME
、CREATED_BY
和MODIFIED_BY
赋值的行为由拦截器自动添加 -
如果上面的保存指令最终导致了update操作,生成的SQL如下
update BOOK set
/* highlight-start */
MODIFIED_TIME = ?,
MODIFIED_TIME,
/* highlight-end */
PRICE = ?,
STORE_ID = ?
where ID = ?
其中,为MODIFIED_TIME
和MODIFIED_BY
赋值的行为由拦截器自动添加