3

AbstractRoutingDataSourceを使用してアクティブなデータソースを切り替えるときに、データソース間でトランザクションを共有するにはどうすればよいですか?

これまでのところ、トランザクションがないと、クエリは両方のデータベースで正しく実行されますが、トランザクションを開始すると、すべてが同じデータベースで実行されます(つまり、2番目のデータベースに切り替えることはできなくなります)。

何か案は?

@Transactional
public void crossDbTransactionTest() {
    // Selects a datasource from my pool of AbstractRoutingDataSources
    DbConnectionContextHolder.setDbConnectionByYear(2012);

    // execute something in the first database
    this.executeSomeJpaQuery("xyz"); 

    // switch to the second database
    DbConnectionContextHolder.setDbConnectionByYear(2011);

    // execute something in the second database
    this.executeSomeJpaQuery("xyz"); // on any errors rollback changes in both databases
}

EDIT1(追加された構成ファイル):

persistence.xml:

<persistence-unit name="primarnaKonekcija" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
        <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
        <property name="hibernate.max_fetch_depth" value="1" />
        <property name="hibernate.transaction.manager_lookup_class"
                              value="org.hibernate.transaction.JBossTransactionManagerLookup" />
    </properties>
</persistence-unit>

spring-jpa.xml:

<!-- Shared DB credentials -->
<context:property-placeholder location="classpath:config.properties" />

<!-- DB connections by year -->
<bean id="parentDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" abstract="true">
    <property name="driverClassName" value="${db.driver}" />
    <property name="username" value="${db.user}" />
    <property name="password" value="${db.password}" />
</bean>
<bean id="dataSource" class="myPackage.DbConnectionRoutingDataSource">
    <!-- Placeholder that is replaced in BeanFactoryPostProcessor -->
    <property name="targetDataSources">
        <map key-type="int">
            <entry key="0" value-ref="placeholderDs" />
        </map>
    </property>
    <property name="defaultTargetDataSource" ref="placeholderDs" />
</bean>

<!-- EntityManager configuration -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="primarnaKonekcija" />
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="databasePlatform" value="org.hibernate.dialect.SQLServerDialect" />
            <property name="showSql" value="true" />
        </bean>
    </property>
</bean>

<tx:annotation-driven />
<tx:jta-transaction-manager />

編集2:

すべてをJTAおよびJNDI提供のデータソースに切り替えてみました。

transaction-type="RESOURCE_LOCAL"をtransaction-type="JTA"に変更しても機能しませんでした-JtaStatusHelperは、transactionManagerがnullであることを示すNullPointerExceptionをスローします。

編集3:

JBossTransactionManagerLookupをpersistence.xmlに追加しました。トランザクション内で2番目のデータソースに切り替えると、「最後の複数のリソースの追加は許可されていません」というメッセージが表示されるようになりました。

編集4:

JBOSSを設定しようとしたので、そのエラーを乗り越えました。データベースの切り替えは、「現在のトランザクションに複数の最後のリソースが追加されました。これはトランザクション的に安全ではないため、信頼しないでください」という予想される警告とともに機能するようになりました。次に、JBOSSでMSSQLXAドライバーを設定してみます。

編集5:

MSSQL XAを構成した後、すべてが意図したとおりに機能し、これを設定するために必要な手順を含む回答が投稿されます。

4

1 に答える 1

3

これは、他に選択肢がない場合を除いて、私がお勧めしないソリューションです。レベル2キャッシュは、このようなソリューションでは機能しない可能性がありますが、基盤となるレガシーデータベースが1つにマージされるまで、時間を購入するために使用することを余儀なくされた(安定した)ソリューションです。

まず、JBossのstandalone.xmlで、データベース接続をXAデータソースとして設定します。MS SQLサーバーを使用している場合は、 http://msdn.microsoft.com/en-us/library/aa342335.aspxでXAを適切にセットアップする方法の指示に従ってください。

スタンドアロン.xml

<datasources>
    <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
        <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</connection-url>
        <driver>h2</driver>
        <security>
            <user-name>sa</user-name>
            <password>sa</password>
        </security>
    </datasource>
    <xa-datasource jta="true" jndi-name="java:jboss/datasources/MYDB_ONE" pool-name="MYDB_ONE" enabled="true" use-java-context="true" use-ccm="true">
        <xa-datasource-property name="ServerName">
            localhost
        </xa-datasource-property>
        <xa-datasource-property name="DatabaseName">
            MYDB_ONE
        </xa-datasource-property>
        <xa-datasource-property name="SelectMethod">
            cursor
        </xa-datasource-property>
        <xa-datasource-class>com.microsoft.sqlserver.jdbc.SQLServerXADataSource</xa-datasource-class>
        <driver>sqljdbc</driver>
        <xa-pool>
            <is-same-rm-override>false</is-same-rm-override>
        </xa-pool>
        <security>
            <user-name>some_user</user-name>
            <password>some_password</password>
        </security>
        <validation>
            <valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mssql.MSSQLValidConnectionChecker"/>
        </validation>
    </xa-datasource>
    <xa-datasource jta="true" jndi-name="java:jboss/datasources/MYDB_TWO" pool-name="MYDB_TWO" enabled="true" use-java-context="true" use-ccm="true">
        <xa-datasource-property name="ServerName">
            localhost
        </xa-datasource-property>
        <xa-datasource-property name="DatabaseName">
            MYDB_TWO
        </xa-datasource-property>
        <xa-datasource-property name="SelectMethod">
            cursor
        </xa-datasource-property>
        <xa-datasource-class>com.microsoft.sqlserver.jdbc.SQLServerXADataSource</xa-datasource-class>
        <driver>sqljdbc</driver>
        <xa-pool>
            <is-same-rm-override>false</is-same-rm-override>
        </xa-pool>
        <security>
            <user-name>some_user</user-name>
            <password>some_password</password>
        </security>
        <validation>
            <valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mssql.MSSQLValidConnectionChecker"/>
        </validation>
    </xa-datasource>
    <drivers>
        <driver name="h2" module="com.h2database.h2">
            <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
        </driver>
        <driver name="sqljdbc" module="com.microsoft.sqlserver.jdbc">
            <driver-class>com.microsoft.sqlserver.jdbc.SQLServerDriver</driver-class>
        </driver>
        <driver name="postgresql" module="org.postgresql">
            <xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
        </driver>
    </drivers>
</datasources>

次に、AbstractRoutingDataSourceの実装をdataSourceとして使用するentityManagerBeanをセットアップします。これは、persistence.xmlファイルを使用しないSpringベースのJPAセットアップです。私の知る限り、JBoss7を使用するときにエンティティの自動パッケージスキャンを取得する唯一の方法はこれです。

springJpaConfig.xml

<!-- Use @PersistenceContext annotations for injecting entity managers -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

<!-- Set up JTA transaction manager -->
<tx:jta-transaction-manager />

<bean id="entityManagerFactoryMyDB" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="MyDB" />
    <property name="dataSource" ref="dataSourceMyDB" />
    <property name="packagesToScan" value="my.package.with.jpa.entities" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="showSql" value="true" />
        </bean>
    </property>
    <property name="jpaPropertyMap">
        <map>
            <entry key="javax.persistence.transactionType" value="jta" />

            <entry key="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform" />
            <entry key="jboss.entity.manager.factory.jndi.name" value="java:app/MyDBEntityManagerFactory" />

            <entry key="hibernate.dialect" value="org.hibernate.dialect.SQLServer2008Dialect" />
        </map>
    </property>
</bean>

<bean id="dataSourceMyDB" class="some.package.AbstractRoutingDataSourceMyDB">
    <property name="lenientFallback" value="false" />
    <property name="defaultTargetDataSource" value="java:jboss/datasources/ExampleDS" />
    <property name="targetDataSources">
        <map key-type="String">
            <!-- This is a placeholder that will be filled in by BeanFactoryPostProcessor -->
        </map>
    </property>
</bean>

<!-- This allows us to modify Spring configuration load the list of datasources -->
<bean class="some.package.DatasourceRegisteringBeanFactoryPostProcessor" />

defaultTargetDataSourceを指定する必要があるため、AbstractRoutingDataSourceMyDBのデフォルトとしてExampleDSを使用しますが、常に有効なDBを手動で選択したいので、最初に接続を手動で選択せずにDBにアクセスしようとすると、クエリを実行しようとします。例外をスローする存在しないExampleDSデータベース(非常にハッキーですが、作業は完了します)。

BeanFactoryPostProcessorで、データソースのリストに入力する必要があります。

DatasourceRegisteringBeanFactoryPostProcessor.java

package some.package
class DatasourceRegisteringBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        HashMap<String, String> connectionsListMyDB = new HashMap<>();

        // Load your connection list from wherever you need to, you can
        // enumerate them directly from JNDI or some configuration location
        connectionsListMyDB.put("db1", "java:jboss/datasources/MYDB_ONE");
        connectionsListMyDB.put("db2", "java:jboss/datasources/MYDB_TWO");

        if (connectionsList.isEmpty())
            throw new RuntimeException("No JPA connections defined");

        // Configure the dataSource bean properties
        BeanDefinitionRegistry factory = (BeanDefinitionRegistry) beanFactory;
        MutablePropertyValues mpv = factory.getBeanDefinition("dataSourceMyDB").getPropertyValues();

        ManagedMap<String, String> mm = (ManagedMap<String, String>) mpv.getPropertyValue(
                "targetDataSources").getValue();
        mm.clear();
        for (Entry<String, String> e : connectionsListMyDB.entrySet()) {
            mm.put(e.getKey(), e.getValue());
        }
    }
}

これは、実行時に接続を切り替えることができるAbstractRoutingDataSourceの実装です。

AbstractRoutingDataSourceMyDB.java

public class AbstractRoutingDataSourceMyDB extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return getDbConnectionMyDB();
    }

    // ThreadLocal variable so that the connection gets set for the current thread
    // using spring's request scope on the class instead of ThreadLocal would also work here.
    private final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public void setDbConnectionMyDB(String myKey) {
        Assert.notNull(myKey, "myKey cannot be null");

        contextHolder.set(myKey);

        String k = contextHolder.get();
    }

    public String getDbConnectionMyDB() {
        return (String) contextHolder.get();
    }

    public void clearDbConnectionMyDB() {
        contextHolder.remove();
    }
}

DAOクラス内から現在の接続を変更する前に、entitymanager.flush()とclear()を呼び出す必要があることに注意してください。そうしないと、トランザクションのスコープ内のすべての保留中の操作が、トランザクションのコミット時に新しい接続で実行されます。これは、Hibernateセッションが、接続が変更されたことを認識していないためです。これは、接続が常に同じデータベースであることがわかっている限りです。


したがって、DAOでは、これを今すぐ実行できます。

SomeTableDAO.java

@PersistenceContext(unitName = "MyDB")
private EntityManager em;

@Autowired
private AbstractRoutingDataSourceMyDB routingSource;

public void someMethod(int id) {
    em.flush();
    em.clear();
    routingSource.setDbConnectionMyDB("db1");
    em.remove(em.getReference(Something.class, id)); // delete something in db1

    em.flush();
    em.clear();
    routingSource.setDbConnectionMyDB("db2");
    em.remove(em.getReference(Something.class, id)); // delete something else with the same id in db2
}

それで、あなたは行き​​ます、それはきれいではありませんが-それはすることができます:)

于 2013-01-30T09:23:01.437 に答える