• 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,更新所有的字段。
  • 相关阅读:
    jumpserver的安装
    安装iostat 命令
    zabbix配置server,proxy,agent架构
    RGB颜色对照表
    【ZYNQ-7000开发之九】使用VDMA在PL和PS之间传输视频流数据
    基于AXI VDMA的图像采集系统
    图像采集调试总结
    DDR3调试总结
    内存系列二:深入理解硬件原理
    在嵌入式设计中使用MicroBlaze(Vivado版本)
  • 原文地址:https://www.cnblogs.com/hankuikui/p/11962710.html
Copyright © 2020-2023  润新知