Java - JPA - anotação @Version

118

Como a @Versionanotação funciona no JPA?

Encontrei várias respostas cujo extrato é o seguinte:

JPA usa um campo de versão em suas entidades para detectar modificações simultâneas no mesmo registro de armazenamento de dados. Quando o tempo de execução JPA detecta uma tentativa de modificar simultaneamente o mesmo registro, ele lança uma exceção para a última tentativa de confirmação da transação.

Mas ainda não tenho certeza de como funciona.


Também a partir das seguintes linhas:

Você deve considerar os campos de versão imutáveis. A alteração do valor do campo tem resultados indefinidos.

Isso significa que devemos declarar nosso campo de versão como final?

Yatendra Goel
fonte
1
Tudo que faz é verificar / atualizar a versão em cada consulta de actualização: UPDATE myentity SET mycolumn = 'new value', version = version + 1 WHERE version = [old.version]. Se alguém atualizou o registro, old.versionnão corresponderá mais ao do banco de dados e a cláusula where impedirá que a atualização aconteça. As 'linhas atualizadas' serão 0, que JPA pode detectar para concluir que uma modificação simultânea aconteceu.
Stijn de Witt,

Respostas:

189

Mas ainda não tenho certeza de como funciona?

Digamos que uma entidade MyEntitytenha uma versionpropriedade anotada :

@Entity
public class MyEntity implements Serializable {    

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Version
    private Long version;

    //...
}

Na atualização, o campo anotado com @Versionserá incrementado e adicionado à WHEREcláusula, algo assim:

UPDATE MYENTITY SET ..., VERSION = VERSION + 1 WHERE ((ID = ?) AND (VERSION = ?))

Se a WHEREcláusula não corresponder a um registro (porque a mesma entidade já foi atualizada por outro encadeamento), o provedor de persistência lançará um OptimisticLockException.

Isso significa que devemos declarar nosso campo de versão como final

Não, mas você pode considerar a possibilidade de tornar o setter protegido como você não deveria chamá-lo.

Pascal Thivent
fonte
5
Recebi uma exceção de ponteiro nulo com este trecho de código, porque Long inicializa como nulo, provavelmente deve ser inicializado explicitamente com 0L.
Markus
2
Não confie no desempacotamento automático longou apenas longValue()em uma chamada . Você precisa de nullverificações explícitas . Inicializando-o explicitamente por conta própria, eu não faria, pois esse campo deve ser gerenciado pelo provedor ORM. Acho que é definido como 0Lna primeira inserção no banco de dados, então se for definido como nullisso indica que este registro ainda não foi persistido.
Stijn de Witt
Isso impedirá a criação de uma entidade que está sendo (tentada) criada mais de uma vez? Quero dizer, suponha que uma mensagem de tópico JMS aciona a criação de uma entidade e há várias instâncias de um aplicativo que escuta a mensagem. Eu só quero evitar o erro de violação de restrição exclusivo.
Manu
Desculpe, percebi que este é um tópico antigo, mas @Version também leva em consideração cache sujo em ambientes em cluster?
Shawn Eion Smith
3
Se estiver usando Spring Data JPA, pode ser melhor usar um wrapper em Longvez de uma primitiva longpara o @Versioncampo, porque SimpleJpaRepository.save(entity)provavelmente tratará um campo de versão nula como um indicador de que a entidade é nova. Se a entidade for nova (conforme indicado pelo fato de a versão ser nula), o Spring irá chamar em.persist(entity). Mas se a versão tiver um valor, ela será chamada em.merge(entity). É também por isso que um campo de versão declarado como um tipo de wrapper deve ser deixado não inicializado.
rdguam
33

Embora a resposta @Pascal seja perfeitamente válida, por experiência própria considero o código abaixo útil para realizar o bloqueio otimista:

@Entity
public class MyEntity implements Serializable {    
    // ...

    @Version
    @Column(name = "optlock", columnDefinition = "integer DEFAULT 0", nullable = false)
    private long version = 0L;

    // ...
}

Por quê? Porque:

  1. O bloqueio otimista não funcionará se o campo anotado com @Versionfor acidentalmente definido como null.
  2. Como esse campo especial não é necessariamente uma versão comercial do objeto, para evitar enganos, prefiro nomear esse campo como algo semelhante optlocka version.

O primeiro ponto não importa se o aplicativo usa apenas JPA para inserir dados no banco de dados, pois o fornecedor de JPA aplicará 0para o @versioncampo no momento da criação. Mas quase sempre instruções SQL simples também estão em uso (pelo menos durante os testes de unidade e integração).

G. Demecki
fonte
Eu chamo isso de u_lmod(usuário modificado pela última vez), em que o usuário pode ser um processo / entidade / aplicativo humano ou definido (automatizado) (portanto, o armazenamento adicional também u_lmod_idpode fazer sentido). (É na minha opinião um metaatributo de negócios muito básico). Normalmente, ele armazena seu valor como DATA (HORA) (significando informações sobre o ano ... milis) em UTC (se a "localização" do fuso horário do editor for importante armazená-la junto com o fuso horário). adicionalmente, muito útil para cenários de sincronização DWH.
Andreas Dietrich
7
Acho que bigint em vez de um inteiro deve ser usado no tipo db para corresponder a um tipo longo de java.
Dmitry
Bom ponto, Dmitry, obrigado, embora quando se trata de versionamento de IMHO isso realmente não importe.
G. Demecki
9

Cada vez que uma entidade é atualizada no banco de dados, o campo de versão será aumentado em um. Cada operação que atualiza a entidade no banco de dados será anexada WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASEà sua consulta.

Ao verificar as linhas afetadas de sua operação, a estrutura jpa pode certificar-se de que não houve modificação simultânea entre carregar e persistir sua entidade, porque a consulta não encontraria sua entidade no banco de dados quando seu número de versão foi aumentado entre carregar e persistir.

estefanglase
fonte
Não via JPAUpdateQuery, não. Portanto, "Cada operação que atualiza a entidade no banco de dados terá anexado WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASE à sua consulta" não é verdade.
Pedro Borges
2

Versão usada para garantir que apenas uma atualização de cada vez. O provedor JPA verificará a versão, se a versão esperada já aumentar, então alguém já atualizou a entidade, então uma exceção será lançada.

Portanto, atualizar o valor da entidade seria mais seguro, mais otimista.

Se o valor muda frequentemente, você pode considerar não usar o campo de versão. Por exemplo, "uma entidade que possui campo contador, que aumentará cada vez que uma página da web for acessada"

uudashr
fonte
0

Apenas adicionando um pouco mais de informação.

JPA gerencia a versão por baixo do capô para você, no entanto, não o faz quando você atualiza seu registro via JPAUpdateClause; nesses casos, você precisa adicionar manualmente o incremento de versão à consulta.

O mesmo pode ser dito sobre a atualização via JPQL, ou seja, não uma simples mudança na entidade, mas um comando de atualização no banco de dados, mesmo que seja feito pelo hibernate

Pedro

Pedro Borges
fonte
3
O que é JPAUpdateClause?
Karl Richter
Boa pergunta, a resposta simples é: mau exemplo :) JPAUpdateClause é específico de QueryDSL, pense em executar uma atualização com JPQL em vez de simplesmente afetar o estado de uma entidade no código.
Pedro Borges,