Spring JPA Repository: Prevent Update on Save

Spring JPA repository: prevent update on save

When using the default configuration, and using CrudRepository#save() or JpaRepository#save() it will delegate to the EntityManager to use either persists() if it is a new entity, or merge() if it is not.

The strategy followed to detect the entity state, new or not, to use the appropiate method, when using the default configuration is as follows:

  • By default, a Property-ID inspection is performed, if it is null, then it is a new entity, otherwise is not.
  • If the entity implements Persistable the detection will be delegated to the isNew() method implemented by the entity.
  • There is a 3rd option, implementing EntityInformation, but further customizations are needed.

source

So in your case, as you are using the username as ID, and it isn't null, the Repository call ends up delegating to EntityManager.merge() instead of persist(). So there are two possible solutions:

  • use a diferent ID property, set it to null, and use any auto-generation method, or
  • make User implement Persistable and use the isNew() method, to determine if it is a new entity or not.

If for some reason, you don't want to modify your entities, you can also change the behaviour modifying the flush mode configuration. By default, in spring data jpa, hibernate flush mode is set to AUTO. What you want to do is to change it to COMMIT, and the property to change it is org.hibernate.flushMode. You can modify this configuration by overriding a EntityManagerFactoryBean in a @Configuration class.


And if you don't want to mess the configuration of the EntityManager, you can use the JpaRepository#flush() or JpaRepository#saveAndFlush() methods, to commit the pending changes to the database.

How to prevent automatic update in Spring Data JPA?

Use Deep cloning to solve your issue.

First override the clone method inside your Address class like below.
Note : Please customize the implementation of clone() method by adding your class attributes.Since you didn't mention the structure of the class Address , I have implemented the solution with my own defined class attributes.

Address class

public class Address {

private String country;
private String city;
private String district;
private String addressValue;

public Address() {
super();

}

public Address(String country, String city, String district, String addressValue) {
super();
this.country = country;
this.city = city;
this.district = district;
this.addressValue = addressValue;
}
//Getters and Setters

@Override
protected Object clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
return new Address(this.getCountry(), this.getCity(), this.getDistrict(),this.getAddressValue());
}
}

}

Then re construct your class Tasket like below.

Tasket Class

Employee employee = employeeRepository.fetchEmployee(employeeName);
List<Address> addressList = employee.getAddress();
List<Address> clonedAddressList = new ArrayList<>();
addressList.forEach(address -> clonedAddressList.add((Address)address.clone()) );
clonedAddressList.forEach(address -> address.setStatus(Status.INVALID.toString()));

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

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

}


Related Topics



Leave a reply



Submit