Spring JPA Read - расщепление записи - есть транзакционное использование источника данных write - PullRequest
5 голосов
/ 09 февраля 2012

У меня есть приложение, которое использует Hibernate / JPA, с Spring и Jersey.В контексте моего приложения я установил источник данных, определил фабрику диспетчера сущностей, установил диспетчер транзакций с этой фабрикой диспетчера сущностей, и у меня есть различные сервисные методы, аннотированные с помощью аннотации транзакций, поэтому у меня также есть определение на основе tx: annotation для связив моем диспетчере транзакций, где это необходимо.Эта установка прекрасно работает, я мог читать и писать просто отлично.Я хотел бы перейти к настройке БД, где у меня есть мастер с несколькими ведомыми (MySQL).Поэтому я хочу, чтобы все методы, аннотированные транзакционными, использовали источник данных, указывающий на главный сервер БД, а все остальные использовали пул соединений подчиненных.

Я пытался создать два разных источника данных с двумя разными фабриками управления сущностями и двумя разными постоянными единицами - по меньшей мере, уродливо.Я пробовал MySQL Proxy, но у нас было больше проблем, чем нам нужно.Пул соединений уже обработан в контейнере сервлета.Могу ли я реализовать что-то в Tomcat, которое считывает транзакцию и направляет ее на нужный сервер базы данных, или есть способ, которым я мог бы использовать все эти методы, аннотированные с помощью транзакционной аннотации, для использования определенного источника данных?

Ответы [ 3 ]

7 голосов
/ 21 февраля 2012

Вот что я в итоге делал, и это работало довольно хорошо.Менеджер сущностей может иметь только один компонент для использования в качестве источника данных.Поэтому мне нужно было создать бин, который при необходимости направлялся между ними.Именно этот Бен я использовал для менеджера сущностей JPA.

Я настроил два разных источника данных в tomcat.В server.xml я создал два ресурса (источники данных).

<Resource name="readConnection" auth="Container" type="javax.sql.DataSource"
          username="readuser" password="readpass"
          url="jdbc:mysql://readipaddress:3306/readdbname"
          driverClassName="com.mysql.jdbc.Driver"
          initialSize="5" maxWait="5000"
          maxActive="120" maxIdle="5"
          validationQuery="select 1"
          poolPreparedStatements="true"
          removeAbandoned="true" />
<Resource name="writeConnection" auth="Container" type="javax.sql.DataSource"
          username="writeuser" password="writepass"
          url="jdbc:mysql://writeipaddress:3306/writedbname"
          driverClassName="com.mysql.jdbc.Driver"
          initialSize="5" maxWait="5000"
          maxActive="120" maxIdle="5"
          validationQuery="select 1"
          poolPreparedStatements="true"
          removeAbandoned="true" />

У вас могут быть таблицы базы данных на одном сервере, и в этом случае IP-адреса или домен будут одинаковыми, просто разные БД- вы получаете jist.

Затем я добавил ссылку на ресурс в файле context.xml в tomcat, который ссылается на них на ресурсы.

<ResourceLink name="readConnection" global="readConnection" type="javax.sql.DataSource"/>
<ResourceLink name="writeConnection" global="writeConnection" type="javax.sql.DataSource"/>

Эти ссылки на ресурсы - это то, что весна читает вконтекст приложения.

В контексте приложения я добавил определение bean-компонента для каждой ссылки на ресурс и добавил одно дополнительное определение bean-компонента, которое ссылается на созданный мной bean-компонент Router Datasource, который принимает карту (перечисление) двух ранее созданных bean-компонентов (определение bean-компонента)).

<!--
Data sources representing master (write) and slaves (read).
-->
<bean id="readDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="readConnection" /> 
    <property name="resourceRef" value="true" />
    <property name="lookupOnStartup" value="true" />
    <property name="cache" value="true" />
    <property name="proxyInterface" value="javax.sql.DataSource" />  
</bean>

<bean id="writeDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="writeConnection" />
    <property name="resourceRef" value="true" />
    <property name="lookupOnStartup" value="true" />
    <property name="cache" value="true" />
    <property name="proxyInterface" value="javax.sql.DataSource" />
</bean>

<!--
Provider of available (master and slave) data sources.
-->
<bean id="dataSource" class="com.myapp.dao.DatasourceRouter">
    <property name="targetDataSources">
      <map key-type="com.myapp.api.util.AvailableDataSources">
         <entry key="READ" value-ref="readDataSource"/>
         <entry key="WRITE" value-ref="writeDataSource"/>
      </map>
   </property>
   <property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>

После этого определение bean-компонента управления сущностями ссылается на bean-компонент dataSource.

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="persistenceUnitName" value="${jpa.persistenceUnitName}" />
    <property name="jpaVendorAdapter"> 
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> 
            <property name="databasePlatform" value="${jpa.dialect}"/>
            <property name="showSql" value="${jpa.showSQL}" />
        </bean>
    </property>
</bean>

Я определил некоторые свойства в файле свойств, но вы можете заменить значения $ {} наваши собственные конкретные ценности.Итак, теперь у меня есть один bean-компонент, который использует два других bean-компонента, которые представляют два моих источника данных.Один боб - тот, который я использую для JPA.Он не замечает никакой маршрутизации.

Итак, теперь компонент маршрутизации.

public class DatasourceRouter extends AbstractRoutingDataSource{

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException{
    // TODO Auto-generated method stub
    return null;
    }

    @Override
    protected Object determineCurrentLookupKey(){
    return DatasourceProvider.getDatasource();
    }

}

Переопределенный метод вызывается менеджером сущности для определения источника данных в основном.DatasourceProvider имеет локальное (многопоточное) свойство потока с методом получения и установки, а также метод очистки источника данных для очистки.

public class DatasourceProvider{
    private static final ThreadLocal<AvailableDataSources> datasourceHolder = new ThreadLocal<AvailableDataSources>();

    public static void setDatasource(final AvailableDataSources customerType){
    datasourceHolder.set(customerType);
    }

    public static AvailableDataSources getDatasource(){
    return (AvailableDataSources) datasourceHolder.get();
    }

    public static void clearDatasource(){
    datasourceHolder.remove();
    }

}

У меня есть общая реализация DAO с методами, которые я использую для обработкиразличные стандартные вызовы JPA (getReference, persist, createNamedQUery & getResultList и т. д.).Прежде чем он сделает вызов entityManager, чтобы сделать все, что ему нужно, я устанавливаю источник данных DatasourceProvider для чтения или записи.Метод также может обрабатывать передаваемое значение, чтобы сделать его немного более динамичным.Вот пример метода.

@Override
public List<T> findByNamedQuery(final String queryName, final Map<String, Object> properties, final int... rowStartIdxAndCount)
{
DatasourceProvider.setDatasource(AvailableDataSources.READ);
final TypedQuery<T> query = entityManager.createNamedQuery(queryName, persistentClass);
if (!properties.isEmpty())
{
    bindNamedQueryParameters(query, properties);
}
appyRowLimits(query, rowStartIdxAndCount);

return query.getResultList();
}

AvailableDataSources - это перечисление с READ или WRITE, которое ссылается на соответствующий источник данных.Вы можете видеть это на карте, определенной в моем bean-компоненте в контексте приложения.

1 голос
/ 23 февраля 2012

У меня такая же потребность: маршрутизировать соединение между базой данных только для чтения и записи только с использованием классического MASTER / SLAVE для масштабирования операций чтения.

В итоге я нашел решение, использующее базовый класс AbstractRoutingDataSource из Spring. Это позволяет вам вводить источник данных, который направляется к нескольким источникам данных на основе некоторых условий, которые вы пишете.

<bean id="commentsDataSource" class="com.nextep.proto.spring.ReadWriteDataSourceRouter">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="READ" value="java:comp/env/jdbc/readdb"/>
            <entry key="WRITE" value="java:comp/env/jdbc/writedb"/>
        </map>
    </property>
    <property name="defaultTargetDataSource" value="java:comp/env/jdbc/readdb"/>
</bean>

А мой роутер просто выглядит следующим образом:

public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
    return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "READ"
            : "WRITE";
}
}

Я нахожу это довольно элегантным, но проблема здесь в том, что Spring, похоже, устанавливает транзакцию только для чтения после внедрения источника данных, поэтому он не работает. Мой простой тест - проверить результат TransactionSynchronizationManager.isCurrentTransactionReadOnly () в моих методах только для чтения (это правда) и в методе defineCurrentLookupKey (), где он равен false при том же вызове.

Если у вас есть идеи ... В любом случае вы можете основывать тест на чем-то еще, кроме TransactionSynchronizationManager, и это будет работать нормально.

Надеюсь, это поможет, Christophe

0 голосов
/ 29 марта 2012
<bean id="entityManagerFactory" 
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="filerp-pcflows" />
    <property name="dataSource" ref="pooledDS" />
    <property name="persistenceXmlLocation" value="classpath:powercenterCPCPersistence.xml" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="showSql" value="true" />
            <!--<property name="formatSql" value="true" />
            --><property name="generateDdl" value="false" />
            <property name="database" value="DB2" />
        </bean>
    </property>
</bean>

->

<bean id="pool" autowire-candidate="false" class="org.apache.commons.pool.impl.GenericObjectPool" destroy-method="close">
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="maxIdle" value="2"/>
    <property name="minIdle" value="0"/>
    <property name="maxActive" value="8"/>
    <property name="testOnBorrow" value="true"/>
</bean>

<bean id="dsConnectionFactory" class="org.apache.commons.dbcp.DataSourceConnectionFactory">
    <constructor-arg><ref bean="dataSource" /></constructor-arg>
</bean> 
<bean id="poolableConnectionFactory" class="org.apache.commons.dbcp.PoolableConnectionFactory">
    <constructor-arg index="0"><ref bean="dsConnectionFactory" /></constructor-arg>
    <constructor-arg index="1"><ref bean="pool" /></constructor-arg>
    <constructor-arg index="2"><null /></constructor-arg>
    <constructor-arg index="3"><value>select 1 from ${cnx.db2.database.creator}.TPROFILE</value></constructor-arg>
    <constructor-arg index="4"><value>false</value></constructor-arg>
    <constructor-arg index="5"><value>true</value></constructor-arg>
</bean>

<bean id="pooledDS" class="org.apache.commons.dbcp.PoolingDataSource"
    depends-on="poolableConnectionFactory">
    <constructor-arg>
        <ref bean="pool" />
    </constructor-arg>
</bean> 
<import resource="powercenterCPCBeans.xml"/>

...