spring boot jpa @PreUpdate结合@DynamicUpdate使用的局限性
通常给实体添加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,更新所有的字段。