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,更新所有的字段。
原文地址:https://www.cnblogs.com/hankuikui/p/11962710.html