【程序大侠传】大表分库分表切换数据库类型导致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: 447
at 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系统创建以来就有了,分库分表的时候根本没有会想到这个配置会有问题!”。阿强听完摇摇头,然后将此任务结束后就又开始进行棘手需求的开发

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/782526.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

驾校管理系统的全面革新与升级

智慧驾校系统是一款专为现代驾校量身定制的综合性管理平台,它深度融合了云计算、大数据、物联网及人工智能等前沿技术,旨在为驾校打造一个高效、智能、便捷的运营生态系统。该系统通过数字化、信息化的手段,彻底革新了传统驾校的管理模式,不仅极大地提升了驾校的运营效率,…

初识Spark

一、简介 官网&#xff1a;Apache Spark™ - Unified Engine for large-scale data analytics Apache的顶级项目&#xff0c;用于大规模数据处理的统一分析引擎。 支持语言&#xff1a;Java、Scala、Python和R (源码为Scala) 高级工具&#xff1a; 1、SparkSQL用于SQL和结构…

ARM汇编与机器码、汇编指令

文章目录 1. CISC与RISC指令集 2. ARM汇编指令 3. 汇编与机器码 4. 汇编指令格式 5. MOV指令 6. BL指令 7. B指令 8. ADD/SUB指令 9. LDR/STR指令 1. CISC与RISC指令集 根据指令的复杂度&#xff0c;所有CPU可以分为两类&#xff1a; CISC&#xff08;Complex Instr…

破局 AI 2.0 时代:利用 AI 提升自我核心竞争力

文章目录 破局 AI 2.0 时代&#xff1a;利用 AI 提升自我核心竞争力1. AI 2.0 时代1.1 特点1.2 发展1.3 影响 2. AI 2.0 时代的机遇 & 挑战2.1 AI 对行业市场的冲击2.2 挑战变为机遇2.3 不同场景下的 AI 效能提升2.3.1 自动化办公任务2.3.2 提升学习效率2.3.3 创意生成与内…

SpringBoot彩蛋之定制启动画面

写在前面 在日常开发中&#xff0c;我们经常会看到各种各样的启动画面。例如以下几种 ① spring项目启动画面 ② mybatisplus启动画面 ③若依项目启动画面 还有很多各式各样好看的启动画面&#xff0c;那么怎么定制这些启动画面呢&#xff1f; 一、小试牛刀 ① 新建一个Spr…

【分布式系统三】监控平台Zabbix对接grafana(截图详细版)

目录 一.安装grafana并启动 二.浏览器访问 三.导入zabbix数据&#xff0c;对接grafana 四.如何导入模版 以前两篇博客为基础 【分布式系统】监控平台Zabbix介绍与部署&#xff08;命令截图版&#xff09;-CSDN博客 【分布式系统】监控平台Zabbix自定义模版配置-CSDN博客 …

前端面试题(CSS篇五)

一、设备像素、css 像素、设备独立像素、dpr、ppi 之间的区别&#xff1f; 设备像素指的是物理像素&#xff0c;一般手机的分辨率指的就是设备像素&#xff0c;一个设备的设备像素是不可变的。 css像素和设备独立像素是等价的&#xff0c;不管在何种分辨率的设备上&#xff0c;…

网络连接线相关问题

问题1&#xff1b; 直通线为什么两头都是T568B&#xff1f;是否可以两台T5568A&#xff1f;或者任意线序&#xff0c;只需两头一致&#xff1f; 不行&#xff0c;施工规范规定。&#xff08;原因&#xff1b;网线最长距离100m&#xff0c;实际用起来要把网线包管&#xff0c;走…

Kafka第四篇——生产数据总体概括,源码解析分区策略,数据收集器,Sender发送线程,key值

目录 流程图以及总体概述 拦截器 分区器以及分区计算策略 为啥进行分区计算&#xff1f; producer生产者怎么知道有哪些分区&#xff1f; 分区计算 如何自定义实现分区器&#xff1f; 想说的在图里啦&#xff01;宝宝&#xff01;&#x1f4a1; ​编辑 如果key值忘记传递了呢&a…

前端后花园周刊vol.18-React Native 称唯一推荐的社区框架是Expo

⚡️行业动态 React Native 团队推荐使用 Expo 框架构建应用程序 React Native 发文称&#xff1a;唯一推荐的社区框架是Expo&#xff0c;Expo 的开发者从 React Native 早期就开始支持 React Native 生态系统&#xff0c;相信 Expo 提供的开发者体验是同类中最好的。 &…

vscode调试教程

VSCode调试 VSCode Debuggers VSCode使用launch.json进行细粒度的控制&#xff0c;可以启动程序或将其附加到复杂的调试场景中 打开Run and Debug视图Ctrl Shift D 点击create a launch.json file&#xff0c;选择C(GDB/LLDB) 会在工作目录自动创建.vscode/launch.json文…

简单的基追踪一维信号降噪方法(MATLAB 2018)

基追踪法是基于冗余过完备字典下的一种信号稀疏表示方法。该方法具有可提高信号的稀疏性、实现阈值降噪和提高时频分辨率等优点。基追踪法采用表示系数的范数作为信号来度量稀疏性&#xff0c;通过最小化l型范数将信号稀疏表示问题定义为一类有约束的极值问题&#xff0c;进而转…

【linux服务器】大语言模型实战教程:LLMS大模型部署到个人服务器或嵌入式开发板(保姆级教学)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引言 说到大语言模型相信大家都不会陌生&#xff0c;大型语言模型(LLMs)是人工智能文本处理的主要类型,也现在最流行的人工智能…

julia系列17: tsp问题代码整理

1. 常用库和基础函数 这里是优化版的函数&#xff1a; using TSPLIB,LKH,Distances,PyPlot MaxNum 10000 tspreadTSPLIB(:att48) dist [round.(Int,euclidean(tsp.nodes[i,:],tsp.nodes[j,:])) for i in 1:tsp.dimension,j in 1:tsp.dimension]; pos(tsp::TSP,t::Vector{In…

Games101学习笔记 Lecture17 Materials and Appearances

Lecture17 Materials and Appearances 材质 BRDF一、Diffuse/Lambertian Material二、Glossy Material三、Ideal reflective/ refractive Material (BSDF)1.镜面反射2.镜面折射3.菲涅尔项 Fresnel 四、Microfacet BRDF 微表面五、Isotropic / Anisotropic Materials (BRDFs)An…

python - 文件 / 永久存储:pickle / 异常处理

一.文件 利用help(open)可以看到open()函数的定义&#xff1a; >>> help(open) Help on built-in function open in module _io:open(file, moder, buffering-1, encodingNone, errorsNone, newlineNone, closefdTrue, openerNone) 默认打开模式是’rt’&#xff0…

王者荣耀与和平精英的语音识别不准确怎么办?分享一次意想不到的解决经历!

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 完整经历 📒🔍 问题初现 🔍🔎 排查之路:从绝望到希望的转折 🔎🎉 顿悟时刻:原来是“她”的恶作剧 🎉⚓️ 相关链接 ⚓️📖 介绍 📖 作为一位打字速度惊人的玩家,我向来自豪于能在王者荣耀和和平精英等游戏…

Three.js机器人与星系动态场景(四):封装Threejs业务组件

实际在写业务的时候不会在每个组件里都写几十行的threejs的初始化工作。我们可以 将通用的threejs的场景、相机、render、轨道控制器等进行统一初始化。同时将非主体的函数提到组件外部&#xff0c;通过import导入进组件。将业务逻辑主体更清晰一些。下面的代码是基于reactthre…

DHCP与TCP的简单解析

目录 一、DHCP 1.1 DHCP概述 1.2 DHCP的优势 1.3 DHCP的模式与分配方式***** 1.3.1 DHCP的模式&#xff1a;C/S模式&#xff08;客户机与服务器模式&#xff09; 1.3.2 DHCP的分配方式 1.4 DHCP的租约过程及原理 1.4.1 DHCP的工作原理***** 1.4.2 更新租约原理***** …

智慧校园-基础平台功能总体概述

智慧校园基础平台是现代教育信息化的核心&#xff0c;它集成了系统管理、基础数据、系统监控、系统工具、流程管理等关键功能&#xff0c;构建了一个全面、智能、安全的校园生态系统。系统管理部分&#xff0c;通过权限管理和用户管理&#xff0c;实现了对用户访问权限的精细化…