#java #spring #hibernate #jpa
#java #весна #спящий режим #jpa
Вопрос:
У меня есть 2 объекта Space и Type. Они оба связаны друг с другом. Используя эти объекты, я сталкиваюсь со многими нежелательными выполняемыми инструкциями update, когда код выполняет даже очень простые операции.
Я предлагаю простой сценарий ниже. Но также в моем приложении выполняются некоторые более сложные пакетные операции (API Spring Boot). И эта проблема приводит к обновлению всех связанных объектов, даже если они не изменены.
Мне нужно как-то избавиться от этих нежелательных обновлений, потому что они вызывают большие проблемы с производительностью для некоторых операций.
Объект пространства (показан частично):
@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;
}
Тип объекта (показан частично):
@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 repo:
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);
}
Космический crud-репозиторий:
public interface RelationalSpaceRepository extends CrudRepository<SpaceDao, byte[]> { }
Журналы гибернации, генерируемые при попадании кода в строку repository.save():
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. Требуется дополнительная информация, например, откуда берутся эти обнаруженные различия. Я бы заподозрил, что чтение SpaceDao при использовании find устаревает между вызовом find и вызовом save — проблема, если вы используете операцию, подобную слиянию. Вы хотели бы убедиться, что вы сохраняете контекст, используемый для первоначального чтения в SpaceDao, для использования в операции сохранения, поскольку его можно использовать для отслеживания фактических различий, которые вы сделали, вместо того, чтобы обнаруживать различия между ним и тем, что в настоящее время находится в базе данных на момент вызова save .
2. Если я правильно помню, JPA основан на управляемых объектах. Поэтому всякий раз, когда вы применяете некоторые изменения к управляемому объекту в сеансе, они будут сохраняться при сбросе / закрытии сеанса. Вы уверены, что обновления не поступают из вашего управления сеансами? Редактировать: но похоже, что вы не применяете никаких изменений…
3. SpaceDao имеет отношение «Многие к одному» с TypeDao, а type также имеет отношение «Многие к одному» с SpaceDao, это правильное отношение или это отношение «Многие ко многим»?
4. Два раза связь ManyToOne очень подозрительна. Кроме того, используйте @JoinColumn только с одной стороны (сторона ManyToOne).
5. @Chris, ну, это так же просто, как показано в методе реализации repo. Это нелогично, но я упростил его, чтобы сначала протестировать возможные решения в этом случае. Как видно, между чтением и записью нет никаких изменений или каких-либо других операций. Кроме того, методы findById() и save() по умолчанию используются в CrudRepository данных Spring.
Ответ №1:
Найдена причина и решение.
Это было вызвано ложными срабатываниями грязных проверок. Я не совсем уверен, почему, но после извлечения объекта из базы данных значения атрибутов для ссылочных типизированных восстанавливаются, я полагаю. В результате чего грязные проверки обрабатывают эти объекты как измененные. Итак, при следующем сбросе контекста все «измененные» объекты сохраняются в базе данных.
В качестве решения я реализовал пользовательский перехватчик 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();
}
}
Комментарии:
1. не решит ли переопределяющий
equals()
метод эту проблему?2. не решит ли переопределяющий
equals()
метод эту проблему?