Acessando múltiplos bancos de dados com JPA e CDI

É bem comum que uma aplicação precise acessar vários bancos de dados para prover suas funcionalidades. Um exemplo clássico dessa situação encontramos em empresas que possuem muitos sistemas, e para evitar ter tabelas e registros duplicados em várias bases de dados distintas, criam as chamadas bases corporativas, responsáveis por conter as informações comuns a todos os sistemas.

Supondo que temos uma aplicação Java, em um ambiente JavaEE, utilizando a JPA para fazer acesso aos bancos de dados, considere as seguintes configurações no arquivo persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

    <persistence-unit name="app" transaction-type="JTA">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <jta-data-source>java:/appDS</jta-data-source>
        
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!-- outras configuracoes... -->
        </properties>
    </persistence-unit>


    <persistence-unit name="corporativo" transaction-type="JTA">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <jta-data-source>java:/corporativoDS</jta-data-source>
        
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!-- outras configuracoes... -->
        </properties>
    </persistence-unit>
</persistence>

Repare que temos dois persistence-unit's configurados, sendo um chamado app, que utiliza o DataSource da nossa aplicação, e o outro chamado corporativo, este utilizando o DataSource corporativo.

Agora considere que temos a seguinte classe DAO em nossa aplicação:

public class ProdutoDao {

    private final EntityManager manager;

    public ProdutoDao(EntityManager manager) {
        this.manager = manager;
    }

    public List<Produto> todosOrdenadosPeloNome() {
        String jpql = "FROM Produto p ORDER BY p.nome";
        return manager.createQuery(jpql, Produto.class).getResultList();
    }

    //outros metodos...
}

Repare que a classe possui um construtor esperando como parâmetro uma instância de um EntityManager. Se nossa aplicação estiver utilizando também o CDI, podemos indicar que ele será o responsável por obter esta instância do EntityManager, com o uso da annotation @Inject:

public class ProdutoDao {

    private final EntityManager manager;

    @Inject
    public ProdutoDao(EntityManager manager) {
        this.manager = manager;
    }

    //restante do codigo...
}

Entretanto agora teremos um problema, pois o CDI não sabe como obter uma instância de um EntityManager. Podemos resolver este problema utilizando o recurso de Producer Method do CDI, que nada mais é do que um método anotado com @Produces, para ensiná-lo a como obter uma instância de um objeto do tipo EntityManager. Isto pode ser feito criando-se uma classe como a seguinte:

@ApplicationScoped
public class EntityManagerProducer implements Serializable {

    private static final long serialVersionUID = 1L;

    @PersistenceUnit
    private EntityManagerFactory factory;

    @RequestScoped
    @Produces
    public EntityManager createEntityManager() {
        return factory.createEntityManager();
    }

    public void closeEntityManager(@Disposes EntityManager manager) {
        if (manager.isOpen()) {
            manager.close();
        }
    }

}

Pronto! Agora o CDI sabe que deverá chamar o método createEntityManager para obter uma instância de um EntityManager, e que esta instância deverá ser mantida apenas pelo tempo de um request.

Note que no código anterior existe também um método chamado closeEntityManager, que possui um parâmetro do tipo EntityManager anotado com @Disposes. Isto indica ao CDI que este método deverá ser chamado sempre que ele for descartar uma instância de um EntityManager.

Mesmo assim ainda teremos problemas, pois utilizamos a annotation @PersistenceUnit para injetar uma instância de um EntityManagerFactory, mas como temos dois persistence-unit's configurados, devemos informar para qual persistence-unit a factory será criada. Além disso, nossa aplicação precisará acessar dois bancos de dados distintos, e sendo assim precisaremos de dois EntityManager's e duas EntityManagerFactory's distintas. Atualizando o código anterior teremos o seguinte:

@ApplicationScoped
public class EntityManagerProducer implements Serializable {

    private static final long serialVersionUID = 1L;

    @PersistenceUnit(unitName = "app")
    private EntityManagerFactory appFactory;

    @PersistenceUnit(unitName = "corporativo")
    private EntityManagerFactory corporativoFactory;

    @RequestScoped
    @Produces
    public EntityManager createAppEntityManager() {
        return appFactory.createEntityManager();
    }

    @RequestScoped
    @Produces
    public EntityManager createCorporativoEntityManager() {
        return corporativoFactory.createEntityManager();
    }

    public void closeEntityManager(@Disposes EntityManager manager) {
        if (manager.isOpen()) {
            manager.close();
        }
    }

}

E por fim um último problema a ser resolvido =D
Como temos dois métodos producer's de EntityManager's, ao tentar injetar uma instância de EntityManager em nossa classe DAO, o CDI não saberá qual dos dois métodos producer's deverá chamar, e com isso lançará uma Exception indicando o problema de ambiguidade.

Este problema pode ser resolvido utilizando outro recurso do CDI chamado Qualifier. Um qualifier nada mais é do que uma Annotation que funciona como uma espécie de Alias, para que assim possamos identificar qual producer deve ser chamado em um determinado ponto de injeção.

Criaremos uma annotation chamada @Corporativo, e para indicar que esta annotation é um qualifier do CDI, basta anotá-la com @Qualifier:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Qualifier
public @interface Corporativo {
}

Agora devemos usar nosso qualifier no método producer:

@ApplicationScoped
public class EntityManagerProducer implements Serializable {

    @RequestScoped
    @Produces
    @Default
    public EntityManager createAppEntityManager() {
        return appFactory.createEntityManager();
    }

    @RequestScoped
    @Produces
    @Corporativo
    public EntityManager createCorporativoEntityManager() {
        return corporativoFactory.createEntityManager();
    }

    //restante do codigo omitido...

}

Repare que adicionamos nosso qualifier no método createCorporativoEntityManager. Note também que no método createAppEntityManager adicionamos a annotation @Default, que nada mais é do que um qualifier do próprio CDI, e serve para indicar que este é o método producer a ser chamado quando não especificarmos um qualifier em algum ponto de injeção.

Agora a classe DAO funciona normalmente, sendo que nela será injetado o EntityManager que acessa o banco de dados da aplicação, pois nenhum qualifier foi utilizado. E para injetar o EntityManager que acessa o banco de dados corporativo, basta alterar o construtor da classe DAO adicionando o qualifier @Corporativo no parâmetro do construtor:

public class ProdutoDao {

    private final EntityManager manager;

    @Inject
    public ProdutoDao(@Corporativo EntityManager manager) {
        this.manager = manager;
    }

    //restante do codigo...
}

A injeção também pode ser feita via atributo, ao invés do construtor:

public class ProdutoDao {

    @Inject
    @Corporativo
    private EntityManager manager;

    //restante do codigo...
}

É possível também injetar os dois EntityManager's na mesma classe DAO:

public class ProdutoDao {

    @Inject
    @Corporativo
    private EntityManager corporativoManager;

    @Inject
    private EntityManager appManager;

    //restante do codigo...
}

Estes e outros recursos do CDI e JPA são apresentados em nossos cursos Web rica com JSF 2, Primefaces 4 e CDI e Persistência com JPA, Hibernate e EJB lite.

E você, já precisou utilizar os recursos Producer/Qualifier do CDI para acessar vários bancos de dados na mesma aplicação?

Tags: , , ,

64 Comentários

  1. IGOR OLIVEIRA 03/08/2016 at 16:00 #

    Olá! Ótimo texto muito bem explicado.

    Estou com um problema aqui um pouco diferente. Tenho uma aplicação que precisa acessar vários bancos de dados diferentes mas não ao mesmo tempo. No momento que o usuário for fazer o login o sistema deverá possibilitar a escolha de qual base será acessada.

    Alguma ideia para resolver este problema?

    Desde já obrigado.

  2. Edison L. Werle 18/10/2016 at 15:38 #

    olá!
    Utilizo desta forma aqui, mas estou com um problema com o hibernate 5 e vi que também usa ele. Não consigo pegar a sessão do hibernate com (Session) manager.unwrap(Session.class);
    Dá erro de Cast:
    java.lang.ClassCastException: org.jboss.weldx.persistence.EntityManager$873477057$Proxy$_$$_WeldClientProxy cannot be cast to org.hibernate.Session

    Preciso tirar o @RequestScoped do método produtor, ai funciona só que ai ele não funciona direito e não faz o Disposes .

    Tem alguma ideia de solução?

  3. Euclides 15/11/2016 at 14:54 #

    otimo post. Porém ao tentar utilizar a forma que você fez o meu EntityManagerFactory está vindo null, porém o entityManager não vem. Saberia como eu resolvo isso ?

  4. Antonio 26/07/2017 at 16:19 #

    Rodrigo Ferreira muito bom esse artigo. Agora me veio uma duvida: como vc configurou o DataSource? Se puder explicar melhor, agradeço.

  5. Rodrigo Ferreira 26/07/2017 at 16:44 #

    Oi Antonio,

    As configurações do DataSource em si dependem do servidor de aplicação e do banco de dados utilizado.

    No caso do exemplo utilizei o Wildfly e o MySQL, então as configs do datasource são feitas no Wildfly, no arquivo standalone/configuration/standalone.xml:

    <datasource jta="true" jndi-name="java:/appDS" pool-name="appDS" enabled="true" use-java-context="true">
        <connection-url>jdbc:mysql://localhost:3306/app</connection-url>
        <driver>com.mysql</driver>
        <security>
            <user-name>root</user-name>
            <password>root</password>
        </security>
    </datasource>
    
    <datasource jta="true" jndi-name="java:/corporativoDS" pool-name="corporativoDS" enabled="true" use-java-context="true">
        <connection-url>jdbc:mysql://localhost:3306/corporativo</connection-url>
        <driver>com.mysql</driver>
        <security>
            <user-name>root</user-name>
            <password>root</password>
        </security>
    </datasource>
    

    Aqui tem um tutorial de configuração de DataSources no Wildfly:
    http://www.ciceroednilson.com.br/configurando-data-source-no-wildfly-9-com-mysql/

    Abraços!

  6. Daniel 07/12/2017 at 17:35 #

    Excelente artigo! Mas fiquei com um problema ao implementar sua solução, bem elegante por sinal.

    Ao desenvolver o método close eu tomo o seguinte erro
    There is no producer method or producer field declared by the (same) bean class that is assignable to the disposed parameter of a disposer method [JSR-346 §3.5.3]

    @ApplicationScoped
    public class EntityManagerProducer {
    @PersistenceUnit(unitName = “e-c-cam-apo”)
    private EntityManagerFactory factoryCamapo;
    @PersistenceUnit(unitName = “e-c-b-comm”)
    private EntityManagerFactory factoryBComm;
    @PersistenceUnit(unitName = “e-c-b-web”)
    private EntityManagerFactory factoryBWeb;

    @RequestScoped
    @Produces
    @Camapo
    public EntityManager createcamapoEntityManager() {
    return factorycamapo.createEntityManager();
    }

    @RequestScoped
    @Produces
    @BComm
    public EntityManager createbCommEntityManager() {
    return factorybComm.createEntityManager();
    }

    @RequestScoped
    @Produces
    @BWeb
    public EntityManager createbWebEntityManager() {
    return factorybWeb.createEntityManager();
    }

    //There is no producer method or producer field declared by the (same) bean class that is assignable to the //disposed parameter of a disposer method [JSR-346 §3.5.3]
    public void close(@Disposes EntityManager manager) {
    if (manager.isOpen()) {
    manager.close();
    }
    }
    }

  7. Rodrigo Ferreira 08/12/2017 at 10:43 #

    Oi @Daniel,

    Esse erro acontece quando o servidor não encontra o @Produces do objeto que esta marcado como @Disposes.
    No seu caso, tem o metodo close recebendo o EntityManager como parametro, que esta anotado com @Disposes.
    Então é como se o servidor não tivesse encontrado os @Produces dos EntityManager’s.

    Verifica se por acaso você não importou o @Produces errado. O correto vem do pacote javax.enterprise.inject.

    Tem que tomar cuidado porque tem outro @Produces no java, mas do pacote javax.ws.rs, que é da especificação JAX-RS e não do CDI.

    Valeu!
    Abs!

  8. Daniel 08/12/2017 at 14:33 #

    Este é o problema.

    Os pacotes parecem estar todos OK

    import javax.enterprise.context.ApplicationScoped;
    import javax.enterprise.context.RequestScoped;
    import javax.enterprise.inject.Disposes;
    import javax.enterprise.inject.Produces;
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.PersistenceUnit;

    @ApplicationScoped
    public class EntityManagerProducer {
    @PersistenceUnit(unitName = “e-c-cam-apo”)
    private EntityManagerFactory factoryCamapo;
    @PersistenceUnit(unitName = “e-c-b-comm”)
    private EntityManagerFactory factoryBComm;
    @PersistenceUnit(unitName = “e-c-b-web”)
    private EntityManagerFactory factoryBWeb;

    @RequestScoped
    @Produces
    @Camapo
    public EntityManager createcamapoEntityManager() {
    return factorycamapo.createEntityManager();
    }

    @RequestScoped
    @Produces
    @BComm
    public EntityManager createbCommEntityManager() {
    return factorybComm.createEntityManager();
    }

    @RequestScoped
    @Produces
    @BWeb
    public EntityManager createbWebEntityManager() {
    return factorybWeb.createEntityManager();
    }

    //There is no producer method or producer field declared by the (same) bean class that is assignable to the //disposed parameter of a disposer method [JSR-346 §3.5.3]
    public void close(@Disposes EntityManager manager) {
    if (manager.isOpen()) {
    manager.close();
    }
    }
    }

    e o beans.xml está

    As anotações importam
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import javax.inject.Qualifier;

  9. Daniel 10/01/2018 at 14:59 #

    Boa tarde, é possível ter um relacionamento @OneToOne por exemplo com entidades de bancos diferentes?

    Abs

  10. Rodrigo Ferreira 10/01/2018 at 15:09 #

    Oi @Daniel, acredito que não, pois isso não é suportado pelos bancos de dados.

    Talvez até tenha algum banco de dados relacional que de suporte a isso, mas o problema é que seria algo específico apenas dele.

  11. Luiz 20/02/2018 at 17:18 #

    @Entity
    @Table(name = “pessoas”)
    public class Pessoas implements Serializable{

    private static final long serialVersionUID = 1L;

    Como mapear no modelo do objeto?

  12. Murilo 24/08/2018 at 17:19 #

    So tenho á agradecer !

  13. Samanta 12/04/2019 at 11:38 #

    Segui todas as orientações, porém estou com esse erro:

    GRAVE: Exceção ao enviar evento de contexto iniciado para instância listener da classe [org.jboss.weld.environment.servlet.Listener]
    org.jboss.weld.exceptions.DeploymentException: WELD-001409 Ambiguous dependencies for type [PessoaDAO] with qualifiers [@Default] at injection point [[field] @Inject private br.com.sistemagratis.converter.PessoaConverter.pessoaDAO]. Possible dependencies [[Managed Bean [class br.com.sistemagratis.dao.impl.ClienteDAOImpl] with qualifiers [@Default @Any], Managed Bean [class br.com.sistemagratis.dao.impl.PessoaDAOImpl] with qualifiers [@Default @Any]]]

  14. Rodrigo Ferreira 15/04/2019 at 13:56 #

    Oi @Samanta,

    O erro é porque em alguma classe faltou colocar a anotação do Quailifier. No meu exemplo do post seria a anotação @Corporativo.

    Dá uma conferida nas suas classes: PessoaConverter e PessoaDao sem tem algum atributo sendo injetado sem a anotação do qualifier.

    Ou talvez esteja faltando a anotação @Default, para indicar ao CDI qual instancia injetar se tiver um @Inject sem o qualifier.

    Abraço!

Deixe uma resposta