一、緣由
上一篇文章Spring3.3 整合 Hibernate3、MyBatis3.2 配置多數據源/動態切換數據源 方法介紹到了怎么樣在Sping、MyBatis、Hibernate整合的應用中動態切換DataSource數據源的方法,但最終遺留下一個問題:不能切換數據庫方言。數據庫方言可能在當前應用的架構中意義不是很大,但是如果單純用MyBatis或Hibernate做數據庫持久化操作,還是要處理這一問題。
那么下面將介紹怎么樣動態切換SessionFactory,為什么要切換SessionFactory?
因為這里切換SessionFactory就可以實現多數據源和多個SessionFactory,每個SessionFactory有自己獨立的數據庫配置和SessionFactory的相關配置。我們的數據庫方言就配置在SessionFactory這里,所以實現了切換SessionFactory也就實現了切換數據庫方言的問題。這個主要是針對Hibernate來操作的,而MyBatis則需要動態切換SqlSessionFactory才行。
二、實現代碼
1、定義全局切換SessionFactory的工具
package com.hoo.framework.spring.support;
/**
* <b>function:</b> 多數據源
* @author hoojo
* @createDate 2013-9-27 上午11:36:57
* @file CustomerContextHolder.java
* @package com.hoo.framework.spring.support
* @project SHMB
* @blog http://blog.csdn.net/IBM_hoojo
* @email hoojo_@126.com
* @version 1.0
*/
public abstract class CustomerContextHolder {public final static String SESSION_FACTORY_MYSQL = "mysql";public final static String SESSION_FACTORY_ORACLE = "oracle";private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();public static void setCustomerType(String customerType) {contextHolder.set(customerType);}public static String getCustomerType() {return contextHolder.get();
}public static void clearCustomerType() {contextHolder.remove();}}同樣上面的靜態變量和前一文章中介紹的一致,它需要和下面配置文件中的SessionFactory的key對應。
2、實現自己的SessionFactory
定義好接口
package com.hoo.framework.spring.support.core;
import org.hibernate.SessionFactory;
/**
* <b>function:</b> 動態SessionFactory接口
* @author hoojo
* @createDate 2013-10-12 下午03:29:52
* @file DynamicSessionFactory.java
* @package com.hoo.framework.spring.support.core
* @project SHMB
* @blog http://blog.csdn.net/IBM_hoojo
* @email hoojo_@126.com
* @version 1.0
*/
public interface DynamicSessionFactory extends SessionFactory {public SessionFactory getHibernateSessionFactory();
}
實現接口
package com.hoo.framework.spring.support.core;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Map;
import java.util.Set;
import javax.naming.NamingException;
import javax.naming.Reference;
import org.hibernate.Cache;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.TypeHelper;
import org.hibernate.classic.Session;
import org.hibernate.engine.FilterDefinition;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.metadata.CollectionMetadata;
import org.hibernate.stat.Statistics;
import com.hoo.framework.spring.support.CustomerContextHolder;
/**
* <b>function:</b> 動態數據源實現
* @author hoojo
* @createDate 2013-10-12 下午03:31:31
* @file DynamicSessionFactoryImpl.java
* @package com.hoo.framework.spring.support.core
* @project SHMB
* @blog http://blog.csdn.net/IBM_hoojo
* @email hoojo_@126.com
* @version 1.0
*/
@SuppressWarnings({ "unchecked", "deprecation" })public class DynamicSessionFactoryImpl implements DynamicSessionFactory {private static final long serialVersionUID = 5384069312247414885L;private Map<Object, SessionFactory> targetSessionFactorys;
private SessionFactory defaultTargetSessionFactory;
/**
* @see com.hoo.framework.spring.support.core.DynamicSessionFactory#getHibernateSessionFactory()
* <b>function:</b> 重寫這個方法,這里最關鍵
* @author hoojo
* @createDate 2013-10-18 上午10:45:25
*/
@Overridepublic SessionFactory getHibernateSessionFactory() {
SessionFactory targetSessionFactory = targetSessionFactorys.get(CustomerContextHolder.getCustomerType());if (targetSessionFactory != null) {
return targetSessionFactory;
} else if (defaultTargetSessionFactory != null) {return defaultTargetSessionFactory;
}return null;
}@Overridepublic void close() throws HibernateException {this.getHibernateSessionFactory().close();
}@Overridepublic boolean containsFetchProfileDefinition(String s) {return this.getHibernateSessionFactory().containsFetchProfileDefinition(s);}@Overridepublic void evict(Class clazz) throws HibernateException {this.getHibernateSessionFactory().evict(clazz);
}@Overridepublic void evict(Class clazz, Serializable serializable) throws HibernateException {this.getHibernateSessionFactory().evict(clazz, serializable);
}@Overridepublic void evictCollection(String s) throws HibernateException {this.getHibernateSessionFactory().evictCollection(s);
}@Overridepublic void evictCollection(String s, Serializable serializable) throws HibernateException {this.getHibernateSessionFactory().evictCollection(s, serializable);
}@Overridepublic void evictEntity(String entity) throws HibernateException {this.getHibernateSessionFactory().evictEntity(entity);
}@Overridepublic void evictEntity(String entity, Serializable serializable) throws HibernateException {this.getHibernateSessionFactory().evictEntity(entity, serializable);
}@Overridepublic void evictQueries() throws HibernateException {this.getHibernateSessionFactory().evictQueries();
}@Overridepublic void evictQueries(String queries) throws HibernateException {this.getHibernateSessionFactory().evictQueries(queries);
}@Overridepublic Map<String, ClassMetadata> getAllClassMetadata() {
return this.getHibernateSessionFactory().getAllClassMetadata();}@Overridepublic Map getAllCollectionMetadata() {
return this.getHibernateSessionFactory().getAllClassMetadata();}@Overridepublic Cache getCache() {
return this.getHibernateSessionFactory().getCache();}@Overridepublic ClassMetadata getClassMetadata(Class clazz) {
return this.getHibernateSessionFactory().getClassMetadata(clazz);}@Overridepublic ClassMetadata getClassMetadata(String classMetadata) {
return this.getHibernateSessionFactory().getClassMetadata(classMetadata);}@Overridepublic CollectionMetadata getCollectionMetadata(String collectionMetadata) {
return this.getHibernateSessionFactory().getCollectionMetadata(collectionMetadata);}@Overridepublic Session getCurrentSession() throws HibernateException {return this.getHibernateSessionFactory().getCurrentSession();}@Overridepublic Set getDefinedFilterNames() {
return this.getHibernateSessionFactory().getDefinedFilterNames();}@Overridepublic FilterDefinition getFilterDefinition(String definition) throws HibernateException {return this.getHibernateSessionFactory().getFilterDefinition(definition);}@Overridepublic Statistics getStatistics() {
return this.getHibernateSessionFactory().getStatistics();}@Overridepublic TypeHelper getTypeHelper() {
return this.getHibernateSessionFactory().getTypeHelper();}@Overridepublic boolean isClosed() {return this.getHibernateSessionFactory().isClosed();}@Overridepublic Session openSession() throws HibernateException {return this.getHibernateSessionFactory().openSession();}@Overridepublic Session openSession(Interceptor interceptor) throws HibernateException {return this.getHibernateSessionFactory().openSession(interceptor);}@Overridepublic Session openSession(Connection connection) {
return this.getHibernateSessionFactory().openSession(connection);}@Overridepublic Session openSession(Connection connection, Interceptor interceptor) {
return this.getHibernateSessionFactory().openSession(connection, interceptor);}@Overridepublic StatelessSession openStatelessSession() {
return this.getHibernateSessionFactory().openStatelessSession();}@Overridepublic StatelessSession openStatelessSession(Connection connection) {
return this.getHibernateSessionFactory().openStatelessSession(connection);}@Overridepublic Reference getReference() throws NamingException {return this.getHibernateSessionFactory().getReference();}public void setTargetSessionFactorys(Map<Object, SessionFactory> targetSessionFactorys) {this.targetSessionFactorys = targetSessionFactorys;
}public void setDefaultTargetSessionFactory(SessionFactory defaultTargetSessionFactory) {this.defaultTargetSessionFactory = defaultTargetSessionFactory;
}}上面最重要的就是getHibernateSessionFactory重寫這個方法,其他方法和原來實現的無異。重寫這個方法后利用CustomerContextHolder動態設置SessionFactory類型就可以動態的切換SessionFactory。
3、動態的事務管理器,因為我們這里是動態切換SessionFactory,所以事務這塊也需要動態切換SessionFactory來完成事務的操作。
package com.hoo.framework.spring.support.tx;
import javax.sql.DataSource;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate3.HibernateTransactionManager;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import com.hoo.framework.spring.support.core.DynamicSessionFactory;
/**
* <b>function:</b> 重寫HibernateTransactionManager事務管理器,實現自己的動態的事務管理器
* @author hoojo
* @createDate 2013-10-12 下午03:54:02
* @file DynamicTransactionManager.java
* @package com.hoo.framework.spring.support.tx
* @project SHMB
* @blog http://blog.csdn.net/IBM_hoojo
* @email hoojo_@126.com
* @version 1.0
*/
public class DynamicTransactionManager extends HibernateTransactionManager {private static final long serialVersionUID = -4655721479296819154L;/**
* @see org.springframework.orm.hibernate4.HibernateTransactionManager#getDataSource()
* <b>function:</b> 重寫
* @author hoojo
* @createDate 2013-10-12 下午03:55:24
*/
@Overridepublic DataSource getDataSource() {
return SessionFactoryUtils.getDataSource(getSessionFactory());
}/**
* @see org.springframework.orm.hibernate4.HibernateTransactionManager#getSessionFactory()
* <b>function:</b> 重寫
* @author hoojo
* @createDate 2013-10-12 下午03:55:24
*/
@Overridepublic SessionFactory getSessionFactory() {
DynamicSessionFactory dynamicSessionFactory = (DynamicSessionFactory) super.getSessionFactory();
SessionFactory hibernateSessionFactory = dynamicSessionFactory.getHibernateSessionFactory();return hibernateSessionFactory;
}}這里主要重寫getDataSource()/getSessionFactory()這兩個方法,getSessionFactory方法是利用我們上面定義的接口來動態獲取我們在上下文(CustomerContextHolder)中定義切換的SessionFactory對象。而getDataSource則是獲得動態SessionFactory的DataSource,這里也不難理解。
4、至此,重寫的接口和實現都完成,下面開始配置相關的代碼
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.2.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.2.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-3.2.xsd ">
<!-- 配置c3p0數據源 -->
<bean id="dataSourceOracle" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"><property name="driverClass" value="${datasource.driver}"/><property name="jdbcUrl" value="${datasource.url}"/><property name="user" value="${datasource.username}"/><property name="password" value="${datasource.password}"/><property name="acquireIncrement" value="${c3p0.acquireIncrement}"/><property name="initialPoolSize" value="${c3p0.initialPoolSize}"/><property name="minPoolSize" value="${c3p0.minPoolSize}"/><property name="maxPoolSize" value="${c3p0.maxPoolSize}"/><property name="maxIdleTime" value="${c3p0.maxIdleTime}"/><property name="idleConnectionTestPeriod" value="${c3p0.idleConnectionTestPeriod}"/><property name="maxStatements" value="${c3p0.maxStatements}"/><property name="numHelperThreads" value="${c3p0.numHelperThreads}"/><property name="preferredTestQuery" value="${c3p0.preferredTestQuery}"/><property name="testConnectionOnCheckout" value="${c3p0.testConnectionOnCheckout}"/></bean><bean id="dataSourceMySQL" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"><property name="driverClass" value="com.mysql.jdbc.Driver"/><property name="jdbcUrl" value="jdbc:mysql://172.31.108.178:3306/world?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull"/><property name="user" value="root"/><property name="password" value="jp2011"/><property name="acquireIncrement" value="${c3p0.acquireIncrement}"/><property name="initialPoolSize" value="${c3p0.initialPoolSize}"/><property name="minPoolSize" value="${c3p0.minPoolSize}"/><property name="maxPoolSize" value="${c3p0.maxPoolSize}"/><property name="maxIdleTime" value="${c3p0.maxIdleTime}"/><property name="idleConnectionTestPeriod" value="${c3p0.idleConnectionTestPeriod}"/><property name="maxStatements" value="${c3p0.maxStatements}"/><property name="numHelperThreads" value="${c3p0.numHelperThreads}"/><property name="preferredTestQuery" value="${c3p0.preferredTestQuery}"/><property name="testConnectionOnCheckout" value="${c3p0.testConnectionOnCheckout}"/></bean><!-- Annotation 配置sessionFactory,配置數據庫連接,注入hibernate數據庫配置 -->
<bean id="mySQLSessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"><property name="dataSource" ref="dataSourceMySQL"/><property name="packagesToScan" value="com.hoo.**.mysqlentity"/><property name="annotatedClasses"><array><value>com.hoo.common.entity.IDGenerator</value></array></property><property name="hibernateProperties"><props><prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop><!-- 鏈接釋放策略 on_close | after_transaction | after_statement | auto -->
<prop key="hibernate.connection.release_mode">after_transaction</prop><prop key="hibernate.show_sql">true</prop><prop key="hibernate.format_sql">true</prop></props></property></bean><!-- Annotation 配置sessionFactory,配置數據庫連接,注入hibernate數據庫配置 -->
<bean id="oracleSessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"><property name="dataSource" ref="dataSourceOracle"/><property name="packagesToScan" value="com.hoo.**.entity"/><property name="hibernateProperties"><props><prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop><prop key="hibernate.connection.release_mode">after_transaction</prop><prop key="hibernate.show_sql">true</prop><prop key="hibernate.format_sql">true</prop><!--prop key="hibernate.hbm2ddl.auto">update</prop-->
</props></property></bean><!-- 動態SessionFactory -->
<bean id="sessionFactory" class="com.hoo.framework.spring.support.core.DynamicSessionFactoryImpl"><property name="defaultTargetSessionFactory" ref="oracleSessionFactory"/><property name="targetSessionFactorys"><map><entry value-ref="oracleSessionFactory" key="oracle"/><entry value-ref="mySQLSessionFactory" key="mysql"/></map></property></bean><!-- 自定義動態切換SessionFactory事務管理器,注入sessionFactory -->
<bean id="transactionManager" class="com.hoo.framework.spring.support.tx.DynamicTransactionManager"><property name="sessionFactory" ref="sessionFactory" /></bean><!-- 配置事務的傳播特性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception"/><tx:method name="edit*" propagation="REQUIRED" rollback-for="java.lang.Exception"/><tx:method name="remove*" propagation="REQUIRED" rollback-for="java.lang.Exception"/><tx:method name="execute*" propagation="REQUIRED" rollback-for="java.lang.Exception"/><tx:method name="*" read-only="true" /></tx:attributes></tx:advice><!-- 配置那些類、方法納入到事務的管理 -->
<aop:config><aop:pointcut expression="execution(* com.hoo.**.service.impl.*.*(..))" id="transactionManagerMethod"/><aop:advisor advice-ref="txAdvice" pointcut-ref="transactionManagerMethod" /></aop:config></beans>配置也和我們之前的配置差不多,就是事務管理器部分注入的SessionFactory是我們自己定義的。
5、簡單測試
@Testpublic void testAdd() {// 主要就是這行代碼 它才是完成SessionFactory的切換
CustomerContextHolder.setCustomerType(CustomerContextHolder.SESSION_FACTORY_MYSQL);DeviceInfo entity = new DeviceInfo();
entity.setSbbh(System.currentTimeMillis() + "");
entity.setIpdz("my ip address2");
entity.setJd(1234);try {
service.add(entity);} catch (Exception e) {
e.printStackTrace();}}經過測試發現可以查詢數據,利用hibernate查詢分頁也可以生成對應數據庫的分頁語句。同樣在服務層的方法中添加完操作后,特意拋出一個異常,數據也能正常回滾操作,所以DynamicTransactionManager也是起到了該有的作用!
這里我的測試用例是手動切換CustomerContextHolder.setCustomerType的,但實際開發中我們還是得用Spring的Aop中的Interceptor進行切面編程,完成動態切換SessionFactory。上一篇文章Spring3.3 整合 Hibernate3、MyBatis3.2 配置多數據源/動態切換數據源 方法已經提到了(讀者可以參考該篇博文中的第二節的3、7小節DataSourceMethodInterceptor MultipleDataSourceInterceptor),這里就不再贅述!
三、引發的問題
上述的實現如果在使用不當的情況下,在實際開發中很可能存在一些問題!
問題1是這樣的,通常事務是在Service這層完成,這個應該是沒有異議。倘若是這樣的話,問題便出現了。而通常我們在MVC中的視圖層中僅僅會調用Service中一個方法來完成所有當前業務的處理。如果這個Service中同時操作dbA、dbB兩個數據庫,事務提交的時候是用哪個數據庫的事務呢?所以我們把不同數據庫的操作放在一個方法,就會出現事務的問題的,除非兩個數據庫的事務能夠同時回滾!
大致情景是:Service中的add4Oracle操作Oracle數據庫,Service中的add4MySQL操作MySQL數據庫,最后把Service中的add4Oracle、add4MySQL方法放到一個operation方法中,MVC視圖層的業務控制調用Service中的operation。像這樣的情況,如果add4Oracle或add4MySQL方法中的某一個方法出現異常,就需要把兩個數據庫事務都回滾。
解決辦法就是在Service或Dao中的一個方法中同時操作兩個數據庫,手動完成事務控制。出現異常就全部回滾,沒有異常就全部提交,只能這樣犧牲下。
問題2是利用攔截器不能同時切換一個方法操作兩個數據庫的可能,例如一個service中的query方法即需要用query MySQL,也需要query Oracle。那么解決辦法就是在query方法中調用當前service的query4Oracle和query4Oracle,那樣繞過去還是能利用攔截器進行動態切換的。
四、總結
要完成數據庫方言的切換,我們就需要配置多個SessionFactory利用自己實現的DynamicSessionFactory和CustomerContextHolder完成SessionFactory的切換。利用DynamicTransactionManager完成當前Session的事務操作。通過對這些對象的操作和配置,最終可以完成SessionFactory的動態切換。實際使用中雖然有些問題出現,但是最終還是有解決方案,盡管有些不完美。所謂的完美的東西總有它不完美的地方,這樣才算完美,比如斷臂的維納斯女神~!