Como atualizar entidades JPA quando o database de back-end é alterado de forma assíncrona?

Eu tenho um database PostgreSQL 8.4 com algumas tabelas e visualizações que são essencialmente unidas em algumas das tabelas. Usei o NetBeans 7.2 (conforme descrito aqui ) para criar serviços baseados em REST derivados dessas visualizações e tabelas e os implementei em um servidor Glassfish 3.1.2.2.

Há outro processo que atualiza de forma assíncrona o conteúdo em algumas das tabelas usadas para construir as visualizações. Eu posso consultar diretamente as visualizações e tabelas e ver essas alterações ocorreram corretamente. No entanto, quando extraídos dos serviços baseados em REST, os valores não são os mesmos que os do database. Estou assumindo que isso acontece porque o JPA armazenou em cache cópias locais do conteúdo do database no servidor Glassfish e o JPA precisa atualizar as entidades associadas.

Eu tentei adicionar alguns methods à class AbstractFacade que o NetBeans gera:

public abstract class AbstractFacade { private Class entityClass; private String entityName; private static boolean _refresh = true; public static void refresh() { _refresh = true; } public AbstractFacade(Class 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; } } ... } 

Em seguida, chamo doRefresh() de cada um dos methods de find que o NetBeans gera. O que normalmente acontece é que a IllegalArgumentsException é lançada informando algo como 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].

Por isso, estou procurando algumas sugestões sobre como atualizar corretamente as entidades associadas às exibições para que estejam atualizadas.

UPDATE: Acontece que meu entendimento do problema subjacente não estava correto. É algo relacionado a outra pergunta que eu postei anteriormente , a saber, a visão não tinha um único campo que pudesse ser usado como um identificador único. O NetBeans requer que eu selecione um campo de ID, então escolhi apenas uma parte do que deveria ser uma chave de várias partes. Isso exibiu o comportamento de que todos os registros com um campo de ID específico eram idênticos, embora o database tivesse registros com o mesmo campo de ID, mas o restante era diferente. A JPA não foi além de olhar para o que eu disse que era o identificador único e simplesmente puxou o primeiro registro encontrado.

Resolvi isso adicionando um campo de identificador exclusivo (nunca consegui fazer com que a chave multiparte funcionasse corretamente).

Recomendo adicionar uma class @Startup @Singleton que estabeleça uma conexão JDBC com o database PostgreSQL e use LISTEN e NOTIFY para lidar com a invalidação de cache.

Atualização : Aqui está outra abordagem interessante, usando pgq e uma coleção de trabalhadores para invalidação .

Sinalização de Invalidação

Adicione um gatilho na tabela que está sendo atualizada que envia um NOTIFY sempre que uma entidade é atualizada. No PostgreSQL 9.0 e acima, o NOTIFY pode conter uma carga útil, geralmente um ID de linha, para que você não precise invalidar todo o seu cache, apenas a entidade que foi alterada. Em versões mais antigas, em que uma carga útil não é suportada, você pode adicionar as inputs invalidadas a uma tabela de log de registro de data e hora que sua class auxiliar consulta quando recebe um NOTIFY ou apenas invalida todo o cache.

Sua class auxiliar agora LISTEN nos events NOTIFY que o gatilho envia. Quando ele recebe um evento NOTIFY , ele pode invalidar inputs de cache individuais (veja abaixo) ou esvaziar todo o cache. Você pode ouvir as notifications do database com o suporte a escutar / notificar do PgJDBC . Você precisará desembrulhar qualquer java.sql.Connection gerenciado pelo org.postgresql.PGConnection getNotifications() para obter a implementação subjacente do PostgreSQL para que você possa org.postgresql.PGConnection -lo em org.postgresql.PGConnection e chamar getNotifications() nele.

Uma alternativa para LISTEN e NOTIFY , você poderia pesquisar uma tabela de log de alterações em um timer e fazer com que um acionador na tabela de problemas anexasse IDs de linha alterados e alterasse os timestamps para a tabela de log de alterações. Essa abordagem será portátil, exceto pela necessidade de um acionador diferente para cada tipo de database, mas é ineficiente e menos pontual. Isso exigirá pesquisas freqüentes e ineficientes, e ainda terá um atraso de tempo que a abordagem listen / notify não faz. No PostgreSQL você pode usar uma tabela UNLOGGED para reduzir um pouco os custos dessa abordagem.

Níveis de cache

O EclipseLink / JPA possui alguns níveis de armazenamento em cache.

O cache de primeiro nível está no nível do EntityManager . Se uma entidade estiver conectada a um EntityManager por persist(...) , merge(...) , find(...) , etc, o EntityManager será obrigado a retornar a mesma instância dessa entidade quando for acessada novamente dentro da mesma session, quer sua inscrição ainda tenha ou não referências a ela. Esta instância anexada não estará atualizada se o conteúdo do database tiver sido alterado desde então.

O cache de segundo nível, que é opcional, está no nível EntityManagerFactory e é um cache mais tradicional. Não está claro se você tem o cache de segundo nível ativado. Verifique seus logs do EclipseLink e seu persistence.xml . Você pode obter access ao cache de segundo nível com EntityManagerFactory.getCache() ; veja Cache .

@thedayofcondor mostrou como limpar o cache do segundo nível com:

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

mas você também pode despejar objects individuais com a chamada evict(java.lang.Class cls, java.lang.Object primaryKey) :

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

que você pode usar do seu ouvinte @Startup @Singleton NOTIFY para invalidar apenas as inputs que foram alteradas.

O cache de primeiro nível não é tão fácil, porque faz parte da lógica do seu aplicativo. Você vai querer aprender sobre como o EntityManager , entidades anexadas e desanexadas, etc funcionam. Uma opção é sempre usar entidades desanexadas para a tabela em questão, em que você usa um novo EntityManager sempre que buscar a entidade. Essa questão:

Invalidando a session JPA EntityManager

tem uma discussão útil sobre como lidar com a invalidação do cache do gerenciador de entidades. No entanto, é improvável que um cache do EntityManager seja um problema, porque um serviço da web RESTful geralmente é implementado usando sessões curtas do EntityManager . Isso provavelmente só será um problema se você estiver usando contextos de persistência estendidos ou se estiver criando e gerenciando suas próprias sessões EntityManager vez de usar a persistência gerenciada por contêiner.

Você pode desativar completamente o armazenamento em cache (consulte: http://wiki.eclipse.org/EclipseLink/FAQ/How_to_disable_the_shared_cache%3F ), mas prepare-se para uma perda de desempenho bastante grande.

Caso contrário, você pode executar um cache claro programaticamente com

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

Você pode mapeá-lo para um servlet para que você possa chamá-lo externamente – isso é melhor se o seu database é modificar externamente muito raramente e você só quer ter certeza de que o JPS vai pegar a nova versão

Apenas um pensamento, mas como você recebe seu EntityManager / Session / whatever?

Se você consultou a entidade em uma session, ela será desanexada na próxima e você precisará mesclá-la novamente no contexto de persistência para obtê-la novamente.

Tentar trabalhar com entidades desanexadas pode resultar em exceções não gerenciadas, você deve consultar novamente a entidade ou tentar com a mesclagem (ou methods semelhantes).

O JPA não faz nenhum cache por padrão. Você precisa configurá-lo explicitamente. Eu acredito que é o efeito colateral do estilo arquitetônico que você escolheu: REST. Eu acho que o cache está acontecendo nos servidores web, servidores proxy, etc. Eu sugiro que você leia isso e depure mais.