| :index-group: DataSources |
| :jbake-type: page |
| :jbake-status: status=published |
| = 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á: |
| |
| [source,java] |
| ---- |
| @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. |
| |
| [source,java] |
| ---- |
| 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: |
| |
| [source,xml] |
| ---- |
| <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` |
| |
| [source,java] |
| ---- |
| package org.superbiz.dynamicdatasourcerouting; |
| |
| import javax.annotation.Resource; |
| import javax.ejb.Stateless; |
| import javax.persistence.EntityManager; |
| import javax.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: |
| |
| [source,java] |
| ---- |
| 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: |
| |
| [source,xml] |
| ---- |
| <!-- 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: |
| |
| [source,java] |
| ---- |
| @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: |
| |
| [source,java] |
| ---- |
| @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 |
| |
| [source,console] |
| ---- |
| ------------------------------------------------------- |
| 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 'javax.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 'javax.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 |
| ---- |