通常给实体添加audit审计字段是一种常用的重构方法,如下:

@Embeddable
@Setter
@Getter
@ToString
public class Audit { /**
* 操作人
*/
private String operName; /**
* 操作、更新时间
*/
private LocalDateTime operDate; }
public interface Auditable {

    Audit getAudit();

    void setAudit(Audit audit);
}
/**
* 监听器 回调方法
*/
@Slf4j
@Transactional
public class AuditListener { @PrePersist
@PreUpdate
public void setCreatedOn(Auditable auditable) { Audit audit = auditable.getAudit();
if(audit == null) {
audit = new Audit();
auditable.setAudit(audit);
} audit.setOperName("hkk");
audit.setOperDate(LocalDateTime.now());
} }

实体类的定义

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "person")
@EntityListeners(value = AuditListener.class)
public class Person implements Auditable { @Embedded
@JsonUnwrapped
private Audit audit; @Id
@GeneratedValue(strategy = GenerationType.AUTO)
private BigDecimal id; private String name; }

测试代码:

@RequestMapping("/")
public List<Person> getPersons() { Optional<Person> byId = personRepository.findById(BigDecimal.ONE); if (byId.isPresent()) {
Person person = byId.get();
person.setName("hkk+" + LocalDateTime.now().toString());
personRepository.save(person); }
else {
Person person = Person.builder()
.name("hkk")
.build(); personRepository.save(person);
} List<Person> persons = personRepository.findAll(); System.out.println(persons); return persons;
}

我们主要关注更新update时生成的sql:

update person set oper_date=?, oper_name=?, name=? where id=?

可以看到默认是把表中的所有字段都进行了更新。

如果一个表中字段数很多,就会影响更新效率。

所以通常我们需要在实体上添加@DynamicInsert 和@DynamicUpdate,如下:

@DynamicInsert
@DynamicUpdate
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "person")
@EntityListeners(value = AuditListener.class)
public class Person implements Auditable { @Embedded
@JsonUnwrapped
private Audit audit; @Id
@GeneratedValue(strategy = GenerationType.AUTO)
private BigDecimal id; private String name; }

这时更新SQL如下:

update person set name=? where id=?

我们发现,我们的审计字段并没有更新,也就是说生成的JPQL并不是我们想要的。

生成JPQL语句的代码是:org.hibernate.sql.Update.toStatementString

public String toStatementString() {
StringBuilder buf = new StringBuilder( (columns.size() * 15) + tableName.length() + 10 );
if ( comment!=null ) {
buf.append( "/* " ).append( comment ).append( " */ " );
}
buf.append( "update " ).append( tableName ).append( " set " );
boolean assignmentsAppended = false;
Iterator iter = columns.entrySet().iterator();
while ( iter.hasNext() ) {
Map.Entry e = (Map.Entry) iter.next();
buf.append( e.getKey() ).append( '=' ).append( e.getValue() );
if ( iter.hasNext() ) {
buf.append( ", " );
}
assignmentsAppended = true;
}
if ( assignments != null ) {
if ( assignmentsAppended ) {
buf.append( ", " );
}
buf.append( assignments );
} boolean conditionsAppended = false;
if ( !primaryKeyColumns.isEmpty() || where != null || !whereColumns.isEmpty() || versionColumnName != null ) {
buf.append( " where " );
}
iter = primaryKeyColumns.entrySet().iterator();
while ( iter.hasNext() ) {
Map.Entry e = (Map.Entry) iter.next();
buf.append( e.getKey() ).append( '=' ).append( e.getValue() );
if ( iter.hasNext() ) {
buf.append( " and " );
}
conditionsAppended = true;
}
if ( where != null ) {
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( where );
conditionsAppended = true;
}
iter = whereColumns.entrySet().iterator();
while ( iter.hasNext() ) {
final Map.Entry e = (Map.Entry) iter.next();
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( e.getKey() ).append( e.getValue() );
conditionsAppended = true;
}
if ( versionColumnName != null ) {
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( versionColumnName ).append( "=?" );
} return buf.toString();
}
}

这里的column是我们想找的,是谁给它赋值的呢?

经常半天的调度,最终定位到这个方法:org.hibernate.event.internal.DefaultFlushEntityEventListener#onFlushEntity

/**
* Flushes a single entity's state to the database, by scheduling
* an update action, if necessary
*/
public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
final Object entity = event.getEntity();
final EntityEntry entry = event.getEntityEntry();
final EventSource session = event.getSession();
final EntityPersister persister = entry.getPersister();
final Status status = entry.getStatus();
final Type[] types = persister.getPropertyTypes(); final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); final Object[] values = getValues( entity, entry, mightBeDirty, session ); event.setPropertyValues( values ); //TODO: avoid this for non-new instances where mightBeDirty==false
boolean substitute = wrapCollections( session, persister, types, values ); if ( isUpdateNecessary( event, mightBeDirty ) ) {
substitute = scheduleUpdate( event ) || substitute;
} if ( status != Status.DELETED ) {
// now update the object .. has to be outside the main if block above (because of collections)
if ( substitute ) {
persister.setPropertyValues( entity, values );
} // Search for collections by reachability, updating their role.
// We don't want to touch collections reachable from a deleted object
if ( persister.hasCollections() ) {
new FlushVisitor( session, entity ).processEntityPropertyValues( values, types );
}
} }
isUpdateNecessary( event, mightBeDirty )用于判断是否有要更新的字段,还有一个重要的操作就是,确定了要更新字段dirtyProperties
private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
final Status status = event.getEntityEntry().getStatus();
if ( mightBeDirty || status == Status.DELETED ) {
// compare to cached state (ignoring collections unless versioned)
dirtyCheck( event );
if ( isUpdateNecessary( event ) ) {
return true;
}
else {
if ( SelfDirtinessTracker.class.isInstance( event.getEntity() ) ) {
( (SelfDirtinessTracker) event.getEntity() ).$$_hibernate_clearDirtyAttributes();
}
event.getSession()
.getFactory()
.getCustomEntityDirtinessStrategy()
.resetDirty( event.getEntity(), event.getEntityEntry().getPersister(), event.getSession() );
return false;
}
}
else {
return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
}
}

dirtyCheck:

/**
* Perform a dirty check, and attach the results to the event
*/
protected void dirtyCheck(final FlushEntityEvent event) throws HibernateException { final Object entity = event.getEntity();
final Object[] values = event.getPropertyValues();
final SessionImplementor session = event.getSession();
final EntityEntry entry = event.getEntityEntry();
final EntityPersister persister = entry.getPersister();
final Serializable id = entry.getId();
final Object[] loadedState = entry.getLoadedState(); int[] dirtyProperties = session.getInterceptor().findDirty(
entity,
id,
values,
loadedState,
persister.getPropertyNames(),
persister.getPropertyTypes()
); if ( dirtyProperties == null ) {
if ( entity instanceof SelfDirtinessTracker ) {
if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() ) {
int[] dirty = persister.resolveAttributeIndexes( ( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes() ); // HHH-12051 - filter non-updatable attributes
// TODO: add Updateability to EnhancementContext and skip dirty tracking of those attributes
int count = 0;
for ( int i : dirty ) {
if ( persister.getPropertyUpdateability()[i] ) {
dirty[count++] = i;
}
}
dirtyProperties = count == 0 ? ArrayHelper.EMPTY_INT_ARRAY : count == dirty.length ? dirty : Arrays.copyOf( dirty, count );
}
else {
dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;
}
}
else {
// see if the custom dirtiness strategy can tell us...
class DirtyCheckContextImpl implements CustomEntityDirtinessStrategy.DirtyCheckContext {
private int[] found; @Override
public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) {
found = new DirtyCheckAttributeInfoImpl( event ).visitAttributes( attributeChecker );
if ( found != null && found.length == 0 ) {
found = null;
}
}
}
DirtyCheckContextImpl context = new DirtyCheckContextImpl();
session.getFactory().getCustomEntityDirtinessStrategy().findDirty(
entity,
persister,
session,
context
);
dirtyProperties = context.found;
}
} event.setDatabaseSnapshot( null ); final boolean interceptorHandledDirtyCheck;
//The dirty check is considered possible unless proven otherwise (see below)
boolean dirtyCheckPossible = true; if ( dirtyProperties == null ) {
// Interceptor returned null, so do the dirtycheck ourself, if possible
try {
session.getEventListenerManager().dirtyCalculationStart(); interceptorHandledDirtyCheck = false;
// object loaded by update()
dirtyCheckPossible = loadedState != null;
if ( dirtyCheckPossible ) {
// dirty check against the usual snapshot of the entity
dirtyProperties = persister.findDirty( values, loadedState, entity, session );
}
else if ( entry.getStatus() == Status.DELETED && !event.getEntityEntry().isModifiableEntity() ) {
// A non-modifiable (e.g., read-only or immutable) entity needs to be have
// references to transient entities set to null before being deleted. No other
// fields should be updated.
if ( values != entry.getDeletedState() ) {
throw new IllegalStateException(
"Entity has status Status.DELETED but values != entry.getDeletedState"
);
}
// Even if loadedState == null, we can dirty-check by comparing currentState and
// entry.getDeletedState() because the only fields to be updated are those that
// refer to transient entities that are being set to null.
// - currentState contains the entity's current property values.
// - entry.getDeletedState() contains the entity's current property values with
// references to transient entities set to null.
// - dirtyProperties will only contain properties that refer to transient entities
final Object[] currentState = persister.getPropertyValues( event.getEntity() );
dirtyProperties = persister.findDirty( entry.getDeletedState(), currentState, entity, session );
dirtyCheckPossible = true;
}
else {
// dirty check against the database snapshot, if possible/necessary
final Object[] databaseSnapshot = getDatabaseSnapshot( session, persister, id );
if ( databaseSnapshot != null ) {
dirtyProperties = persister.findModified( databaseSnapshot, values, entity, session );
dirtyCheckPossible = true;
event.setDatabaseSnapshot( databaseSnapshot );
}
}
}
finally {
session.getEventListenerManager().dirtyCalculationEnd( dirtyProperties != null );
}
}
else {
// either the Interceptor, the bytecode enhancement or a custom dirtiness strategy handled the dirty checking
interceptorHandledDirtyCheck = true;
} logDirtyProperties( id, dirtyProperties, persister ); event.setDirtyProperties( dirtyProperties );
event.setDirtyCheckHandledByInterceptor( interceptorHandledDirtyCheck );
event.setDirtyCheckPossible( dirtyCheckPossible ); }

我们发现,代码执行到这里,并没有执行我们AuditListener, 它是什么时候执行的呢?
其实就是isUpdateNecessary方法的后面:substitute = scheduleUpdate( event ) || substitute;

private boolean scheduleUpdate(final FlushEntityEvent event) {
final EntityEntry entry = event.getEntityEntry();
final EventSource session = event.getSession();
final Object entity = event.getEntity();
final Status status = entry.getStatus();
final EntityPersister persister = entry.getPersister();
final Object[] values = event.getPropertyValues(); if ( LOG.isTraceEnabled() ) {
if ( status == Status.DELETED ) {
if ( !persister.isMutable() ) {
LOG.tracev(
"Updating immutable, deleted entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
else if ( !entry.isModifiableEntity() ) {
LOG.tracev(
"Updating non-modifiable, deleted entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
else {
LOG.tracev(
"Updating deleted entity: ",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
}
else {
LOG.tracev(
"Updating entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
} final boolean intercepted = !entry.isBeingReplicated() && handleInterception( event ); // increment the version number (if necessary)
final Object nextVersion = getNextVersion( event ); // if it was dirtied by a collection only
int[] dirtyProperties = event.getDirtyProperties();
if ( event.isDirtyCheckPossible() && dirtyProperties == null ) {
if ( !intercepted && !event.hasDirtyCollection() ) {
throw new AssertionFailure( "dirty, but no dirty properties" );
}
dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;
} // check nullability but do not doAfterTransactionCompletion command execute
// we'll use scheduled updates for that.
new Nullability( session ).checkNullability( values, persister, true ); // schedule the update
// note that we intentionally do _not_ pass in currentPersistentState!
session.getActionQueue().addAction(
new EntityUpdateAction(
entry.getId(),
values,
dirtyProperties,
event.hasDirtyCollection(),
( status == Status.DELETED && !entry.isModifiableEntity() ?
persister.getPropertyValues( entity ) :
entry.getLoadedState() ),
entry.getVersion(),
nextVersion,
entity,
entry.getRowId(),
persister,
session
)
); return intercepted;
}
就是这行代码,final boolean intercepted = !entry.isBeingReplicated() && handleInterception( event );

总结:也就是说框架先执行了数据的脏数据检查,然后再执行了AuditListener的审计字段赋值,在脏数据检查时,就已经确定了要更新字段,改不了了,所以更新时,就不能更新我们的审计字段了。
目前的解决方法就是,去掉@DynamicUpdate,更新所有的字段。

spring boot jpa @PreUpdate结合@DynamicUpdate使用的局限性的更多相关文章

  1. spring boot JPA中实体类常用注解

    spring boot jpa中的注解很多,参数也比较多.没必要全部记住,但是经常查看官方文档也比较麻烦,记录一下一些常用的注解.通过一些具体的例子来帮助记忆. @Entity @Table(name ...

  2. Spring boot Jpa添加对象字段使用数据库默认值

    Spring boot Jpa添加对象字段使用数据库默认值 jpa做持久层框架,项目中数据库字段有默认值和非空约束,这样在保存对象是必须保存一个完整的对象,但在开发中我们往往只是先保存部分特殊的字段其 ...

  3. spring boot jpa 使用update 报错解决办法

    在spring boot jpa 中自定义sql,执行update操作报错解决办法: 在@Query(...)上添加 @Modifying@Transactional注解

  4. Spring Boot(五):Spring Boot Jpa 的使用

    在上篇文章Spring Boot(二):Web 综合开发中简单介绍了一下 Spring Boot Jpa 的基础性使用,这篇文章将更加全面的介绍 Spring Boot Jpa 常见用法以及注意事项. ...

  5. Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题

      (转载)Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题   这几天在用spring boot开发项目, 在开发的过程中遇到一个问题hibernate在执 ...

  6. Spring Boot Jpa 的使用

    Spring Boot Jpa 介绍 首先了解 Jpa 是什么? Jpa (Java Persistence API) 是 Sun 官方提出的 Java 持久化规范.它为 Java 开发人员提供了一种 ...

  7. (转)Spring Boot(五):Spring Boot Jpa 的使用

    http://www.ityouknow.com/springboot/2016/08/20/spring-boot-jpa.html 在上篇文章Spring Boot(二):Web 综合开发中简单介 ...

  8. Spring boot JPA 用自定义主键策略 生成自定义主键ID

    最近学习Spring boot JPA 学习过程解决的一些问题写成随笔,大家一起成长.这次遇到自定义主键的问题 package javax.persistence; public enum Gener ...

  9. Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例

    Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例 一.快速上手 1,配置文件 (1)pom包配置 pom包里面添加jpa和thymeleaf的相关包引用 ...

随机推荐

  1. laravel 文件上传总结

    调用 store 方法会生成唯一的 ID 来作为文件名,如果想获取原件本来的名称可以使用 $file = $request->file('file'); $file->getClientO ...

  2. excel怎么只打印某页?excel怎么只打印某几页

    有时候我们需要打印的excel文件,内容较多有好几页,而我们只需要打印里面的部分内容,为了减少纸张.碳粉的浪费,我们怎样精准打印某页或某几页呢?   工具/原料   Excel / WPS软件 方法/ ...

  3. Json文件删除元素

    方法1:delete 注意,该方法删除之后的元素会变为null,并非真正的删除!!! 举例: 原json: { "front" : { "image" : [ ...

  4. Oracle 字段监控 ( column monitor)

    Oracle 字段监控 ( column monitor) */--> Oracle 字段监控 ( column monitor) Table of Contents 1. 开启与关闭 2. 字 ...

  5. js前台传数组,java后台接收转list,前后台用正则校验

    前台,传参数时,将数组对象转换成json串,后台java收到后用 JSONArray.fromObject 转成集合. 前台js:var params = {"FileNameList&qu ...

  6. idea用法

    更新gradle的依赖后,刷新项目引入jar包的方法: view--Tool Buttons 在右侧 Gradle 点刷新

  7. java:shiro(认证,赋予角色,授权...)

    1.shiro(权限框架(认证,赋予角色,授权...)): readme.txt(运行机制): 1.从jsp的form中的action属性跳转到springmvc的Handler中(controlle ...

  8. “vmware 未能初始化监视器设备”的解决方法

    从挂起状态唤醒时出现"vmware 未能初始化监视器设备"的提示,在cmd中输入命令 net start vmci net start vmx86 可能还不能成功启动,提示&quo ...

  9. Android开发 互相调用模式之C#主导

    首先明确一个概念,当我们不使用Android Studio提供的那些包,仅仅是Unity打包apk,打包出来的apk里面也包含了SDK (1)首先删除Unity下我们创建的Plugins文件夹,因为这 ...

  10. 简单场景的类继承、复杂场景的类继承(钻石继承)、super、类的方法

    1.python子类调用父类成员有2种方法,分别是普通方法和super方法 class Base(object): #基类的表示方法可以多种,Base(),Base,Base(object),即新式类 ...