web-dev-qa-db-fra.com

Comment actualiser les entités JPA lorsque la base de données principale change de manière asynchrone?

J'ai une base de données PostgreSQL 8.4 avec des tables et des vues qui sont essentiellement des jointures sur certaines des tables. J'ai utilisé NetBeans 7.2 (comme décrit dans ici ) pour créer des services REST dérivés de ces vues et tables et les ai déployés sur un serveur Glassfish 3.1.2.2.

Un autre processus met à jour de manière asynchrone le contenu de certaines des tables utilisées pour générer les vues. Je peux directement interroger les vues et les tables et voir que ces modifications ont eu lieu correctement. Toutefois, une fois extraites des services basés sur REST, les valeurs ne sont pas les mêmes que celles de la base de données. Je suppose que cela est dû au fait que JPA a mis en cache des copies locales du contenu de la base de données sur le serveur Glassfish et que JPA doit actualiser les entités associées.

J'ai essayé d'ajouter quelques méthodes à la classe AbstractFacade générée par NetBeans:

public abstract class AbstractFacade<T> {
    private Class<T> entityClass;
    private String entityName;
    private static boolean _refresh = true;

    public static void refresh() { _refresh = true; }

    public AbstractFacade(Class<T> entityClass) {
        this.entityClass = entityClass;
        this.entityName = entityClass.getSimpleName();
    }

    private void doRefresh() {
        if (_refresh) {
            EntityManager em = getEntityManager();
            em.flush();

            for (EntityType<?> entity : em.getMetamodel().getEntities()) {
                if (entity.getName().contains(entityName)) {
                    try {
                        em.refresh(entity);
                        // log success
                    }
                    catch (IllegalArgumentException e) {
                        // log failure ... typically complains entity is not managed
                    }
                }
            }

            _refresh = false;
        }
    }

...

}

J'appelle ensuite doRefresh() à partir de chacune des méthodes find générées par NetBeans. Ce qui se produit normalement, c’est que la variable IllegalArgumentsException est lancée en indiquant quelque chose comme Can not refresh not managed object: EntityTypeImpl@28524907:MyView [ javaType: class org.my.rest.MyView descriptor: RelationalDescriptor(org.my.rest.MyView --> [DatabaseTable(my_view)]), mappings: 12].

Je recherche donc des suggestions sur la manière d'actualiser correctement les entités associées aux vues afin qu'elles soient à jour.

METTRE À JOUR: Il s'avère que ma compréhension du problème sous-jacent n'était pas correcte. C'est un peu lié à une autre question que j'ai posée plus tôt , à savoir que la vue n'avait pas de champ unique pouvant être utilisé comme identifiant unique. NetBeans exigeait que je sélectionne un champ ID. Je n'ai donc choisi qu'une partie de ce qui aurait dû être une clé à plusieurs parties. Cela montrait que tous les enregistrements avec un champ ID particulier étaient identiques, même si la base de données avait des enregistrements avec le même champ ID mais que le reste était différent. JPA ne s'est pas contentée de regarder ce que j'avais dit comme identifiant unique et avait simplement extrait le premier enregistrement trouvé.

J'ai résolu ce problème en ajoutant un champ identifiant unique (jamais pu obtenir la clé multipart pour fonctionner correctement).

31
andand

Je recommande l'ajout d'une classe @Startup@Singleton qui établit une connexion JDBC vers la base de données PostgreSQL et utilise LISTEN ET NOTIFY pour gérer l'invalidation du cache.

Update: Voici une autre approche intéressante, utilisant pgq et un ensemble d'ouvriers pour invalidation }.

Signalisation d'invalidation

Ajoutez un déclencheur sur la table en cours de mise à jour qui envoie un NOTIFY à chaque mise à jour d’une entité. Sur PostgreSQL 9.0 et les versions ultérieures, NOTIFY peut contenir une charge utile, généralement un ID de ligne, de sorte que vous n'avez pas à invalider l'intégralité de votre cache, mais uniquement l'entité qui a été modifiée. Sur les anciennes versions où les données utiles ne sont pas prises en charge, vous pouvez ajouter les entrées non validées à une table de journal horodatée que votre classe d'assistance interroge lorsqu'elle obtient un NOTIFY ou invalider tout le cache.

Votre classe d'assistance maintenant LISTENs sur les événements NOTIFY envoyés par le déclencheur. Lorsqu'il obtient un événement NOTIFY, il peut invalider des entrées de cache individuelles (voir ci-dessous) ou vider tout le cache. Vous pouvez écouter les notifications de la base de données avec le support technique listen/notify de PgJDBC . Vous devrez déballer tout Java.sql.Connection de pooler de connexions géré pour accéder à l'implémentation PostgreSQL sous-jacente afin de pouvoir le transtyper en org.postgresql.PGConnection et d'appeler getNotifications() dessus.

En guise d'alternative à LISTEN et NOTIFY, vous pouvez interroger une table de journal des modifications sur un minuteur et demander à un déclencheur sur la table des problèmes d'ajouter les ID de ligne modifiés et de modifier les horodatages dans la table de journal des modifications. Cette approche sera portable, à l'exception du besoin d'un déclencheur différent pour chaque type de base de données, mais elle est inefficace et moins rapide. Cela nécessitera des interrogations fréquentes et inefficaces, tout en laissant un délai supplémentaire par rapport à l'approche écoute/notification. Dans PostgreSQL, vous pouvez utiliser une table UNLOGGED pour réduire un peu les coûts de cette approche.

Niveaux de cache

EclipseLink/JPA a plusieurs niveaux de mise en cache.

Le cache de premier niveau est au niveau EntityManager . Si une entité est attachée à un EntityManager par persist(...), merge(...), find(...), etc., le EntityManager doit renvoyer la même instance de cette entité lorsqu’elle est à nouveau accédée au cours de la même session, que votre application soit ou non a des références à cela. Cette instance attachée ne sera pas à jour si le contenu de votre base de données a été modifié depuis.

Le cache de second niveau, facultatif, se trouve au niveau EntityManagerFactory et constitue un cache plus traditionnel. Il n'est pas clair si le cache de second niveau est activé. Vérifiez vos journaux EclipseLink et votre persistence.xml. Vous pouvez accéder au cache de second niveau avec EntityManagerFactory.getCache(); voir Cache .

@thedayofcondor a montré comment vider le cache de second niveau avec:

em.getEntityManagerFactory().getCache().evictAll();

mais vous pouvez aussi expulser des objets individuels avec l'appel evict(Java.lang.Class cls, Java.lang.Object primaryKey) :

em.getEntityManagerFactory().getCache().evict(theClass, thePrimaryKey);

que vous pouvez utiliser à partir de votre écouteur @Startup@SingletonNOTIFY pour invalider uniquement les entrées modifiées.

Le cache de premier niveau n'est pas si facile, car il fait partie de la logique de votre application. Vous voudrez en savoir plus sur le fonctionnement de EntityManager, des entités attachées et détachées, etc. Une option consiste à toujours utiliser des entités détachées pour la table en question, où vous utilisez un nouveau EntityManager chaque fois que vous extrayez l'entité. Cette question:

(Session JPA EntityManager invalide } _

a une discussion utile sur la gestion de l’invalidation du cache du gestionnaire d’entités. Toutefois, il est peu probable qu'un problème se pose avec un cache EntityManager, car un service Web RESTful est généralement implémenté à l'aide de courtes sessions EntityManager. Cela ne devrait poser problème que si vous utilisez des contextes de persistance étendue ou si vous créez et gérez vos propres sessions EntityManager plutôt que d'utiliser la persistance gérée par le conteneur.

50
Craig Ringer

Vous pouvez soit désactiver entièrement la mise en cache (voir: http://wiki.Eclipse.org/EclipseLink/FAQ/How_to_disable_the_shared_cache%3F ), mais soyez prêt à subir une perte de performances assez importante.

Sinon, vous pouvez effectuer un nettoyage du cache par programmation avec 

em.getEntityManagerFactory().getCache().evictAll();

Vous pouvez le mapper sur un servlet de manière à pouvoir l'appeler de manière externe. Cette option est préférable si votre base de données est modifiée en externe très rarement et que vous voulez simplement être sûr que JPS récupérera la nouvelle version.

8
thedayofcondor

Juste une pensée, mais comment recevez-vous votre EntityManager/Session/peu importe? 

Si vous avez interrogé l'entité au cours d'une session, elle sera détachée lors de la suivante et vous devrez la réintégrer dans le contexte de persistance pour qu'elle soit à nouveau gérée. 

Essayer de travailler avec des entités détachées peut entraîner des exceptions non gérées. Vous devez interroger à nouveau l'entité ou l'essayer avec la fusion (ou des méthodes similaires).

0
Volker