I've read the SO suggested questions related to this one and checked if I have the same issue as version, concurrent modification, etc but I think mine is slightly different.
Stack:
- JavaEE
- PostgreSQL
- JSF2.2
- Primefaces
I'm saving an entity Endpoint and it works on the first call, but saving the same entity again throws the error in the title.
Here's the entity class: https://github.com/meveo-org/meveo/blob/develop/meveo-admin/ejbs/src/main/java/org/meveo/service/technicalservice/endpoint/EndpointService.java
public E update(E entity) throws BusinessException {
preUpdate(entity);
try {
entity = getEntityManager().merge(entity);
} catch(Exception e) {
if(e instanceof UndeclaredThrowableException) {
throw new BusinessException(e.getCause().getCause());
} else {
throw new BusinessException(e);
}
}
postUpdate(entity);
return entity;
}
public void updateNoMerge(E entity) throws BusinessException {
preUpdate(entity);
postUpdate(entity);
}
And here is the service that saves the entity: https://github.com/meveo-org/meveo/blob/develop/meveo-model/src/main/java/org/meveo/model/technicalservice/endpoint/Endpoint.java
@Entity
@Table(name = "service_endpoint")
@GenericGenerator(name = "ID_GENERATOR", strategy = "increment")
@NoIntersectionBetween(
firstCollection = "pathParameters.endpointParameter.parameter",
secondCollection = "parametersMapping.endpointParameter.parameter"
)
@NamedQueries({
@NamedQuery(name = "findByParameterName", query = "SELECT e FROM Endpoint e " +
"INNER JOIN e.service as service " +
"LEFT JOIN e.pathParameters as pathParameter " +
"LEFT JOIN e.parametersMapping as parameterMapping " +
"WHERE service.code = :serviceCode " +
"AND (pathParameter.endpointParameter.parameter = :propertyName OR parameterMapping.endpointParameter.parameter = :propertyName)"),
@NamedQuery(name = "Endpoint.deleteByService", query = "DELETE from Endpoint e WHERE e.service.id=:serviceId")})
@ImportOrder(5)
@ExportIdentifier({ "code" })
@ModuleItem("Endpoint")
@ModuleItemOrder(80)
@ObservableEntity
public class Endpoint extends BusinessEntity {
private static final long serialVersionUID = 6561905332917884613L;
@ElementCollection(fetch = FetchType.LAZY)
@Fetch(value = FetchMode.SUBSELECT)
@CollectionTable(name = "service_endpoint_roles", joinColumns = @JoinColumn(name = "endpoint_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
/**
* Technical service associated to the endpoint
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id", updatable = false, nullable = false)
private Function service;
/**
* Whether the execution of the service will be syncrhonous.
* If asynchronous, and id of execution will be returned to the user.
*/
@Type(type = "numeric_boolean")
@Column(name = "synchronous", nullable = false)
private boolean synchronous;
/**
* Method used to access the endpoint.
* Conditionates the input format of the endpoint.
*/
@Enumerated(EnumType.STRING)
@Column(name = "method", nullable = false)
private EndpointHttpMethod method;
/**
* Parameters that will be exposed in the endpoint path
*/
@OneToMany(mappedBy = "endpointParameter.endpoint", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@OrderColumn(name = "position")
private List<EndpointPathParameter> pathParameters = new ArrayList<>();
/**
* Mapping of the parameters that are not defined as path parameters
*/
@OneToMany(mappedBy = "endpointParameter.endpoint", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<TSParameterMapping> parametersMapping = new ArrayList<>();
/**
* JSONata query used to transform the result
*/
@Column(name = "jsonata_transformer")
private String jsonataTransformer;
/**
* Context variable to be returned by the endpoint
*/
@Column(name = "returned_variable_name")
private String returnedVariableName;
/**
* Context variable to be returned by the endpoint
*/
@Type(type = "numeric_boolean")
@Column(name = "serialize_result", nullable = false)
private boolean serializeResult;
/**
* Content type of the response
*/
@Column(name = "content_type")
private String contentType;
The persistence.xml file https://github.com/meveo-org/meveo/blob/master/meveo-admin/web/src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="MeveoAdmin">
<jta-data-source>java:jboss/datasources/MeveoAdminDatasource</jta-data-source>
<jar-file>lib/meveo-model-${project.version}.jar</jar-file>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<properties>
<property name="hibernate.archive.autodetection" value="class" />
<property name="hibernate.hbm2ddl.auto" value="validate" /> <!-- DB structure is managed by liquibase, not hibernate -->
<property name="hibernate.show_sql" value="true" />
<property name="format_sql" value="true"/>
<property name="use_sql_comments" value="false"/>
<property name="hibernate.connection.harSet" value="utf-8"/>
<property name="hibernate.connection.useUnicode" value="true"/>
<property name="hibernate.connection.characterEncoding" value="UTF-8" />
<!-- <property name="hibernate.default_schema" value="public" /> Disable for Mysql/mariaDB instalation -->
<property name="hibernate.cache.use_second_level_cache" value="true" />
<property name="hibernate.cache.use_query_cache" value="true" />
<property name="hibernate.cache.use_minimal_puts" value="true" />
<property name="hibernate.cache.default_cache_concurrency_strategy" value="transactional" />
<property name="hibernate.generate_statistics" value="false" />
<property name="hibernate.discriminator.ignore_explicit_for_joined" value="true" />
<property name="hibernate.ejb.event.flush" value="org.meveo.jpa.event.FlushEventListener" /> <!-- Needed for ES -->
<property name="hibernate.jpa.compliance.global_id_generators" value="false"/>
<property name="hibernate.c3p0.min_size" value="5"></property>
<property name="hibernate.c3p0.max_size" value="20"></property>
<property name="hibernate.c3p0.acquire_increment" value="5"></property>
<property name="hibernate.c3p0.timeout" value="1800"></property>
<!-- <property name="hibernate.persister.resolver" value="org.hibernate.util.CustomPersisterClassResolver"></property> -->
<property name="hibernate.transaction.factory_class" value="org.hibernate.transaction.JTATransactionFactory"/>
<property name="hibernate.connection.isolation" value="4"></property>
</properties>
</persistence-unit>
In EndpointService.update, I tried experimenting with:
- super.update - calls em.merge
- super.updateNoMerge - basically does nothing with regards to the entity, it just calls some pre and posts update triggers.
In the second update I got these logs for both methods above:
19:31:31,022 ERROR [org.jboss.as.ejb3.invocation] (default task-1) WFLYEJB0034: EJB Invocation failed on component EndpointService for method public long org.meveo.service.base.PersistenceService.count(org.meveo.admin.util.pagination.PaginationConfiguration): javax.ejb.EJBException: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.meveo.model.technicalservice.endpoint.Endpoint#1] ->Which is triggered when count query is executed. I think at this point hibernate decides to flush the save transactions.
more logs here...
Caused by: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.meveo.model.technicalservice.endpoint.Endpoint#1]
After the error, save the entity again and it will work. It's this cycle:
1,0,PAGE_RELOAD,1,0
Where 1 is successful and 0 fails.
I already checked the entity relationships as well as child entities but I couldn't figure out the problem.
Any idea?
Here's the full error log: https://www.dropbox.com/s/a0r12rf1vf6rshe/optimisticlockexception.txt
OptimisticLockException
protects you from race conditions – crizzis