简体   繁体   中英

How to prevent unwanted update statements being executed using Hibernate + JPA

I have 2 entities Space and Type. They are both linked to each-other. Using these objects, I encounter many unwanted update statements being executed, when the code performs even very simple operations.

I am putting a simple scenario down below. But also, there are some more complex batch operations being performed in my application (a Spring Boot API). And, this issue is causing all the linked entities being updated, even though they are not modified.

I need to somehow get rid of these unwanted updates, because they are causing big performance issues for some operations.

Space entity (shown partially):

@Entity
@Table(name = "spaces")
@Getter
@Setter
@NoArgsConstructor
public class SpaceDao {
        @Id
        @GeneratedValue(generator = "uuid")
        @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
        private byte[] uuid;

        @ManyToOne
        @JoinColumn(name = "type_id")
        private TypeDao type;
}

Type entity (shown partially):

@Entity
@Table(name = "types")
@Getter
@Setter
@NoArgsConstructor
public class TypeDao {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
  
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="space_id")
    private SpaceDao space;
}

Save method in the Space repo implementation:

public Space saveSpace(Space space) {
    SpaceDao spaceDao = SpaceMapper.toDao(space);

    // Intentionally simplified this logic, to point out that
    // I am only reading and saving the object, without any changes.
    SpaceDao existingSpaceDao = relationalSpaceRepository.findById(spaceDao.getUuid()).get();
    // Following line is where the magic happens
    SpaceDao savedSpaceDao = relationalSpaceRepository.save(existingSpaceDao);

    return SpaceMapper.toModelObject(savedSpaceDao, true);
}

Space crud repository:

public interface RelationalSpaceRepository extends CrudRepository<SpaceDao, byte[]> { }

Hibernate logs generated when code hits repository.save() line:

Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?
Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?
Hibernate: update types set category=?, definition=?, description=?, disabled=?, logical_order=?, name=?, space_id=? where id=?
Hibernate: update types set category=?, definition=?, description=?, disabled=?, logical_order=?, name=?, space_id=? where id=?
Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?
Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?
Hibernate: update types set category=?, definition=?, description=?, disabled=?, logical_order=?, name=?, space_id=? where id=?
Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?
Hibernate: update types set category=?, definition=?, description=?, disabled=?, logical_order=?, name=?, space_id=? where id=?
Hibernate: update spaces set description=?, location=?, name=?, parent_id=?, properties=?, status_id=?, status=?, subtype_id=?, subtype=?, type_id=?, type=? where uuid=?

Found the reason and a solution.

It was caused by false positive dirty checks. I am not exactly sure why, but after an entity is retrieved from database, attribute values for reference typed ones gets reinstantiated I believe. Causing the dirty checks to treat these entities as modified. So, the next time context gets flushed, all "modified" entities is being persisted to the database.

As a solution, I implemented a custom Hibernate interceptor, extending EmptyInterceptor. And registered it as hibernate.session_factory.interceptor . That way, I am able to do my custom comparisons and evaluate the dirty flag manually.

Interceptor implementation:

@Component
public class CustomHibernateInterceptor extends EmptyInterceptor {

    private static final long serialVersionUID = -2355165114530619983L;

    @Override
    public int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
            String[] propertyNames, Type[] types) {
        if (entity instanceof BaseEntity) {
            Set<String> dirtyProperties = new HashSet<>();
            for (int i = 0; i < propertyNames.length; i++) {
                if (isModified(currentState, previousState, types, i)) {
                    dirtyProperties.add(propertyNames[i]);
                }
            }

            int[] dirtyPropertiesIndices = new int[dirtyProperties.size()];
            List<String> propertyNamesList = Arrays.asList(propertyNames);
            int i = 0;
            for (String dirtyProperty : dirtyProperties) {
                dirtyPropertiesIndices[i++] = propertyNamesList.indexOf(dirtyProperty);
            }
            return dirtyPropertiesIndices;
        }

        return super.findDirty(entity, id, currentState, previousState, propertyNames, types);
    }

    private boolean isModified(Object[] currentState, Object[] previousState, Type[] types, int i) {
        boolean equals = true;
        Object oldValue = previousState[i];
        Object newValue = currentState[i];

        if (oldValue != null || newValue != null) {
            if (types[i] instanceof AttributeConverterTypeAdapter) {
                // check for JSONObject attributes
                equals = String.valueOf(oldValue).equals(String.valueOf(newValue));
            } else if (types[i] instanceof BinaryType) {
                // byte arrays in our entities are always UUID representations
                equals = Utilities.byteArrayToUUID((byte[]) oldValue)
                        .equals(Utilities.byteArrayToUUID((byte[]) newValue));
            } else if (!(types[i] instanceof CollectionType)) {
                equals = Objects.equals(oldValue, newValue);
            }
        }

        return !equals;
    }
}

Registering in the config:

@Configuration
public class XDatabaseConfig {

    @Bean(name = "xEntityManagerFactory")
    @Primary
    public EntityManagerFactory entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        CustomHibernateInterceptor interceptor = new CustomHibernateInterceptor();
        vendorAdapter.setGenerateDdl(Boolean.FALSE);
        vendorAdapter.setShowSql(Boolean.TRUE);
        vendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5InnoDBDialect");
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.x.dal.relational.model");
        factory.setDataSource(xDataSource());
        factory.getJpaPropertyMap().put("hibernate.session_factory.interceptor", interceptor);
        factory.afterPropertiesSet();
        factory.setLoadTimeWeaver(new InstrumentationLoadTimeWeaver());
        return factory.getObject();
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM