web-dev-qa-db-fra.com

Spring Boot Data JPA - Modification de la requête de mise à jour - Actualisation du contexte de persistance

Je travaille avec Spring Boot 1.3.0.M4 et une base de données MySQL.

J'ai un problème lors de l'utilisation de requêtes de modification, EntityManager contient des entités obsolètes après l'exécution de la requête.

Référentiel JPA d'origine:

public interface EmailRepository extends JpaRepository<Email, Long> {

    @Transactional
    @Modifying
    @Query("update Email e set e.active = false where e.active = true and e.expire <= NOW()")
    Integer deactivateByExpired();

}

Supposons que nous ayons Email [id = 1, actif = true, expire = 2015/01/01] dans DB.

Après l'exécution:

emailRepository.save(email);
emailRepository.deactivateByExpired();
System.out.println(emailRepository.findOne(1L).isActive()); // prints true!! it should print false

Première approche pour résoudre le problème: ajouter clearAutomatically = true

public interface EmailRepository extends JpaRepository<Email, Long> {

    @Transactional
    @Modifying(clearAutomatically = true)
    @Query("update Email e set e.active = false where e.active = true and e.expire <= NOW()")
    Integer deactivateByExpired();

}

Cette approche efface le contexte de persistance pour ne pas avoir de valeurs obsolètes, mais elle supprime toutes les modifications non vidées encore en attente dans EntityManager. Comme j'utilise uniquement les méthodes save() et non saveAndFlush() certaines modifications sont perdues pour d'autres entités :(


Deuxième approche pour résoudre le problème: implémentation personnalisée pour le référentiel

public interface EmailRepository extends JpaRepository<Email, Long>, EmailRepositoryCustom {

}

public interface EmailRepositoryCustom {

    Integer deactivateByExpired();

}

public class EmailRepositoryImpl implements EmailRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    @Override
    public Integer deactivateByExpired() {
        String hsql = "update Email e set e.active = false where e.active = true and e.expire <= NOW()";
        Query query = entityManager.createQuery(hsql);
        entityManager.flush();
        Integer result = query.executeUpdate();
        entityManager.clear();
        return result;
    }

}

Cette approche fonctionne de manière similaire à @Modifying(clearAutomatically = true) mais elle oblige d'abord EntityManager à vider toutes les modifications apportées à DB avant d'exécuter la mise à jour, puis elle efface le contexte de persistance. De cette façon, il n'y aura pas d'entités obsolètes et toutes les modifications seront enregistrées dans la base de données.


Je voudrais savoir s'il existe un meilleur moyen d'exécuter des instructions de mise à jour dans JPA sans avoir le problème des entités obsolètes et sans le vidage manuel vers DB. Peut-être désactiver le cache de 2e niveau? Comment puis-je le faire dans Spring Boot?


Mise à jour 2018

Spring Data JPA a approuvé mon PR, il y a maintenant une option flushAutomatically dans @Modifying().

@Modifying(flushAutomatically = true, clearAutomatically = true)
47
ciri-cuervo

Je sais que ce n'est pas une réponse directe à votre question, car vous avez déjà créé un correctif et lancé une demande d'extraction sur Github. Merci pour ça!

Mais je voudrais expliquer la façon dont vous pouvez procéder par JPA. Vous souhaitez donc modifier toutes les entités qui correspondent à un critère spécifique et mettre à jour une valeur pour chacune. L'approche normale consiste simplement à charger toutes les entités nécessaires:

@Query("SELECT * FROM Email e where e.active = true and e.expire <= NOW()")
List<Email> findExpired();

Ensuite, parcourez-les et mettez à jour les valeurs:

for (Email email : findExpired()) {
  email.setActive(false);
}

Hibernate connaît maintenant toutes les modifications et les écrit dans la base de données si la transaction est effectuée ou si vous appelez EntityManager.flush() manuellement. Je sais que cela ne fonctionnera pas bien si vous avez une grande quantité d'entrées de données, car vous chargez toutes les entités en mémoire. Mais c'est la meilleure façon de synchroniser le cache d'entité en veille prolongée, les caches de 2e niveau et la base de données.

Est-ce que cette réponse dit "l'annotation" @ Modification "est inutile"? Non! Si vous vous assurez que les entités modifiées ne sont pas dans votre cache local, par exemple application en écriture seule, cette approche est juste le chemin à parcourir.

Et pour mémoire: vous n'avez pas besoin de @Transactional Sur vos méthodes de référentiel.

Juste pour l'enregistrement v2: la colonne active ressemble car elle a une dépendance directe à expire. Alors pourquoi ne pas supprimer active complètement et ne regarder que expire dans chaque requête?

8
Johannes Leimer

Comme l'a dit klaus-groenbaek, vous pouvez injecter EntityManager et utiliser sa méthode de rafraîchissement:

@Inject
EntityManager entityManager;

...

emailRepository.save(email);
emailRepository.deactivateByExpired();
Email email2 = emailRepository.findOne(1L);
entityManager.refresh(email2);
System.out.println(email2.isActive()); // prints false
1
Eric Bonnot