1.概述
其實最簡單的辦法就是使用原生sql,如 session.createSQLQuery("sql"),或者使用jdbcTemplate。但是項目中已經使用了hql的方式查詢,修改起來又累,風險又大!所以,必須找到一種比較好的解決方案,實在不行再改寫吧!經過3天的時間的研究,終於找到一種不錯的方法,下面講述之。
2.步驟
2.1 新建hibernate interceptor類
/** * Created by hdwang on 2017/8/7. * * hibernate攔截器:表名替換 */ public class AutoTableNameInterceptor extends EmptyInterceptor { private String srcName = StringUtils.EMPTY; //源表名 private String destName = StringUtils.EMPTY; // 目標表名 public AutoTableNameInterceptor() {} public AutoTableNameInterceptor(String srcName,String destName){ this.srcName = srcName; this.destName = destName; } @Override public String onPrepareStatement(String sql) { if(srcName.equals(StringUtils.EMPTY) || destName.equals(StringUtils.EMPTY)){ return sql; } sql = sql.replaceAll(srcName, destName); return sql; } }
這個interceptor會攔截所有數據庫操作,在發送sql語句之前,替換掉其中的表名。
2.2 配置到sessionFactory去
先看一下sessionFactory是個啥東西。
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean" > <property name="dataSource" ref="defaultDataSource"></property> <property name="packagesToScan"> <list> <value>com.my.pay.task.entity</value> <value>com.my.pay.paycms.entity</value> <value>com.my.pay.data.entity.payincome</value> </list> </property> <property name="mappingLocations"> <list> <value>classpath*:/hibernate/hibernate-sql.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="hibernate.show_sql">false</prop> <prop key="hibernate.format_sql">false</prop> <prop key="hibernate.hbm2ddl.auto">none</prop> <!-- 開啟查詢緩存 --> <prop key="hibernate.cache.use_query_cache">false</prop> <!-- 配置二級緩存 --> <prop key="hibernate.cache.use_second_level_cache">true</prop> <!-- 強制Hibernate以更人性化的格式將數據存入二級緩存 --> <prop key="hibernate.cache.use_structured_entries">true</prop> <!-- Hibernate將收集有助於性能調節的統計數據 --> <prop key="hibernate.generate_statistics">false</prop> <!-- 指定緩存配置文件位置 --> <prop key="hibernate.cache.provider_configuration_file_resource_path">/spring/ehcache.xml</prop> <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop> <prop key="hibernate.current_session_context_class">jta</prop> <prop key="hibernate.transaction.factory_class">org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory</prop> <prop key="hibernate.transaction.manager_lookup_class">com.atomikos.icatch.jta.hibernate3.TransactionManagerLookup</prop> </props> </property> </bean>
public class LocalSessionFactoryBean extends HibernateExceptionTranslator implements FactoryBean<SessionFactory>, ResourceLoaderAware, InitializingBean, DisposableBean { private DataSource dataSource; private Resource[] configLocations; private String[] mappingResources; private Resource[] mappingLocations; private Resource[] cacheableMappingLocations; private Resource[] mappingJarLocations; private Resource[] mappingDirectoryLocations; private Interceptor entityInterceptor; private NamingStrategy namingStrategy; private Object jtaTransactionManager; private Object multiTenantConnectionProvider; private Object currentTenantIdentifierResolver; private RegionFactory cacheRegionFactory; private Properties hibernateProperties; private Class<?>[] annotatedClasses; private String[] annotatedPackages; private String[] packagesToScan; private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); private Configuration configuration; private SessionFactory sessionFactory;
那其實呢,sessionFactory是LocalSessionFactoryBean對象的一個屬性,這點可以在LocalSessionFactoryBean類中可以看到,至於bean的注入為何是class的屬性而非class本身,那是因為它實現了 FactoryBean<SessionFactory> 接口。sessionFacotry是由LocalSessionFactoryBean對象配置后生成的。生成后將sessionFactory對象注入到了spring容器,且僅此一個而已,默認單例嘛。
我們對數據庫的操作都是用session對象,它是由sessionFactory對象生成的。下面是sessionFactory對象的兩個方法:
/** * Open a {@link Session}. * <p/> * JDBC {@link Connection connection(s} will be obtained from the * configured {@link org.hibernate.service.jdbc.connections.spi.ConnectionProvider} as needed * to perform requested work. * * @return The created session. * * @throws HibernateException Indicates a problem opening the session; pretty rare here. */ public Session openSession() throws HibernateException; /** * Obtains the current session. The definition of what exactly "current" * means controlled by the {@link org.hibernate.context.spi.CurrentSessionContext} impl configured * for use. * <p/> * Note that for backwards compatibility, if a {@link org.hibernate.context.spi.CurrentSessionContext} * is not configured but JTA is configured this will default to the {@link org.hibernate.context.internal.JTASessionContext} * impl. * * @return The current session. * * @throws HibernateException Indicates an issue locating a suitable current session. */ public Session getCurrentSession() throws HibernateException;
那我們的項目使用getCurrentSession()獲取session對象的。
hibernate interceptor怎么配置呢?
LocalSessionFactoryBean對象的entityInterceptor屬性可以配置,你可以在xml中配置它,加到sessionFactory這個bean的xml配置中去。
<property name="entityInterceptor"> <bean class="com.my.pay.common.AutoTableNameInterceptor"/> </property>
那,它只能配置一個。因為sessionFactory是單例,他也只能是單例,引用sessionFactory的Dao對像也是單例,service,controller通通都是單例。那么有個問題就是,動態替換表名,如何動態?動態多例這條路已經封死了。那只剩下,動態修改interceptor對象的值。聽起來像是不錯的建議。我嘗試后只能以失敗告終,無法解決線程安全問題!待會兒描述原因。
所以配置到xml中無法實現我的需求。那么就只能在代碼中設置了,還好sessionFactory對象提供了我們修改它的入口。
@Resource(name = "sessionFactory") private SessionFactory sessionFactory; protected Session getSession(){ if(autoTableNameInterceptorThreadLocal.get() == null){ return this.sessionFactory.getCurrentSession(); }else{ SessionBuilder builder = this.sessionFactory.withOptions().interceptor(autoTableNameInterceptorThreadLocal.get()); Session session = builder.openSession(); return session; } }
/** * 線程域變量,高效實現線程安全(一個請求對應一個thread) */ private ThreadLocal<AutoTableNameInterceptor> autoTableNameInterceptorThreadLocal = new ThreadLocal<>(); public List<WfPayLog> find(Long merchantId, Long poolId,String sdk, Long appId,String province, Integer price, String serverOrder, String imsi,Integer iscallback,String state, Date start, Date end, Paging paging) { 。。。。 //定制表名攔截器,設置到線程域 autoTableNameInterceptorThreadLocal.set(new AutoTableNameInterceptor("wf_pay_log","wf_pay_log_"+ DateUtil.formatDate(start,DateUtil.YEARMONTH_PATTERN))); List<WfPayLog> wfPayLogs; if (paging == null) { wfPayLogs = (List<WfPayLog>) find(hql.toString(), params); //find方法里面有 this.getSession().createQuery("hql") 等方法
} else {
wfPayLogs = (List<WfPayLog>) findPaging(hql.toString(), "select count(*) " + hql.toString(), params, paging);
}
return wfPayLogs;
}
紅色標識的代碼就是核心代碼,核心說明。意思是,在DAO層對象中,注入sessionFactory對象創建session就可以操作數據庫了,我們改變了session的獲取方式。當需要改變表名的時候,我們定義線程域變量,在需要interceptor的時候將interceptor對象保存到線程域中去,然后你操作的時候再拿到這個配置有攔截器的session去操作數據庫,這個時候interceptor就生效了。
不用線程域變量保存,直接定義對象成員變量肯定是不行的,因為會有並發問題(多個請求(線程)同時調用dao方法,dao方法執行的時候又調用getSession()方法,可能當你getSession的時候,別的請求,已經把interceptor給換掉了。),當然用synchronized也可以解決。線程域的使用,比synchronized同步鎖高效得多。線程域的使用,保證了interceptor對象和請求(線程)是綁在一起的,dao方法的執行,只要執行語句在同一個線程內,線程所共享的對象信息肯定一致的,所以不存在並發問題。
上面曾說過,單例interceptor不行,原因是:無法解決線程安全問題。 AutoTableNameInterceptor是一個單例,你在dao層可以修改他的值,比如新增set操作,沒問題。可是你set的同時,別的請求也在set,就會導致destName,srcName的值一直在變動,除非你的請求是串行的(排隊的,一個一個來的)。而且可能n個dao實例都會調用interceptor, 你怎么實現線程同步?除非你在dao操作的時候鎖住整個interceptor對象,這個多影響性能! 使用線程域,沒法實現,經過測試,發現hibernate底層會有多個線程調用interceptor方法,而不是我們的請求線程!所以,從dao到interceptor已經不是一個線程。interceptor的onPrepareStatement回調方法又是如此的單調,功能有限,哎。再說了,使用單例,是sessionFactory的全局配置,影響效率,通過代碼添加是臨時性的。代碼添加僅僅是添加到這個session而已,這點可以從源碼看出。下面貼出源碼
public interface SessionFactoryImplementor extends Mapping, SessionFactory { } public final class SessionFactoryImpl implements SessionFactoryImplementor { @Override public SessionBuilder withOptions() { return new SessionBuilderImpl( this ); } static class SessionBuilderImpl implements SessionBuilder { private final SessionFactoryImpl sessionFactory; private Interceptor interceptor; private Connection connection; private ConnectionReleaseMode connectionReleaseMode; private boolean autoClose; private boolean autoJoinTransactions = true; private boolean flushBeforeCompletion; private String tenantIdentifier; SessionBuilderImpl(SessionFactoryImpl sessionFactory) { this.sessionFactory = sessionFactory; final Settings settings = sessionFactory.settings; // set up default builder values... this.interceptor = sessionFactory.getInterceptor(); this.connectionReleaseMode = settings.getConnectionReleaseMode(); this.autoClose = settings.isAutoCloseSessionEnabled(); this.flushBeforeCompletion = settings.isFlushBeforeCompletionEnabled(); } protected TransactionCoordinatorImpl getTransactionCoordinator() { return null; } @Override public Session openSession() { return new SessionImpl( connection, sessionFactory, getTransactionCoordinator(), autoJoinTransactions, sessionFactory.settings.getRegionFactory().nextTimestamp(), interceptor, flushBeforeCompletion, autoClose, connectionReleaseMode, tenantIdentifier ); } @Override public SessionBuilder interceptor(Interceptor interceptor) { this.interceptor = interceptor; return this; } @Override public SessionBuilder noInterceptor() { this.interceptor = EmptyInterceptor.INSTANCE; return this; } @Override public SessionBuilder connection(Connection connection) { this.connection = connection; return this; } @Override public SessionBuilder connectionReleaseMode(ConnectionReleaseMode connectionReleaseMode) { this.connectionReleaseMode = connectionReleaseMode; return this; } @Override public SessionBuilder autoJoinTransactions(boolean autoJoinTransactions) { this.autoJoinTransactions = autoJoinTransactions; return this; } @Override public SessionBuilder autoClose(boolean autoClose) { this.autoClose = autoClose; return this; } @Override public SessionBuilder flushBeforeCompletion(boolean flushBeforeCompletion) { this.flushBeforeCompletion = flushBeforeCompletion; return this; } @Override public SessionBuilder tenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; return this; } } }
代碼中給出了從sessionFactory->openSession的過程,sessionFacotry->withOptions->sessionBuilder->openSession->session,new SessionImpl構造出了session對象,內部也沒有針對sessionFactory的修改(代碼沒粘貼),所以withOptions的核心功能是,利用已有的sessionFacotry構造出特定的session。
3.經過多翻測試,還發現一個問題
spring對http請求的處理,采用的是線程池,並不是每個請求單獨重新創建一個線程。即請求與線程的關系是多對一,不是一對一。這樣就帶來一個問題,因為ThreadLocal的綁定對象是線程Thread,因為線程池的關系,同一個線程綁定的數據,在不同的請求中都可以獲取到。
因為項目中,對表名的替換有采用hql的,也用了sql的,且同時出現在同一個類中。就是說同一個Dao對象中的兩個方法,一個使用hql,一個使用sql查詢,分別對應session.createQuery 和 session.createSQLQuery。可惜hibernate interceptor是對session的所有操作都攔截。因為我們對普通的sql查詢,采用的是直接修改表名的方式,並不想采用hibernate interceptor策略去修改。故而,導致普通的查詢方式,表名被替換了兩次,一次自己的主動修改,一次interceptor。這肯定不行,解決方法如下:
移除interceptor,我上面是通過threadLocal的值判斷是否添加interceptor的,所以移除threadLocal即可。在find方法return前,remove掉。
autoTableNameInterceptorThreadLocal.remove();
這樣,即使在同一個類中,同一個threadLocal,不同查詢方式,因為調用不同的session,而做到互不干擾。核心關鍵就是我們針對ThreadLocal這個全局變量值的設定操作完后及時移除了。
原來一直以為,每個請求會新建線程去處理的,媽的,又被坑了一次。線程池真是個坑貨。所以所,ThreadLocal雖然解決了並發問題,不一定真正解決了你的問題,你的問題還可能是線程內問題!像這個就是線程內問題。多個請求,多次請求均可能被此線程處理,全局變量的使用,實在是危險至極!
4.spring+hibernate版本
<properties>
<hibernate.version>4.1.0.Final</hibernate.version>
<spring.version>4.0.0.RELEASE</spring.version>
</properties>
5.參考文章
http://blog.csdn.net/meng2602956882/article/details/22914493
https://my.oschina.net/cloudcross/blog/831277
http://liuguxing.iteye.com/blog/889448
http://blog.csdn.net/qq_24489717/article/details/70147100
http://redhat.iteye.com/blog/1057974
http://ks2144634.blog.163.com/blog/static/13358550320109895135535/
http://blog.csdn.net/unifirst/article/details/50482031