英文:
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 = "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=?
答案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<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();
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论