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 theisNew()
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 theisNew()
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
In Activity.Oncreate(), Why Does Intent.Getextras() Sometimes Return Null
Spring Data Query Method With Optional @Param
How to Equal Two 2D Arrays Together in Java
Convert to Int from String of Numbers Having Comma
How to Get Exponents Without Using the Math.Pow for Java
How to Check If the File Is Image
How to Wait for All Threads to Finish, Using Executorservice
Use a Binary Search on an Int Array Sorted in Descending Order
This Program Is About String Compression in Java
Replacement for Wsimport With Jdk 11
Password Encrypt and Decrypt Using Spring-Security
How to Filter Nested Loops Using Java 8 Streams and Filters
How to Listen With Multiple Port for Multiple Client in Single Jpos Instance
Run Gradle Task With Spring Profiles (Integration Tests)
Selecting Specific Rows from Sqlite Database-Android-Sqlite Database