写点什么

【程序大侠传】大表分库分表切换数据库类型导致 pagehelper 生成 sql 语法报错

作者:Disaster
  • 2024-07-08
    上海
  • 本文字数:12539 字

    阅读完需:约 41 分钟

【程序大侠传】大表分库分表切换数据库类型导致pagehelper生成sql语法报错

前序

代码剑宗等级分明,其门下弟子等级划分如下:


  1. 入门弟子刚刚拜入代码剑宗,学习基础编程语言和基本剑法(语法和基础概念)。他们的代码还显得生涩,但已经开始展现出对优雅代码的追求。

  2. 江湖小虾初步掌握了几种编程语言,能够写出基本的算法剑法(简单算法)。他们的代码开始变得简洁高效,但还需要更多的实战经验。

  3. 江湖侠客在代码剑宗中已经小有名气,能够独立完成复杂的项目。他们的算法剑法(算法优化)和架构心法(系统设计)已经初见成效,代码如行云流水,功能完备。

  4. 武林高手技术炉火纯青,精通多种编程语言和框架,他们的代码不仅高效,而且极具美感。无论是算法剑法还是架构心法,他们都能运用自如,解决各种复杂的技术难题。

  5. 武林宗师在代码剑宗中享有极高的声望,擅长传授弟子,指导团队。他们在算法优化和系统架构设计方面有独到的见解,能够引领技术方向,推动团队进步。

  6. 武林至尊代码剑宗的巅峰存在,技术造诣无人能及。他们的代码不仅极致高效,还能预见未来的技术趋势,推动整个行业的发展。每一行代码都如同绝世剑招,令人叹为观止。

  7. 绝世神功达到了编程和算法的终极境界,代码剑宗的传说人物。他们的技术超越了凡人的理解,能够创造出前所未有的奇迹。江湖中流传着他们的神话,后辈们都以他们为榜样,追随他们的步伐。


在代码剑宗中,越高级别,能够修炼的功法与接收到的任务就越深奥,也正是如此,代码剑宗中的弟子每个人都想提升自己的级别,而级别的提升主要由个人的积分所决定,主流的获取积分大致有两种方式,一种是通过不断接宗门内的任务不断获取宗门的积分,而任务越难积分也就越多,另一种是由门派武林至尊主动跟门派内的长老去申请。当然还有其他的提升级别的方式,如:换门派等,不过这些方式有一定的风险。而我们的主角阿强经过长达 2 年多的修炼,目前成为了一名武林高手。虽然在门派中的等级不低,但是由于其所在的部门大多是武林宗师、武林至尊级别,因此阿强一直没觉得自己的职级有多高,反而觉得自己的职级太低。但也正是在这种环境下,阿强一直在想法设法地去接一些难度高的任务去获取积分。而门派中除了自己去接受的任务之外,每个人每半个月都会统一分配一定积分点的任务,这些任务积分不会很多。偶尔有一些积分多的任务,往往都被抢走,除了极个别的那种难度很高的任务没什么人去接之外,其他的稍微难度低一点,积分高的任务都是刚一出来就被领取。而那种突发又比较紧急的任务往往积分高,这种任务一般需要接任务的人在某一方面的能力比较突出。没有这方面能力的人接这种任务往往完不成,而一旦没有完成则是会扣个人积分,而所扣积分的多少是由此任务的紧急程度决定。

第三章 什么?sql 语法报错了?

上次阿强略微出手解决了 NPE 的问题之后,就回到洞府继续思考阿汝提出的需求,正当他完成需求落地的方案正打算把需求的排期给到天工阁小凯时,脑海里就听到门派的紧急任务传音:“警告,F 服务线上出现大量 sql 语法错误,请及时处理!!”,阿强听到此传音后,风紧扯乎地查看了一些此任务的难度与解决完的积分奖励,阿强连忙接下了此任务。阿强这么快接下来这任务是因为他对于 F 服务是比较熟悉的,之前接一些任务的时候有过 F 服务的开发经验。看到任务负责人变成自己后,阿强把需求排期的传音发给小凯后便打开任务查看具体内容,10 分钟后....,阿强眼神闪烁,心里则是在想,大表分库分表切换怎么会影响 pagehelper 的分页 sql 的生成?半响后,阿强摇摇了头,心里甩掉一些无用的心绪。阿强使用了天书法器,开始查看起了 F 服务的 error 日志,不一会他就找到了 sql 语法报错的输出日志。


Caused by: org.postgresql.util.PSQLException: ERROR: LIMIT #,# syntax is not supported  Hint: Use separate LIMIT and OFFSET clauses.  Position: 447at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2455)at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2155)at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:288)at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:430)at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:356)at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:168)at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:116)at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)at com.xxx.hbdl.core.jdbc.BasePreparedStatement.executeQuery(BasePreparedStatement.java:513)at com.xxx.hbdl.atom.wrapper.AtomPreparedStatementWrapper.lambda$executeQuery$1(AtomPreparedStatementWrapper.java:56)at io.micrometer.core.instrument.composite.CompositeTimer.recordCallable(CompositeTimer.java:68)at com.xxx.bdl.atom.AtomExecutionTemplate.execute(AtomExecutionTemplate.java:93)at com.xxx.bdl.atom.wrapper.AtomStatementWrapper$1.executeSql(AtomStatementWrapper.java:144)at com.xxx.bdl.core.jdbc.filter.DefaultJdbcFilterChain.executeSql(DefaultJdbcFilterChain.java:61)at com.xxx.bdl.atom.jdbc.filter.HhJdbcFilter.executeSql(HahasJdbcFilter.java:35)at com.xxx.bdl.core.jdbc.filter.DefaultJdbcFilterChain.executeSql(DefaultJdbcFilterChain.java:59)at com.xxx.bdl.core.jdbc.filter.JdbcFilter.executeSql(JdbcFilter.java:81)at com.xxx.bdl.core.jdbc.filter.DefaultJdbcFilterChain.executeSql(DefaultJdbcFilterChain.java:59)at com.xxx.bdl.atom.wrapper.AtomStatementWrapper.executeInternal(AtomStatementWrapper.java:147)at com.xxx.bdl.atom.wrapper.AtomPreparedStatementWrapper.executeQuery(AtomPreparedStatementWrapper.java:57)at com.xxx.bdl.group.GroupExecutor.executeQuery(GroupExecutor.java:90)at com.xxx.bdl.group.GroupPst.executeQuery(GroupPst.java:97)at com.xxx.bdl.group.GroupPst.execute(GroupPst.java:109)at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63)at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)at sun.reflect.GeneratedMethodAccessor287.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)at com.sun.proxy.$Proxy460.query(Unknown Source)at sun.reflect.GeneratedMethodAccessor287.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)at com.sun.proxy.$Proxy460.query(Unknown Source)at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:326)at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)at sun.reflect.GeneratedMethodAccessor315.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)at com.hellobike.druid.mybatis.plugin.SqlMonitorInterceptor.intercept(SqlMonitorInterceptor.java:42)at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)at com.sun.proxy.$Proxy459.query(Unknown Source)at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:136)at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)at com.sun.proxy.$Proxy459.query(Unknown Source)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)at sun.reflect.GeneratedMethodAccessor706.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426)... 112 common frames omitted
复制代码


从日志中,除了武侠世界中常见的 spring、mybatis 等灵域框架,F 系统还基于 mybatis 框架针对自身情况进行了封装,上述日志中输出的堆栈中包含 dbl 前限定名的即是封装后 mybatis 框架。优化后的 mybatis 框架并不影响 pagehelper 插件的使用,因此阿强并没有过多地关注此灵域框架,直接就将重点放在了“org.postgresql.util.PSQLException: ERROR: LIMIT #,# syntax is not supported”上,这句话表示 pg 数据库不支持 LIMIT 的写法,为此,阿强特意去找来了 mysql 与 pg 两种数据库类型支持的分页 sql 写法。


-- MySQL分页语法    SELECT * from tableName where  1=1 limit 10 offset 10;    SELECT * from tableName where  1=1 limit 0 , 10;    SELECT * from tableName where  1=1 limit 10;
复制代码


-- PG分页语法SELECT * FROM t_privilege_role limit 10 offset 10;SELECT * FROM t_privilege_role offset 10 limit 10;
复制代码


从上面不难看出 MYSQL 兼容 PG 的分页 sql 语法名,但是 PG 并不支持 MYSQL 中的 LIMIT xx 的写法,但是从阿强的印象中,F 系统一直都是使用的 PG 数据库,联想到大表分库分表的背景,他特意跑去了青云台(守护盟所维护的系统)看了 F 系统目前使用的数据库类型,果不其然,F 系统目前除了 PG 数据库,还使用了 MYSQL 数据库。此时的阿强猜测是因为大表分库分表所导致的此次问题。但是任务可不只是单纯地知道是什么导致的就可以的,还需要解决这个问题。阿强通过法器 IDEA 打开了 F 项目的代码,查看了 PageHelper 的版本和项目配置如下:


版本:5.1.4项目配置(yml类型):spring:  datasource.druid.stat-view-servlet.enabled: false  jackson:    date-format: yyyy-MM-dd HH:mm:ss    time-zone: GMT+8  application.name: F  pagehelper:    helperDialect: postgresql    reasonable: true    supportMethodsArguments: true    params: count=countSql
复制代码


阿强查看完配置,配置的是 pg,按道理来说,所有的分页 sql 都应该是用的 pg 的语法,但实际情况却不是如此。再思考到 F 系统的问题,F 系统此次上线报错并不是所有的节点都报错,而只是其中的一台节点报错,其他节点都在正常运行。也就是说,pagehelper 只有在这台报错的节点生成 limit xxx 分页 sql,其他节点生成的 sql 同时支持 mysql 与 pg,那也就是说,报错的那台节点生成的分页 sql 是用的 mysql 语法。那么为什么 pagehelper 会去选择使用 mysql 的语法格式生成分页 sql 呢,为了弄懂这块逻辑,阿强用 idea 打开了 pagehelper 的代码,1 小时后,阿强已大致知晓 pagehelper 生成分页 sql 的原理,其中涉及到本次问题导致的核心逻辑在与方言的获取这块的逻辑上面,而 pagehelper 对于方言的处理的入口则是在 PageInterceptor 中的 intercept 中:


@Override    public Object intercept(Invocation invocation) throws Throwable {        try {            Object[] args = invocation.getArgs();            MappedStatement ms = (MappedStatement) args[0];            Object parameter = args[1];            RowBounds rowBounds = (RowBounds) args[2];            ResultHandler resultHandler = (ResultHandler) args[3];            Executor executor = (Executor) invocation.getTarget();            CacheKey cacheKey;            BoundSql boundSql;            //由于逻辑关系,只会进入一次            if (args.length == 4) {                //4 个参数时                boundSql = ms.getBoundSql(parameter);                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);            } else {                //6 个参数时                cacheKey = (CacheKey) args[4];                boundSql = (BoundSql) args[5];            }            checkDialectExists();            //对 boundSql 的拦截处理            if (dialect instanceof BoundSqlInterceptor.Chain) {                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);            }            List resultList;            //调用方法判断是否需要进行分页,如果不需要,直接返回结果            if (!dialect.skip(ms, parameter, rowBounds)) {                //开启debug时,输出触发当前分页执行时的PageHelper调用堆栈                // 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全                debugStackTraceLog();                Future<Long> countFuture = null;                //判断是否需要进行 count 查询                if (dialect.beforeCount(ms, parameter, rowBounds)) {                    if (dialect.isAsyncCount()) {                        countFuture = asyncCount(ms, boundSql, parameter, rowBounds);                    } else {                        //查询总数                        Long count = count(executor, ms, parameter, rowBounds, null, boundSql);                        //处理查询总数,返回 true 时继续分页查询,false 时直接返回                        if (!dialect.afterCount(count, parameter, rowBounds)) {                            //当查询总数为 0 时,直接返回空的结果                            return dialect.afterPage(new ArrayList(), parameter, rowBounds);                        }                    }                }                resultList = ExecutorUtil.pageQuery(dialect, executor,                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);                if (countFuture != null) {                    Long count = countFuture.get();                    dialect.afterCount(count, parameter, rowBounds);                }            } else {                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);            }            return dialect.afterPage(resultList, parameter, rowBounds);        } finally {            if (dialect != null) {                dialect.afterAll();            }        }    }
复制代码


而其中方言的初始化入口则是在 intercept 中调用的 checkDialectExists 中:


private void checkDialectExists() {        if (dialect == null) {            synchronized (default_dialect_class) {                if (dialect == null) {                    setProperties(new Properties());                }            }        }    }

@Override public void setProperties(Properties properties) { //缓存 count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); //在这里指定方言处理器,通过反射拿到具体实例 String dialectClass = properties.getProperty("dialect"); if (StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } Dialect tempDialect = ClassUtil.newInstance(dialectClass, properties); //方言初始化入口 tempDialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix"); if (StringUtil.isNotEmpty(countSuffix)) { this.countSuffix = countSuffix; }
// debug模式,用于排查不安全分页调用 debug = Boolean.parseBoolean(properties.getProperty("debug"));
// 通过 countMsId 配置自定义类 String countMsIdGenClass = properties.getProperty("countMsIdGen"); if (StringUtil.isNotEmpty(countMsIdGenClass)) { countMsIdGen = ClassUtil.newInstance(countMsIdGenClass, properties); } // 初始化完成后再设置值,保证 dialect 完成初始化 dialect = tempDialect; }
复制代码


最终方言的初始化如果没有指定通过 dialect 配置指定 dialectClass,则会进入 PageHelper 这个类中的 setProperties 方法中:


//类:PageHelper@Override    public void setProperties(Properties properties) {        setStaticProperties(properties);        pageParams = new PageParams();        autoDialect = new PageAutoDialect();        pageBoundSqlInterceptors = new PageBoundSqlInterceptors();        pageParams.setProperties(properties);        autoDialect.setProperties(properties);        pageBoundSqlInterceptors.setProperties(properties);        //20180902新增 aggregateFunctions, 允许手动添加聚合函数(影响行数)        CountSqlParser.addAggregateFunctions(properties.getProperty("aggregateFunctions"));        // 异步 asyncCountService 并发度设置,这里默认为应用可用的处理器核心数 * 2,更合理的值应该综合考虑数据库服务器的处理能力        int asyncCountParallelism = Integer.parseInt(properties.getProperty("asyncCountParallelism",                "" + (Runtime.getRuntime().availableProcessors() * 2)));        asyncCountService = new ForkJoinPool(asyncCountParallelism,                pool -> {                    final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);                    worker.setName("pagehelper-async-count-" + worker.getPoolIndex());                    return worker;                }, null, true);    }
复制代码


autoDialect#setProperties 中的逻辑:


public void setProperties(Properties properties) {
this.properties = properties; //初始化自定义AutoDialect initAutoDialectClass(properties); //使用 sqlserver2012 作为默认分页方式,这种情况在动态数据源时方便使用 String useSqlserver2012 = properties.getProperty("useSqlserver2012"); if (StringUtil.isNotEmpty(useSqlserver2012) && Boolean.parseBoolean(useSqlserver2012)) { registerDialectAlias("sqlserver", SqlServer2012Dialect.class); registerDialectAlias("sqlserver2008", SqlServerDialect.class); } initDialectAlias(properties); //指定的 Helper 数据库方言,和 不同 String dialect = properties.getProperty("helperDialect"); //运行时获取数据源 String runtimeDialect = properties.getProperty("autoRuntimeDialect"); //1.动态多数据源 if (StringUtil.isNotEmpty(runtimeDialect) && "TRUE".equalsIgnoreCase(runtimeDialect)) { this.autoDialect = false; } //2.动态获取方言 else if (StringUtil.isEmpty(dialect)) { autoDialect = true; } //3.指定方言 else { autoDialect = false; this.delegate = instanceDialect(dialect, properties); } }
复制代码


到这里方言的初始化就完成了,而分页 sql 的生成则是在 PageInterceptor#intercept 中:


@Override    public Object intercept(Invocation invocation) throws Throwable {    //do something......resultList = ExecutorUtil.pageQuery(dialect, executor,                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);   //do something......}
/** * 分页查询 * * @param dialect * @param executor * @param ms * @param parameter * @param rowBounds * @param resultHandler * @param boundSql * @param cacheKey * @param <E> * @return * @throws SQLException */ public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException { //判断是否需要进行分页查询 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分页的缓存 key CacheKey pageKey = cacheKey; //处理参数对象 parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //调用方言获取分页 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql); //设置动态参数 for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //对 boundSql 的拦截处理 if (dialect instanceof BoundSqlInterceptor.Chain) { pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey); } //执行分页查询 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不执行分页的情况下,也不执行内存分页 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } }
复制代码


其中 dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);这句会执行到 com.github.pagehelper.PageHelper#getPageSql(MappedStatement, BoundSql, java.lang.Object, RowBounds, CacheKey)方法中:


@Override    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {        return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);    }
复制代码


而 autoDialect 的初始化则是在 com.github.pagehelper.PageHelper#skip 方法中,触发点同样是在 PageInterceptor#intercept


public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {        Page page = pageParams.getPage(parameterObject, rowBounds);        if (page == null) {            return true;        } else {            //设置默认的 count 列            if (StringUtil.isEmpty(page.getCountColumn())) {                page.setCountColumn(pageParams.getCountColumn());            }            //设置默认的异步 count 设置            if (page.getAsyncCount() == null) {                page.setAsyncCount(pageParams.isAsyncCount());            }            autoDialect.initDelegateDialect(ms, page.getDialectClass());            return false;        }    }/**     * 多数据动态获取时,每次需要初始化,还可以运行时指定具体的实现     *     * @param ms     * @param dialectClass 分页实现,必须是 {@link AbstractHelperDialect} 实现类,可以使用当前类中注册的别名,例如 "mysql", "oracle"     */    public void initDelegateDialect(MappedStatement ms, String dialectClass) {        if (StringUtil.isNotEmpty(dialectClass)) {            AbstractHelperDialect dialect = urlDialectMap.get(dialectClass);            if (dialect == null) {                lock.lock();                try {                    if ((dialect = urlDialectMap.get(dialectClass)) == null) {                        dialect = instanceDialect(dialectClass, properties);                        urlDialectMap.put(dialectClass, dialect);                    }                } finally {                    lock.unlock();                }            }            dialectThreadLocal.set(dialect);        } else if (delegate == null) {            if (autoDialect) {                this.delegate = autoGetDialect(ms);            } else {              //如果没有设置动态多数据源、动态获取方言则会进入此方法                dialectThreadLocal.set(autoGetDialect(ms));            }        }    }/**     * 自动获取分页方言实现     *     * @param ms     * @return     */    public AbstractHelperDialect autoGetDialect(MappedStatement ms) {        DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();        Object dialectKey = autoDialectDelegate.extractDialectKey(ms, dataSource, properties);        if (dialectKey == null) {            return autoDialectDelegate.extractDialect(dialectKey, ms, dataSource, properties);        } else if (!urlDialectMap.containsKey(dialectKey)) {            lock.lock();            try {                if (!urlDialectMap.containsKey(dialectKey)) {                    urlDialectMap.put(dialectKey, autoDialectDelegate.extractDialect(dialectKey, ms, dataSource, properties));                }            } finally {                lock.unlock();            }        }        return urlDialectMap.get(dialectKey);    }
复制代码


从上述代码我们知道 Dialect 如果没有指定 dialectClass 那就会通过数据库的连接去自动获取分页方言实现。其中 initDelegateDialect 中的代码需要格外关注:


if (autoDialect) {                this.delegate = autoGetDialect(ms);            } else {              //如果没有设置动态多数据源、动态获取方言则会进入此方法,              //也就是说,如果没有配置helperDialect与autoRuntimeDialect这两个配置,              //那么在多数据源的场景下pagehelper永远会拿执行sql中的第一个ds去动态获取dialect                dialectThreadLocal.set(autoGetDialect(ms));            }
复制代码


分析到这里,阿强已经完整地了解到了 pagehelper 中方言的获取与方言对于分页 sql 的处理原理,此时他已经知道发生此次问题的根因所在,但是为了验证自己心中所想,他在 F 系统中写了两个测试方法:


//案例一:先pg查询,后mysql查询    @Test    public void queryTest(){        PageHelper.startPage(1, 1);        List<PGBean> list = pgRepository.select();        PageInfo<MySqlBean> page = new PageInfo(list);
System.out.println(JSON.toJSONString(page));
Result<List<Response>> result = mysqlRepository.select (); System.out.println(JSON.toJSONString(result)); }//案例二:先mysql查询,后pg查询 @Test public void queryTest(){ PageHelper.startPage(1, 1); Result<List<Response>> result = mysqlRepository.select (); System.out.println(JSON.toJSONString(result)); List<PGBean> list = pgRepository.select(); PageInfo<MySqlBean> page = new PageInfo(list);
System.out.println(JSON.toJSONString(page));
}
复制代码


在测试环境执行后,其中案例一正常运行,案例二报一样的错误。此时真相大白,yaml 配置有问题!


spring:  datasource.druid.stat-view-servlet.enabled: false  jackson:    date-format: yyyy-MM-dd HH:mm:ss    time-zone: GMT+8  application.name: F  pagehelper:    helperDialect: postgresql    reasonable: true    supportMethodsArguments: true    params: count=countSql
复制代码


阿强又仔细检查了一下配置,发现 pagehelper 的配置项多了一个 spring 的前缀,难怪 pagehelper 配置了 Pg,但是却用的 MYSQL 语法生成分页 sql,原来是配置没有生效。发现问题之后的阿强轻叹了一口气,又传音给了负责此项目的分库分表的师兄弟小济,小济收到我的消息后,不一会就回复说,“此配置自 F 系统创建以来就有了,分库分表的时候根本没有会想到这个配置会有问题!”。阿强听完摇摇头,然后将此任务结束后就又开始进行棘手需求的开发

发布于: 2024-07-08阅读数: 2
用户头像

Disaster

关注

talk is cheap,show me the code 2021-12-29 加入

A coder who likes open source, graduated from Jishou University with a major in computer science and technology, has worked in the field of network security and Android, and is now constantly explorin

评论

发布
暂无评论
【程序大侠传】大表分库分表切换数据库类型导致pagehelper生成sql语法报错_Disaster_InfoQ写作社区