spring hibernate實現動態替換表名(分表)

1.概述spring

其實最簡單的辦法就是使用原生sql,如 session.createSQLQuery("sql"),或者使用jdbcTemplate。可是項目中已經使用了hql的方式查詢,修改起來又累,風險又大!因此,必須找到一種比較好的解決方案,實在不行再改寫吧!通過3天的時間的研究,終於找到一種不錯的方法,下面講述之。sql

 

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去session

先看一下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對象的。app

 

hibernate interceptor怎麼配置呢?ide

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

相關文章
相關標籤/搜索