Interceptor before save
Concept
Any entity object will be intercepted by interceptors before being saved by save commands (whether inserted or updated).
At this point, users have an opportunity to modify the data to be saved, especially to assign values to some missing properties.
-
Database default values can only provide business-irrelevant default value rules.
-
Interceptors can provide default values based on business context-related information, such as the user's identity information in the permission system.
Users can provide default values that are closely combined with the business based on such business context information, which cannot be achieved by database-level default values.
Define Intercepted Data Format
Draft interceptors work with Save Command to adjust data before objects are saved.
If most entity tables have the four fields created_time, modified_time, created_by and modified_by, a super class can be provided as follows:
- 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?
}
All entities that need these fields can derive from this superclass.
The @OnDissociate(DissociateAction.SET_NULL)
here is to prevent deletion operations on associated User
data from being blocked due to these two foreign keys. When associated User
is deleted, these two foreign keys are automatically cleared.
Of course, user can directly intercept the entity type (decorated with @Entity), rather than an abstract type (decorated with @MappedSupperClass).
However, if an abstract type is intercepted, the save operations of all derived entity types will be intercepted, which can greatly improve the flexibility of the system, especially when the abstract type supports multiple inheritance.
Therefore, the example in this article chooses to intercept the abstract type instead of the entity type.
Define Interceptor
Assume there is a service class called UserService
whose Java method getCurrentUserId()
or Kotlin property currentUserId
returns the id of the currently logged in user.
The interceptor must implement the org.babyfish.jimmer.sql.DraftInterceptor
interface.
If using Spring management (two ways of using DraftHandler
will be introduced below), the code is:
- 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
}
}
}
}
}
The beforeSave
method is called before an object is saved, where the user can make final adjustments to the data to be saved.
If the isNew
parameter is true, it means the subsequent operation is an insert; otherwise, it is an update.
Please do not modify properties decorated with @Id
or @Key
in the beforeSave
method.
Controlling the format of the original parameter
It was mentioned above that if the current operation is UPDATE
, the original
parameter of the beforeSave
method is non-null, representing the old value in the database.
original
is a Jimmer dynamic object. By default, only the id
and key
properties are loaded and accessible. However, can we control the format of the original
object to allow more properties to be accessed?
The DraftInterceptor
interface provides another default method dependencies()
which returns a collection of properties to indicate that in addition to the id property and key property, which other properties of the original
object need to be loaded.
- 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
)
}
The returned property collection does not need to contain the id
property and key
property, because they are always loaded.
Apply interceptor
Using Jimmer Spring Starter
In the above, the class BaseEntityDraftInterceptor
is decorated with @Component
, obviously a Spring-managed object.
If using Spring Boot Starter and ensuring the interceptor is Spring-managed, then Jimmer will register it automatically without additional configuration.
Otherwise, it must be manually registered.
Not Using Jimmer Spring Starter
If jimmer spring starter is not used, attaching the interceptor to the SqlClient object makes it take effect:
- Java
- Kotlin
@Bean
public JSqlClient sqlClient(
List<DraftInterceptor<?>> interceptors,
...other params omitted...
) {
return JSqlClient
.newBuilder()
.addDraftInterceptors(interceptors)
...other config omitted...
.build();
}
@Bean
fun sqlClient(
interceptors: List<DraftInterceptor<?>>,
...other params omitted...
): KSqlClient =
newKSqlClient {
addDraftInterceptors(interceptors)
...other config omitted...
}
Although only one DraftInterceptor
is demoed in this article, there may be many in an actual project.
So here a collection is used for Spring to inject all DraftInterceptor
instances.
Final Usage
Assume Book
inherits from BaseEntity
, then it can be used like:
- 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)
-
If the above save command finally results in an insert operation, the generated SQL is:
insert into BOOK(
/* highlight-start */
CREATED_TIME,
MODIFIED_TIME,
CREATED_BY,
MODIFIED_BY,
/* highlight-end */
NAME,
EDITION,
PRICE,
STORE_ID
) values(
/* highlight-next-line */
?, ?, ?, ?,
?, ?, ?, ?
)Where the assignment behavior for
CREATED_TIME
,MODIFIED_TIME
,CREATED_BY
andMODIFIED_BY
is automatically added by the interceptor. -
If the above save command finally results in an update operation, the generated SQL is:
update BOOK set
/* highlight-start */
MODIFIED_TIME = ?,
MODIFIED_TIME,
/* highlight-end */
PRICE = ?,
STORE_ID = ?
where ID = ?Where the assignment behavior for
MODIFIED_TIME
andMODIFIED_BY
is automatically added by the interceptor.