Lembre-se do uso de pits Spring Data JPA: sobre armazenamento em cache e instantâneos.

Ao corrigir um bug no projeto no grupo recentemente, foi descoberto que o bug foi causado pelo uso impróprio do Spring Data JPA. Depois de reparar o bug com sucesso, porque não sei muito sobre Spring Data JPA, pretendo escrever um blog sobre as informações relevantes que consultei no processo de resolução do bug. O conteúdo do blog é principalmente para iniciantes e o conteúdo é simples.
Simule primeiro o processo de geração do bug. A lógica do código a seguir pode ser um pouco inconsistente com a lógica do código que normalmente escrevemos, mas é importante entender a causa do bug por meio do código:

@Service
public class UserService {
    
    

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUser(String name) {
    
    
        User user = userRepository.findById(1).get();
        
        System.out.println("user name: " + user.getName());
        System.out.println("user age: " + user.getAge());
        System.out.println("name will be updated to " + name);
        
        userRepository.updateUserName(name, 1);
        
        User user1 = userRepository.findById(1).get();
        
        System.out.println("user1 age: " + user1.getAge());
        System.out.println("age will be updated to " + 18);
        user1.setAge(18);
        userRepository.save(user1);
    }
}
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    
    

    @Query(value = "update User set name = ?1 where id = ?2 ")
    @Modifying
    void updateUserName(String name, Integer id);
}

UserServiceA updateUser(String name)função original prevista do UserRepository.findById()método é: primeiro chame o método para descobrir o registro com id 1 na tabela do usuário e, em seguida, chame UserRepository.updateUserName(String name, Integer id)para modificar o valor da coluna de nome no registro com id 1 na tabela do usuário de acordo com o nome do parâmetro passado e, em seguida, chame o UserRepository.findById()método Consulte o registro com id 1 na tabela User depois que o nome for alterado e modifique o valor da coluna de idade do registro para 18. Mas o resultado após a execução do método é um pouco inesperado:
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
podemos descobrir que embora o registro com id 1 na tabela User seja modificado com sucesso para 18, o nome ainda é jack. Antes de explicar esse fenômeno, você precisa entender alguns conceitos sobre Spring Data JPA:

Cache de nível 1

O cache Spring Data JPA é usado quando 自定义Repositoryo find(), ou findxx()quando o método de gravação para consultar, a primeira consulta ao banco de dados irá então consultar os resultados serão armazenados na memória como um cache e, em seguida, consultar de volta diretamente para o mesmo buffer de gravação O resultado do retorno, não mais para consultar o banco de dados.
Insira a descrição da imagem aquiNo updateUser()log da execução do método acima , podemos ver que apenas uma selectinstrução foi executada , portanto, User user1 = userRepository.findById(1).get();essa instrução não executou uma consulta ao banco de dados. O UserRepository.updateUserName(name, 1)usuário1 é na verdade o usuário. O resultado dessa execução de código é modificar o id da tabela do usuário no banco de dados para 1. O valor do nome no registro e o valor do atributo de nome do objeto Usuário no cache ainda é "jack".

rubor()

flush()方法Sincronize as informações de status de todas as entidades modificadas no cache para o banco de dados. Ao usar o método save para atualizar uma entidade consultada no banco de dados dentro de uma transação, a instrução de atualização não será executada , mas a função de instantâneo será usada para sincronizar as informações de estado da entidade modificada no cache para o No banco de dados, se você deseja sincronizar as informações do estado da entidade modificada para o banco de dados antes que a transação seja confirmada, você deve save()chamar manualmente o flush()método após chamar o método para sincronizar a entidade modificada com o banco de dados, ou usar o saveAndFlush()método para salvar a entidade modificada (na verdade, o saveAndFlush()método está em Depois de chamar o save()método, chame o flush()método para sincronizar os dados).

Instantâneo

Além do cache de primeiro nível, Spring Data JPA também tem uma área de instantâneo. Quando os resultados da consulta são colocados no cache de primeiro nível, uma cópia dos dados será copiada para a área de instantâneo ao mesmo tempo. Spring Data JPA usa a área de instantâneo e os dados no cache Se é consistente determinar se os dados foram modificados após serem consultados no banco de dados.
No exemplo acima, ao executar User user = userRepository.findById(1).get();este código, uma zona de buffer e instantâneos são salvos enquanto uma instância do usuário, conforme mostrado abaixo:
Insira a descrição da imagem aqui
Quando o método foi executado user1.setAge(18);, as informações de estado do buffer e o instantâneo instanciam a região do usuário da seguinte forma:
Insira a descrição da imagem aqui
quando Quando a transação é confirmada, a fim de manter os dados do banco de dados sincronizados, o Hibernate irá limpar o cache de primeiro nível e determinar se os objetos no cache de primeiro nível são consistentes com os objetos no instantâneo de acordo com o valor do campo da chave primária. Se as propriedades dos dois objetos mudarem, execute A instrução de atualização sincroniza o conteúdo em cache com o banco de dados e atualiza o instantâneo; se forem consistentes, a instrução de atualização não é executada. Portanto, no log da execução do código acima, podemos ver que quando a updateUser()execução do método termina (o updateUser()método usa a anotação @Transactional, a transação será enviada após o término da execução do método), ela passará de acordo com o valor do atributo do usuário na área de cache (o nome é "jack", idade é 18) para modificar o registro do banco de dados correspondente (a instrução de atualização é impressa), fazendo com que UserRepository.updateUserName(name, 1)o efeito da execução do código seja sobrescrito.

Se você modificar UserServiceo updateUser()método em:

    @Transactional
    public void updateUser(String name) {
    
    
        User user = userRepository.findById(1).get();

        System.out.println("user name: " + user.getName());
        System.out.println("user age: " + user.getAge());
        System.out.println("name will be updated to " + name);

        user.setName(name);
        userRepository.save(user);

        User user1 = userRepository.findById(1).get();

        System.out.println("user1 age: " + user1.getAge());
        System.out.println("age will be updated to " + 18);
        user1.setAge(18);
        userRepository.save(user1);
    }

Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Pode-se ver que tanto o nome quanto a idade foram modificados com sucesso para "tom" e 18, isso porque definimos o nome do usuário que foi consultado pela primeira vez como "jack", e usuário1 e usuário são o mesmo objeto. Portanto, user1.setAge(18)o nome e a idade do usuário1 são "tom" e 18, respectivamente, após a execução . Ao mesmo tempo, pode-se descobrir que no log de execução de código acima, apenas uma instrução de atualização é impressa quando o método é executado. Isso também prova que o save()método não entra em vigor, mas depois que a transação é enviada, JPA usa a função de instantâneo para atualizar os dados no banco de dados. (Você pode remover as duas chamadas de método de salvamento e o resultado ainda é o mesmo).

Se userRepository.save(user);modificarmos o código acima para userRepository.saveAndFlush(user);depois, podemos ver que o método userRepository.saveAndFlush(user);imprime a instrução de atualização após a execução , conforme mostrado na figura abaixo:
Insira a descrição da imagem aqui
Mas deve-se notar que mesmo se você usar saveAndFlush()as informações de entidade modificadas para sincronizar com o banco de dados, mas se você estiver em uma transação Quando uma transação anormal é revertida durante a execução do método antes do commit, os dados no banco de dados serão revertidos de acordo.

Agora que você sabe onde está o problema, como resolvê-lo?

Solução: Defina o valor do atributo clearAutomatically na anotação @Modifying na instrução de atualização personalizada como true.

Definir a clearAutomaticallypropriedade como true significa que o cache de primeiro nível será esvaziado após a User user1 = userRepository.findById(1).get();execução da instrução de atualização personalizada . Essa instrução deve ser consultada novamente no banco de dados quando for executada para garantir que o valor do nome no objeto user1 seja atualizado O valor "tom". O código e o registro de execução são os seguintes:

@Service
public class UserService {
    
    

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUser(String name) {
    
    
        User user = userRepository.findById(1).get();

        System.out.println("user name: " + user.getName());
        System.out.println("user age: " + user.getAge());
        System.out.println("name will be updated to " + name);
        userRepository.updateUserName(name, 1);
        User user1 = userRepository.findById(1).get();

        System.out.println("user1 age: " + user1.getAge());
        System.out.println("age will be updated to " + 18);
        user1.setAge(18);
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    
    

    @Query(value = "update User set name = ?1 where id = ?2 ")
    @Modifying(clearAutomatically = true)
    void updateUserName(String name, Integer id);
}

Insira a descrição da imagem aqui

Resumo
Ao usar Spring Data JPA para operações de atualização, preste atenção ao uso de transações e à sincronização de dados entre o buffer e o banco de dados.
Ao Repositoryherdar uma classe personalizada JpaRepository<T, ID>, você descobrirá que, ao chamar um método personalizado Repositorydo savetempo, a implementação real é JpaRepository<T, ID>uma subclasse SimpleJpaRepository<T, ID>do savemétodo, o @Transactionalnível de isolamento anotado do método é o valor padrão Propagation.REQUIRED, portanto, se você servicecamada de métodos Quando o savemétodo do chamador salva as informações de estado do objeto de entidade modificado no banco de dados,

  1. Se serviceo método falhar @Transactionalem abrir uma transação, o savemétodo iniciará uma transação.Quando o savemétodo é executado e a transação é confirmada, as informações do objeto modificadas pela instrução SQL serão sincronizadas com o banco de dados.
  2. Se serviceo método for @Transactionalabrir uma transação, savea execução não salvará os dados no banco de dados (nenhuma instrução SQL é executada), mas sincronizará os dados no banco de dados por meio da função de instantâneo após a confirmação da transação.

flush()O método é usado para sincronizar os dados no buffer para o banco de dados.No JPA, quando uma transação é enviada, o JPA chama automaticamente o flush()método para sincronizar os dados com o banco de dados e limpar o cache. No entanto, deve-se notar que antes de executar a instrução SQL customizada, se os dados no cache forem inconsistentes com os dados na captura instantânea, porque o conteúdo da instrução executada não é conhecido, o JPA chamará automaticamente para manter o banco de dados e os dados em cache consistentes. flush()Método para sincronizar os dados do cache com o banco de dados. Veja o seguinte código como exemplo:

@Service
public class UserService {
    
    

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUser(String name) {
    
    
        User user = userRepository.findById(1).get();
        System.out.println("user age is " + user.getAge());
        user.setAge(18);
        System.out.println("user age change to 18");
        userRepository.updateUserName(name, 1);
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    
    

    @Query(value = "update User set name = ?1 where id = ?2 ")
    @Modifying
    void updateUserName(String name, Integer id);
}

O log de execução do código é o seguinte:
Insira a descrição da imagem aqui
Pode-se ver que antes userRepository.updateUserName(name, 1);deste código ser executado , porque o valor de idade do objeto do usuário no cache foi modificado, o Spring Data JPA flush()atualiza as informações do objeto no cache para o banco de dados por meio de um método.

Spring Data JPA encapsula muitos métodos práticos para programadores. Os programadores podem facilmente usar Spring Data JPA para escrever código de camada de acesso a dados, mas às vezes, a estrutura faz muito por nós, mas se torna uma desvantagem, porque quando nós Quando o mecanismo do framework não é compreendido, os métodos fornecidos pelo framework serão usados ​​incorretamente, o que levará a erros, que às vezes são difíceis de encontrar através da depuração. Portanto, ao usar uma estrutura, você deve ter uma compreensão adequada de seu mecanismo.

PS: Eu não escrevo um blog há muito tempo. Levei um dia para escrever este blog. Com certeza, escrever esse tipo de coisa leva tempo para praticar. Eu me culpo por ser muito preguiçoso.

Acho que você gosta

Origin blog.csdn.net/weixin_40759863/article/details/109273688
Recomendado
Clasificación