如何使用Hibernate + JPA防止执行不需要的更新语句

huangapple go评论75阅读模式
英文:

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

问题

我有两个实体:Space(空间)和Type(类型)。它们彼此相互关联。使用这些对象,当代码执行甚至非常简单的操作时,会出现许多不必要的更新语句被执行的情况。

我在下面描述一个简单的场景。此外,在我的应用程序(一个Spring Boot API)中还执行了一些更复杂的批量操作。这个问题导致所有关联的实体都被更新,即使它们没有被修改。

我需要想办法摆脱这些不必要的更新,因为它们对某些操作造成了严重的性能问题。

Space实体(部分展示):

@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
@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;
}

Space仓库实现中的保存方法:

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

    // 故意简化了这个逻辑,以指出我只是读取和保存对象,没有任何更改。
    SpaceDao existingSpaceDao = relationalSpaceRepository.findById(spaceDao.getUuid()).get();
    // 下面这行是发生魔法的地方
    SpaceDao savedSpaceDao = relationalSpaceRepository.save(existingSpaceDao);

    return SpaceMapper.toModelObject(savedSpaceDao, true);
}

Space crud 仓库:

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

当代码执行repository.save()这行时生成的Hibernate日志:

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=?
英文:

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 = &quot;spaces&quot;)
@Getter
@Setter
@NoArgsConstructor
public class SpaceDao {
        @Id
        @GeneratedValue(generator = &quot;uuid&quot;)
        @GenericGenerator(name = &quot;uuid&quot;, strategy = &quot;org.hibernate.id.UUIDGenerator&quot;)
        private byte[] uuid;

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

Type entity (shown partially):

@Entity
@Table(name = &quot;types&quot;)
@Getter
@Setter
@NoArgsConstructor
public class TypeDao {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
  
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name=&quot;space_id&quot;)
    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&lt;SpaceDao, byte[]&gt; { }

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=?

答案1

得分: 0

找到了原因和解决方案。

这是由于错误的正面脏检查引起的。我不太确定原因,但在从数据库检索实体之后,引用类型的属性值会被重新实例化,我认为是这样的。导致脏检查将这些实体视为已修改。因此,下次上下文刷新时,所有“已修改”的实体都会被持久化到数据库中。

作为解决方案,我实现了一个自定义的 Hibernate 拦截器,继承 EmptyInterceptor。并将其注册为 hibernate.session_factory.interceptor。这样,我可以进行自定义比较并手动评估脏标志。

拦截器实现:

@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;
    }
}

在配置中注册:

@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();
    }

}
英文:

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&lt;String&gt; dirtyProperties = new HashSet&lt;&gt;();
for (int i = 0; i &lt; propertyNames.length; i++) {
if (isModified(currentState, previousState, types, i)) {
dirtyProperties.add(propertyNames[i]);
}
}
int[] dirtyPropertiesIndices = new int[dirtyProperties.size()];
List&lt;String&gt; 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 = &quot;xEntityManagerFactory&quot;)
@Primary
public EntityManagerFactory entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
CustomHibernateInterceptor interceptor = new CustomHibernateInterceptor();
vendorAdapter.setGenerateDdl(Boolean.FALSE);
vendorAdapter.setShowSql(Boolean.TRUE);
vendorAdapter.setDatabasePlatform(&quot;org.hibernate.dialect.MySQL5InnoDBDialect&quot;);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan(&quot;com.x.dal.relational.model&quot;);
factory.setDataSource(xDataSource());
factory.getJpaPropertyMap().put(&quot;hibernate.session_factory.interceptor&quot;, interceptor);
factory.afterPropertiesSet();
factory.setLoadTimeWeaver(new InstrumentationLoadTimeWeaver());
return factory.getObject();
}
}

huangapple
  • 本文由 发表于 2020年9月23日 20:55:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/64028474.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定