Concorrência no Eclipselink JPA

Olá,

Estou desenvolvendo um projeto, mas estou com um problema na concorrência, estou utilizando o JPA Eclipselink, mas está acontecendo o seguinte:

Estou utilizando a classe abaixo para realizar alguns testes.
Estou fazendo login no sistema com 2 usuários e atualizando a mesma informação, no caso o campo “Valor”.

Busco a informação no banco de dados: vem 100.

No usuário 1, estou mudando o valor para 105 e salvando.

No usuário 2, a informação ainda é 100 e estou mudando para 110 e salvando e o JPA está permitindo que seja atualizado para 100, perdendo assim o que o usuário 1 fez.

Não está sendo disparado nenhuma PersistenceException.

Li que o JPA faz esse controle por default, mas eu tenho que adicionar alguma coisa a minha classe?

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package br.com.celg.entidade;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.xml.bind.annotation.XmlRootElement;

/**
 *
 * @author Rodrigo
 */
@Entity
@Table(name = "AIC")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "Aic.findAll", query = "SELECT e FROM Aic e"),
    @NamedQuery(name = "Aic.findById", query = "SELECT e FROM Aic e WHERE e.codigo = :codigo"),
    @NamedQuery(name = "Aic.findByOdex", query = "SELECT e FROM Aic e WHERE e.odexPk.odex = :odex"),
    @NamedQuery(name = "Aic.findByValor", query = "SELECT e FROM Aic e WHERE e.valor = :valor"),
    @NamedQuery(name = "Aic.findByDescricaoOdex", query = "SELECT e FROM Aic e WHERE e.descricaoOdex = :descricaoOdex"),
    @NamedQuery(name = "Aic.findByInteressado", query = "SELECT e FROM Aic e WHERE e.interessado = :interessado"),
    @NamedQuery(name = "Aic.findByObrasTerceiros", query = "SELECT e FROM Aic e WHERE e.obrasTerceiros = :obrasTerceiros"),
    @NamedQuery(name = "Aic.findByOdi", query = "SELECT e FROM Aic e WHERE e.odi = :odi"),
    @NamedQuery(name = "Aic.findByCr", query = "SELECT e FROM Aic e WHERE e.cr = :cr"),
    @NamedQuery(name = "Aic.findByDataAvisoConclusao", query = "SELECT e FROM Aic e WHERE e.dataAvisoConclusao = :dataAvisoConclusao"),
    @NamedQuery(name = "Aic.findByDataInicio", query = "SELECT e FROM Aic e WHERE e.dataInicio = :dataInicio"),
    @NamedQuery(name = "Aic.findByDataFim", query = "SELECT e FROM Aic e WHERE e.dataFim = :dataFim"),
    @NamedQuery(name = "Aic.findByStatusSgt", query = "SELECT e FROM Aic e WHERE e.statusSgt = :statusSgt"),
    @NamedQuery(name = "Aic.findByStatusAvisoConclusao", query = "SELECT e FROM Aic e WHERE e.statusAvisoConclusao = :statusAvisoConclusao"),
    @NamedQuery(name = "Aic.findByDataEnergizacao", query = "SELECT e FROM Aic e WHERE e.dataEnergizacao = :dataEnergizacao"),
    @NamedQuery(name = "Aic.findByRegional", query = "SELECT e FROM Aic e WHERE e.regional = :regional"),
    @NamedQuery(name = "Aic.findByResponsavel", query = "SELECT e FROM Aic e WHERE e.responsavel = :responsavel"),
    @NamedQuery(name = "Aic.findByMunicipio", query = "SELECT e FROM Aic e WHERE e.municipio = :municipio")})
public class Aic extends SuperLogic implements Serializable {
    private static final long serialVersionUID = 1L;
    // @Max(value=?)  @Min(value=?)//if you know range of your decimal fields consider using these annotations to enforce field validation
  
    @Column(name="DATA")
    @Temporal(TemporalType.TIMESTAMP)
    private Date data;
    @javax.persistence.Transient
    private String odex;
    @Column(name = "VALOR")
    private BigDecimal valor;
    @Column(name = "DESCRICAO_ODEX")
    private String descricaoOdex;
    @Column(name = "INTERESSADO")
    private String interessado;
    @Column(name = "OBRAS_TERCEIROS")
    private String obrasTerceiros;
    @Column(name = "ODI")
    private Long odi;
    @Column(name = "DATA_AVISO_CONCLUSAO")
    @Temporal(TemporalType.TIMESTAMP)
    private Date dataAvisoConclusao;
    @Column(name = "DATA_INICIO")
    @Temporal(TemporalType.TIMESTAMP)
    private Date dataInicio;
    @Column(name = "DATA_FIM")
    @Temporal(TemporalType.TIMESTAMP)
    private Date dataFim;
    @Column(name = "MUNICIPIO")
    private String municipio;
    @Column(name = "REGIONAL")
    private String regional;
    @Column(name = "RESPONSAVEL")
    private String responsavel;
    @Column(name = "STATUS_SGT")
    private String statusSgt;
    @Column(name = "STATUS_AVISO_CONCLUSAO")
    private String statusAvisoConclusao;
    @Column(name = "DATA_ENERGIZACAO")
    @Temporal(TemporalType.TIMESTAMP)
    private Date dataEnergizacao;
    @Column(name = "DATA_REF")
    private String dataRef;
    @JoinColumn(name = "ODEX_PK", referencedColumnName = "CODIGO")
    @ManyToOne(cascade = CascadeType.ALL)
    private Odex odexPk;
    @JoinColumn(name = "CR", referencedColumnName = "CODIGO")
    @ManyToOne
    private Cr cr;
    @javax.persistence.Transient
    private ClassTaxonomia idClassTaxonomia;

    public Aic() {
    }

    public Date getData() {
        return data;
    }

    public void setData(Date data) {
        this.data = data;
    }

    public String getOdex() {
        return odex;
    }

    public void setOdex(String odex) {
        this.odex = odex;
    }

    public BigDecimal getValor() {
        return valor;
    }

    public void setValor(BigDecimal valor) {
        this.valor = valor;
    }

    public String getDescricaoOdex() {
        return descricaoOdex;
    }

    public void setDescricaoOdex(String descricaoOdex) {
        this.descricaoOdex = descricaoOdex;
    }

    public String getInteressado() {
        return interessado;
    }

    public void setInteressado(String interessado) {
        this.interessado = interessado;
    }

    public String getObrasTerceiros() {
        return obrasTerceiros;
    }

    public void setObrasTerceiros(String obrasTerceiros) {
        this.obrasTerceiros = obrasTerceiros;
    }

    public Long getOdi() {
        return odi;
    }

    public void setOdi(Long odi) {
        this.odi = odi;
    }

    public Date getDataAvisoConclusao() {
        return dataAvisoConclusao;
    }

    public void setDataAvisoConclusao(Date dataAvisoConclusao) {
        this.dataAvisoConclusao = dataAvisoConclusao;
    }

    public Date getDataInicio() {
        return dataInicio;
    }

    public void setDataInicio(Date dataInicio) {
        this.dataInicio = dataInicio;
    }

    public Date getDataFim() {
        return dataFim;
    }

    public void setDataFim(Date dataFim) {
        this.dataFim = dataFim;
    }

    public String getMunicipio() {
        return municipio;
    }

    public void setMunicipio(String municipio) {
        this.municipio = municipio;
    }

    public String getRegional() {
        return regional;
    }

    public void setRegional(String regional) {
        this.regional = regional;
    }

    public String getResponsavel() {
        return responsavel;
    }

    public void setResponsavel(String responsavel) {
        this.responsavel = responsavel;
    }

    public String getStatusSgt() {
        return statusSgt;
    }

    public void setStatusSgt(String statusSgt) {
        this.statusSgt = statusSgt;
    }

    public String getStatusAvisoConclusao() {
        return statusAvisoConclusao;
    }

    public void setStatusAvisoConclusao(String statusAvisoConclusao) {
        this.statusAvisoConclusao = statusAvisoConclusao;
    }

    public Date getDataEnergizacao() {
        return dataEnergizacao;
    }

    public void setDataEnergizacao(Date dataEnergizacao) {
        this.dataEnergizacao = dataEnergizacao;
    }

    public String getDataRef() {
        return dataRef;
    }

    public void setDataRef(String dataRef) {
        this.dataRef = dataRef;
    }

    public Odex getOdexPk() {
        return odexPk;
    }

    public void setOdexPk(Odex odexPk) {
        this.odexPk = odexPk;
    }
    
    public Cr getCr() {
        return cr;
    }

    public void setCr(Cr cr) {
        this.cr = cr;
    }

    public ClassTaxonomia getIdClassTaxonomia() {
        return idClassTaxonomia;
    }

    public void setIdClassTaxonomia(ClassTaxonomia idClassTaxonomia) {
        this.idClassTaxonomia = idClassTaxonomia;
    }

   
    
    @Override
    public int hashCode() {
        int hash = 0;
        hash += (getCodigo() != null ? getCodigo().hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Aic)) {
            return false;
        }
        Aic other = (Aic) object;
        if ((this.getCodigo() == null && other.getCodigo() != null) || (this.getCodigo() != null && !this.getCodigo().equals(other.getCodigo()))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "br.com.celg.entidade.Aic[ codigo=" + getCodigo() + " ]";
    }
    
}

Tecnicamente, não está incorreto, salvo, se o momento em que ambos realizam a atualização do valor é o mesmo.
Aí sim você tem um problema de concorrência.
Pelo teu relato, você tem um problema é com teus requisitos. Não está claro o comportamento esperado pelo sistema.

Não entendi muito bem o que você quis dizer.
A atualização do valor não é feita no mesmo momento (milissegundos), estou disparando uma atualização e depois a outra.

Eu só preciso que quando um usuário tente alterar um dado que já foi alterado, ele não consiga, o que é feito pela concorrência do JPA que não está funcionando.

Aqui as requisições que estão sendo realizadas no BD:

Detalhado:   UPDATE AIC SET MUNICIPIO = ?, RESPONSAVEL = ?, STATUS_AVISO_CONCLUSAO = ?, STATUS_SGT = ?, VALOR = ?, DATAALTERACAO = ?, MATRICULA_USUARIO = ? WHERE (CODIGO = ?)
	bind => [8 parameters bound] Aqui o valor passou de 100 para 105
Detalhado:   SELECT CODIGO, ATIVO, DATAALTERACAO, DATAINCLUSAO, EMAIL, MATRICULA, MATRICULA_USUARIO, NOME, SENHA, CR FROM USUARIO WHERE (((? = ?) AND (MATRICULA = ?)) AND (ATIVO = ?))
	bind => [4 parameters bound]
Detalhado:   SELECT CODIGO, ATIVO, DATAALTERACAO, DATAINCLUSAO, EMAIL, MATRICULA, MATRICULA_USUARIO, NOME, SENHA, CR FROM USUARIO WHERE (((? = ?) AND (MATRICULA = ?)) AND (ATIVO = ?))
	bind => [4 parameters bound]
Detalhado:   SELECT t0.PROCESS_DESCRIPTIONS FROM CLASS_TAXONOMIA t0, KPI_NAME t1 WHERE (((? = ?) AND (t1.ATIVO = ?)) AND (t0.CODIGO = t1.ID_CLASS_TAXONOMIA))
	bind => [3 parameters bound]
Detalhado:   SELECT CODIGO, ATIVO, DATAALTERACAO, DATA_EFETIVA, DATAINCLUSAO, ENEL_2_FAST_CLOSING, ENEL_FAST_CLOSING, MATRICULA_USUARIO, PROCESS_DESCRIPTIONS FROM CLASS_TAXONOMIA WHERE ((? = ?) AND (ATIVO = ?)) ORDER BY PROCESS_DESCRIPTIONS
	bind => [3 parameters bound]
Detalhado:   SELECT CODIGO, ATIVO, CR, DATAALTERACAO, DATAINCLUSAO, MATRICULA_USUARIO, ID_AREA, ID_DEPARTAMENTO FROM CR WHERE ((? = ?) AND (ATIVO = ?)) ORDER BY CR
	bind => [3 parameters bound]
Detalhado:   SELECT CODIGO, AREA, ATIVO, DATAALTERACAO, DATAINCLUSAO, MATRICULA_USUARIO FROM AREA WHERE ((? = ?) AND (ATIVO = ?)) ORDER BY AREA
	bind => [3 parameters bound]
Detalhado:   UPDATE AIC SET VALOR = ?, DATAALTERACAO = ?, MATRICULA_USUARIO = ? WHERE (CODIGO = ?) (Aqui deveria estar aparecendo um erro, pois o valor para esse usuário era 100, não verificou que já foi atualizado para 105, e colocou 110, tirando a atualização feita pelo outro usuário).
	bind => [4 parameters bound]

Logo, não tem concorrência, não concorda?
Concorrência só existe quando dois ou mais acessos tentam alterar algo ao mesmo tempo. Aí entra o mecanismo do JPA ou o do próprio SGBD.

Por isso me referi aos requisitos. Afinal, no meu entendimento, é preciso validar antes de realizar uma alteração. Caso o valor seja o mesmo, ok, segue o barco. Caso seja diferente, segue um fluxo alternativo.

Mas assim não há a perda de informações?
Achei que o JPA lidava com esse tipo de caso.

Li também sobre o Lock otimista e o pessimista, nenhum deles resolveriam esse problema então, certo?

De novo: só existe concorrência se, ao mesmo tempo, dois ou mais acessos, tentam alterar o mesmo elemento.

Em termos de JPA seria cada uma das instâncias de cada um dos usuários abrir a transação e tentar aplicar alguma modificação, antes do commit. Isso, sim você trata com lock otimista ou pessimista.
Agora, se você já comitou a transação com o usuário X, o usuário Y não está concorrendo com ele.

Você quis dizer que o lock otimista ou pessimista resolveria meu problema?

Leia

1 curtida