两次保存实体时出现 OptimisticLockException

OptimisticLockException when saving an entity twice

我已经阅读了与此相关的 SO 建议问题,并检查了我是否有与版本、并发修改等相同的问题,但我认为我的问题略有不同。

堆栈:

我正在保存一个实体端点,它在第一次调用时工作,但再次保存同一个实体会在标题中引发错误。

这是实体 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);
}

这是保存实体的服务: 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;

persistence.xml 文件 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>

在 EndpointService.update 中,我尝试尝试:

  1. super.update - 调用 em.merge
  2. super.updateNoMerge - 基本上对实体什么都不做,它只是调用一些前后更新触发器。

在第二次更新中,我得到了上述两种方法的这些日志:

19:31:31,022 错误 [org.jboss.as.ejb3.invocation](默认任务 1)WFLYEJB0034:方法 public long [=76= 的组件端点服务上的 EJB 调用失败](org.meveo.admin.util.pagination.PaginationConfiguration): javax.ejb.EJBException: javax.persistence.OptimisticLockException: 行已被另一个事务更新或删除(或 unsaved-value 映射不正确): [org.meveo.model.technicalservice.endpoint.Endpoint#1] -> 执行计数查询时触发。我认为此时休眠决定刷新保存事务。

此处有更多日志...

原因:javax.persistence.OptimisticLockException:行已被另一个事务更新或删除(或 unsaved-value 映射不正确):[org.meveo.model.technicalservice.endpoint.Endpoint#1]

报错后,再次保存实体即可。就是这个循环:

1,0,PAGE_RELOAD,1,0

其中 1 成功,0 失败。

我已经检查了实体关系以及 child 个实体,但我无法找出问题所在。

有什么想法吗?

这是完整的错误日志:https://www.dropbox.com/s/a0r12rf1vf6rshe/optimisticlockexception.txt

问题原因:在同一个服务中定义了Event Observer。

public void onEndpointUpdate(@Observes E entity) {
    // call another service that manipulates the entity here.
}

解决方案:将事件侦听器重构为另一个无状态 class 以避免陈旧的异常。