@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());
}
}
Roteamento dinâmico de fonte de dados
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á:
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