Preloader image

A API de fonte de dados dinâmica do TomEE visa permitir o uso de várias fontes de dados como se fosse uma do ponto de vista do aplicativo.

Pode ser útil por razões técnicas (balanceamento de carga, por exemplo) ou razões geralmente mais funcionais (filtragem, agregação, enriquecimento …​). No entanto, observe que você pode escolher apenas uma fonte de dados por transação. Isso significa que o objetivo desse recurso não é alternar mais de uma vez fonte de dados em uma transação. O código a seguir não funcionará:

@Stateless
public class MyEJB {
    @Resource private MyRouter router;
    @PersistenceContext private EntityManager em;

    public void workWithDataSources() {
        router.setDataSource("ds1");
        em.persist(new MyEntity());

        router.setDataSource("ds2"); // mesma transação -> esta invocação não funciona
        em.persist(new MyEntity());
    }
}

Neste exemplo, a implementação simplesmente usa uma fonte de dados de seu nome e precisa ser definido antes de usar qualquer operação JPA na transação (para manter a lógica simples no exemplo).

A implementação do roteador

Nosso roteador possui dois parâmetros de configuração: uma lista de nomes jndi representando fontes de dados para usar uma fonte de dados padrão para usar

Implementação de roteador

A interface Router (org.apache.openejb.resource.jdbc.Router) tem somente um método para implementar, public DataSource getDataSource()

Nossa implementação DeterminedRouter usa um ThreadLocal para gerenciar a fonte de dados usada atualmente. Lembre-se de que a JPA usou mais de uma vez o Método getDatasource() para uma operação. Alterar a fonte de dados em uma transação é perigosa e deve ser evitada.

package org.superbiz.dynamicdatasourcerouting;

import org.apache.openejb.resource.jdbc.AbstractRouter;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class DeterminedRouter extends AbstractRouter {
    private String dataSourceNames;
    private String defaultDataSourceName;
    private Map<String, DataSource> dataSources = null;
    private ThreadLocal<DataSource> currentDataSource = new ThreadLocal<DataSource>();

    /**
     * @param datasourceList nome do recurso de origem de dados, separador é um espaço
     */
    public void setDataSourceNames(String datasourceList) {
        dataSourceNames = datasourceList;
    }

    /**
     * fonte de dados de pesquisa em recursos openejb
     */
    private void init() {
        dataSources = new ConcurrentHashMap<String, DataSource>();
        for (String ds : dataSourceNames.split(" ")) {
            try {
                Object o = getOpenEJBResource(ds);
                if (o instanceof DataSource) {
                    dataSources.put(ds, DataSource.class.cast(o));
                }
            } catch (NamingException e) {
                // ignorado
            }
        }
    }

    /**
     * @return o usuário selecionou a fonte de dados, se estiver definida
      * ou o padrão
     *  @throws IllegalArgumentException se a fonte de dados não for encontrada
     */
    @Override
    public DataSource getDataSource() {
        // lazy init of routed datasources
        if (dataSources == null) {
            init();
        }

        // se nenhuma fonte de dados estiver selecionada, use a fonte padrão
        if (currentDataSource.get() == null) {
            if (dataSources.containsKey(defaultDataSourceName)) {
                return dataSources.get(defaultDataSourceName);

            } else {
                throw new IllegalArgumentException("você precisa especificar pelo menos uma fonte de dados");
            }
        }

        // o desenvolvedor configurou a fonte de dados para usar
        return currentDataSource.get();
    }

    /**
     *
     * @param datasourceName nome da fonte de dados
     */
    public void setDataSource(String datasourceName) {
        if (dataSources == null) {
            init();
        }
        if (!dataSources.containsKey(datasourceName)) {
            throw new IllegalArgumentException("data source called " + datasourceName + " can't be found.");
        }
        DataSource ds = dataSources.get(datasourceName);
        currentDataSource.set(ds);
    }

    /**
     * redefinir a fonte de dados
     */
    public void clear() {
        currentDataSource.remove();
    }

    public void setDefaultDataSourceName(String name) {
        this.defaultDataSourceName = name;
    }
}

Declarando a implementação

Para poder usar seu roteador como um recurso, você precisa fornecer uma configuração de serviço. Isso é feito em um arquivo que você pode encontrar em META-INF/org.router/ e chamado service-jar.xml (para sua implementação é claro que você pode alterar o nome do pacote).

Ele contém o seguinte código:

<ServiceJar>
  <ServiceProvider id="DeterminedRouter" <!-- o nome que você deseja usar -->
      service="Resource"
      type="org.apache.openejb.resource.jdbc.Router"
      class-name="org.superbiz.dynamicdatasourcerouting.DeterminedRouter"> <!-- classe de implementação -->

    # os parametros

    DataSourceNames
    DefaultDataSourceName
  </ServiceProvider>
</ServiceJar>

Usando o roteador

Aqui temos um bean sem estado RoutedPersister que usa nosso DeterminedRouter

package org.superbiz.dynamicdatasourcerouting;

import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Stateless
public class RoutedPersister {
    @PersistenceContext(unitName = "router")
    private EntityManager em;

    @Resource(name = "My Router", type = DeterminedRouter.class)
    private DeterminedRouter router;

    public void persist(int id, String name, String ds) {
        router.setDataSource(ds);
        em.persist(new Person(id, name));
    }
}

O teste

No modo de teste e usando a configuração de estilo de propriedade, a seguinte configuração é usada:

public class DynamicDataSourceTest {
    @Test
    public void route() throws Exception {
        String[] databases = new String[]{"database1", "database2", "database3"};

        Properties properties = new Properties();
        properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, LocalInitialContextFactory.class.getName());

        // Recursos
        // fontes de dados
        for (int i = 1; i <= databases.length; i++) {
            String dbName = databases[i - 1];
            properties.setProperty(dbName, "new://Resource?type=DataSource");
            dbName += ".";
            properties.setProperty(dbName + "JdbcDriver", "org.hsqldb.jdbcDriver");
            properties.setProperty(dbName + "JdbcUrl", "jdbc:hsqldb:mem:db" + i);
            properties.setProperty(dbName + "UserName", "sa");
            properties.setProperty(dbName + "Password", "");
            properties.setProperty(dbName + "JtaManaged", "true");
        }

        // router
        properties.setProperty("My Router", "new://Resource?provider=org.router:DeterminedRouter&type=" + DeterminedRouter.class.getName());
        properties.setProperty("My Router.DatasourceNames", "database1 database2 database3");
        properties.setProperty("My Router.DefaultDataSourceName", "database1");

        // routed datasource
        properties.setProperty("Routed Datasource", "new://Resource?provider=RoutedDataSource&type=" + Router.class.getName());
        properties.setProperty("Routed Datasource.Router", "My Router");

        Context ctx = EJBContainer.createEJBContainer(properties).getContext();
        RoutedPersister ejb = (RoutedPersister) ctx.lookup("java:global/dynamic-datasource-routing/RoutedPersister");
        for (int i = 0; i < 18; i++) {
            // persistir uma pessoa no banco de dados db -> tipo de round robin manual
            String name = "record " + i;
            String db = databases[i % 3];
            ejb.persist(i, name, db);
        }

        // afirmar o número de registros do banco de dados usando jdbc
        for (int i = 1; i <= databases.length; i++) {
            Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:db" + i, "sa", "");
            Statement st = connection.createStatement();
            ResultSet rs = st.executeQuery("select count(*) from PERSON");
            rs.next();
            assertEquals(6, rs.getInt(1));
            st.close();
            connection.close();
        }

        ctx.close();
    }
}

Configuração via openejb.xml

O testcase acima usa propriedades para configuração. O caminho idêntico para fazê-lo através do conf/openejb.xml é o seguinte:

<!-- Roteador e fonte de dados -->
<Resource id="My Router" type="org.apache.openejb.router.test.DynamicDataSourceTest$DeterminedRouter" provider="org.routertest:DeterminedRouter">
    DatasourceNames = database1 database2 database3
    DefaultDataSourceName = database1
</Resource>
<Resource id="Routed Datasource" type="org.apache.openejb.resource.jdbc.Router" provider="RoutedDataSource">
    Router = My Router
</Resource>

<!-- fontes de dados reais -->
<Resource id="database1" type="DataSource">
    JdbcDriver = org.hsqldb.jdbcDriver
    JdbcUrl = jdbc:hsqldb:mem:db1
    UserName = sa
    Password
    JtaManaged = true
</Resource>
<Resource id="database2" type="DataSource">
    JdbcDriver = org.hsqldb.jdbcDriver
    JdbcUrl = jdbc:hsqldb:mem:db2
    UserName = sa
    Password
    JtaManaged = true
</Resource>
<Resource id="database3" type="DataSource">
    JdbcDriver = org.hsqldb.jdbcDriver
    JdbcUrl = jdbc:hsqldb:mem:db3
    UserName = sa
    Password
    JtaManaged = true
</Resource>

Algum hack para o OpenJPA

Usar mais de uma fonte de dados atrás de um EntityManager significa que bancos de dados já foram criados. Se não for esse o caso, o provedor JPA precisa criar a fonte de dados no momento da inicialização.

Hibernate faz isso, se você declarar seus bancos de dados vão funcionar. Contudo com o OpenJPA (o provedor JPA padrão para o OpenEJB), a criação é preguiçosa e isso acontece apenas uma vez; quando você alterna o banco de dados, ele não vai funcionar mais.

É claro que o OpenEJB fornece os recursos @Singleton e @Startup do Java EE 6 e podemos fazer um bean apenas fazendo uma descoberta simples, mesmo que não exista entidades, apenas para forçar a criação do banco de dados:

@Startup
@Singleton
public class BoostrapUtility {
    // injetar todos os bancos de dados reais

    @PersistenceContext(unitName = "db1")
    private EntityManager em1;

    @PersistenceContext(unitName = "db2")
    private EntityManager em2;

    @PersistenceContext(unitName = "db3")
    private EntityManager em3;

    // forçar a criação de banco de dados

    @PostConstruct
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public void initDatabase() {
        em1.find(Person.class, 0);
        em2.find(Person.class, 0);
        em3.find(Person.class, 0);
    }
}

Usando a fonte de dados roteada

Agora você configurou a maneira como deseja rotear sua operação JPA, registrou os recursos e você inicializou seus bancos de dados, você pode usar e veja como é simples:

@Stateless
public class RoutedPersister {
    // injeção da fonte de dados "em proxy"
    @PersistenceContext(unitName = "router")
    private EntityManager em;

    // injeção do roteador, você precisa configurar o banco de dados
    @Resource(name = "My Router", type = DeterminedRouter.class)
    private DeterminedRouter router;

    public void persist(int id, String name, String ds) {
        router.setDataSource(ds); // configurando o banco de dados para a transação atual
        em.persist(new Person(id, name)); // usará o banco de dados ds automaticamente
    }
}

Executando

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest
Apache OpenEJB 4.0.0-beta-1    build: 20111002-04:06
http://tomee.apache.org/
INFO - openejb.home = /Users/dblevins/examples/dynamic-datasource-routing
INFO - openejb.base = /Users/dblevins/examples/dynamic-datasource-routing
INFO - Using 'jakarta.ejb.embeddable.EJBContainer=true'
INFO - Configuring Service(id=Default Security Service, type=SecurityService, provider-id=Default Security Service)
INFO - Configuring Service(id=Default Transaction Manager, type=TransactionManager, provider-id=Default Transaction Manager)
INFO - Configuring Service(id=My Router, type=Resource, provider-id=DeterminedRouter)
INFO - Configuring Service(id=database3, type=Resource, provider-id=Default JDBC Database)
INFO - Configuring Service(id=database2, type=Resource, provider-id=Default JDBC Database)
INFO - Configuring Service(id=Routed Datasource, type=Resource, provider-id=RoutedDataSource)
INFO - Configuring Service(id=database1, type=Resource, provider-id=Default JDBC Database)
INFO - Found EjbModule in classpath: /Users/dblevins/examples/dynamic-datasource-routing/target/classes
INFO - Beginning load: /Users/dblevins/examples/dynamic-datasource-routing/target/classes
INFO - Configuring enterprise application: /Users/dblevins/examples/dynamic-datasource-routing
WARN - Method 'lookup' is not available for 'jakarta.annotation.Resource'. Probably using an older Runtime.
INFO - Configuring Service(id=Default Singleton Container, type=Container, provider-id=Default Singleton Container)
INFO - Auto-creating a container for bean BoostrapUtility: Container(type=SINGLETON, id=Default Singleton Container)
INFO - Configuring Service(id=Default Stateless Container, type=Container, provider-id=Default Stateless Container)
INFO - Auto-creating a container for bean RoutedPersister: Container(type=STATELESS, id=Default Stateless Container)
INFO - Auto-linking resource-ref 'java:comp/env/My Router' in bean RoutedPersister to Resource(id=My Router)
INFO - Configuring Service(id=Default Managed Container, type=Container, provider-id=Default Managed Container)
INFO - Auto-creating a container for bean org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest: Container(type=MANAGED, id=Default Managed Container)
INFO - Configuring PersistenceUnit(name=router)
INFO - Configuring PersistenceUnit(name=db1)
INFO - Auto-creating a Resource with id 'database1NonJta' of type 'DataSource for 'db1'.
INFO - Configuring Service(id=database1NonJta, type=Resource, provider-id=database1)
INFO - Adjusting PersistenceUnit db1 <non-jta-data-source> to Resource ID 'database1NonJta' from 'null'
INFO - Configuring PersistenceUnit(name=db2)
INFO - Auto-creating a Resource with id 'database2NonJta' of type 'DataSource for 'db2'.
INFO - Configuring Service(id=database2NonJta, type=Resource, provider-id=database2)
INFO - Adjusting PersistenceUnit db2 <non-jta-data-source> to Resource ID 'database2NonJta' from 'null'
INFO - Configuring PersistenceUnit(name=db3)
INFO - Auto-creating a Resource with id 'database3NonJta' of type 'DataSource for 'db3'.
INFO - Configuring Service(id=database3NonJta, type=Resource, provider-id=database3)
INFO - Adjusting PersistenceUnit db3 <non-jta-data-source> to Resource ID 'database3NonJta' from 'null'
INFO - Enterprise application "/Users/dblevins/examples/dynamic-datasource-routing" loaded.
INFO - Assembling app: /Users/dblevins/examples/dynamic-datasource-routing
INFO - PersistenceUnit(name=router, provider=org.apache.openjpa.persistence.PersistenceProviderImpl) - provider time 504ms
INFO - PersistenceUnit(name=db1, provider=org.apache.openjpa.persistence.PersistenceProviderImpl) - provider time 11ms
INFO - PersistenceUnit(name=db2, provider=org.apache.openjpa.persistence.PersistenceProviderImpl) - provider time 7ms
INFO - PersistenceUnit(name=db3, provider=org.apache.openjpa.persistence.PersistenceProviderImpl) - provider time 6ms
INFO - Jndi(name="java:global/dynamic-datasource-routing/BoostrapUtility!org.superbiz.dynamicdatasourcerouting.BoostrapUtility")
INFO - Jndi(name="java:global/dynamic-datasource-routing/BoostrapUtility")
INFO - Jndi(name="java:global/dynamic-datasource-routing/RoutedPersister!org.superbiz.dynamicdatasourcerouting.RoutedPersister")
INFO - Jndi(name="java:global/dynamic-datasource-routing/RoutedPersister")
INFO - Jndi(name="java:global/EjbModule1519652738/org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest!org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest")
INFO - Jndi(name="java:global/EjbModule1519652738/org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest")
INFO - Created Ejb(deployment-id=RoutedPersister, ejb-name=RoutedPersister, container=Default Stateless Container)
INFO - Created Ejb(deployment-id=org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest, ejb-name=org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest, container=Default Managed Container)
INFO - Created Ejb(deployment-id=BoostrapUtility, ejb-name=BoostrapUtility, container=Default Singleton Container)
INFO - Started Ejb(deployment-id=RoutedPersister, ejb-name=RoutedPersister, container=Default Stateless Container)
INFO - Started Ejb(deployment-id=org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest, ejb-name=org.superbiz.dynamicdatasourcerouting.DynamicDataSourceTest, container=Default Managed Container)
INFO - Started Ejb(deployment-id=BoostrapUtility, ejb-name=BoostrapUtility, container=Default Singleton Container)
INFO - Deployed Application(path=/Users/dblevins/examples/dynamic-datasource-routing)
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.504 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0