博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
spring hibernate实现动态替换表名(分表)
阅读量:4698 次
发布时间:2019-06-09

本文共 12199 字,大约阅读时间需要 40 分钟。

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是个啥东西。

com.my.pay.task.entity
com.my.pay.paycms.entity
com.my.pay.data.entity.payincome
classpath*:/hibernate/hibernate-sql.xml
org.hibernate.dialect.MySQL5Dialect
false
false
none
false
true
true
false
/spring/ehcache.xml
org.hibernate.cache.ehcache.EhCacheRegionFactory
jta
org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory
com.atomikos.icatch.jta.hibernate3.TransactionManagerLookup
public class LocalSessionFactoryBean extends HibernateExceptionTranslator        implements FactoryBean
, 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
接口。sessionFacotry是由LocalSessionFactoryBean对象配置后生成的。生成后将sessionFactory对象注入到了spring容器,且仅此一个而已,默认单例嘛。 我们对数据库的操作都是用session对象,它是由sessionFactory对象生成的。下面是sessionFactory对象的两个方法:
/**     * Open a {
@link Session}. *

* 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. *

* 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配置中去。

那,它只能配置一个。因为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
autoTableNameInterceptorThreadLocal = new ThreadLocal<>();public List
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
wfPayLogs; if (paging == null) { wfPayLogs = (List
) find(hql.toString(), params); //find方法里面有 this.getSession().createQuery("hql") 等方法
    } else {        wfPayLogs = (List
) 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版本

4.1.0.Final
4.0.0.RELEASE
 

5.参考文章

 

转载于:https://www.cnblogs.com/hdwang/p/7307268.html

你可能感兴趣的文章
魔戒三曲,黑暗散去;人皇加冕,光明归来
查看>>
Error和Exception
查看>>
Python和Singleton (单件)模式[转载]
查看>>
httpclient设置proxy与proxyselector
查看>>
IT常用单词
查看>>
拓扑排序
查看>>
NYOJ--32--SEARCH--组合数
查看>>
JMS
查看>>
gulpfile 压缩模板
查看>>
【34.14%】【BZOJ 3110】 [Zjoi2013]K大数查询
查看>>
【 henuacm2016级暑期训练-动态规划专题 A 】Cards
查看>>
第五篇:白话tornado源码之褪去模板的外衣
查看>>
设备常用框架framework
查看>>
bootstrap模态框和select2合用时input无法获取焦点(转)
查看>>
21世纪经济网APP
查看>>
解决NetworkOnMainThreadException
查看>>
1039 到底买不买
查看>>
农银电商项目学习笔记(一)
查看>>
MockObject
查看>>
Chukwa
查看>>