底层分页SQL:

示例 start=10,end=20 查第11-20的数据
public String searchByPage(String sql, int start, int end, List<Object> valueList) {
		String ret = "";
		String dbType = ConfigUtil.getInstance().getDbType();

		switch (dbType) {
		case "oracle":
			ret = "SELECT * FROM (SELECT A.*, ROWNUM RN FROM (" + sql + ") A WHERE ROWNUM <= ? ) TEMP WHERE RN > ?";
			valueList.add(end);
			valueList.add(start);
			break;

		case "dm":
			ret = "SELECT * FROM (SELECT A.*, ROWNUM RN FROM (" + sql + ") A WHERE ROWNUM <= ? ) TEMP WHERE RN > ?";
			valueList.add(end);
			valueList.add(start);
			break;

		case "db2":
			ret = "SELECT * FROM (SELECT TEMP.*, ROW_NUMBER() OVER() AS NUM FROM (" + sql + ") TEMP ) WHERE NUM <= ? AND NUM > ?";
			valueList.add(end);
			valueList.add(start);
			break;

		case "sqlserver":
			sql = sql.replaceFirst("select", "");
			sql =  sql.substring(0,sql.lastIndexOf("order by"));
			String str = sql.substring(0,sql.indexOf(","));
			ret = "SELECT * FROM (SELECT row_number() OVER (ORDER BY " + str + " DESC ) AS row," + sql + "  ) tt WHERE tt.row BETWEEN ? AND ?";
			byte bytes[] = {(byte) 0xC2,(byte) 0xA0};
            String UTFSpace;
			try {
				UTFSpace = new String(bytes,"utf-8");
				ret = ret.replaceAll(UTFSpace, " ");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			
			valueList.add(start+1);
			valueList.add(end);
			break;

		case "mysql":
			ret = sql + " LIMIT ?, ?";
			valueList.add(start);
			valueList.add(end - start);
			break;
			
		// GBase 8s 中分页支持两种
		// 1.使用SKIP m FIRST/LIMIT n
		// 2.TOP m,n
		case "gbase8s":
			ret = sql + " skip " + start + " first " + (end - start);
			break;
			
		case "gbase8t":
			ret = sql + " LIMIT ?, ?";
			valueList.add(start);
			valueList.add(end - start);
			break;
			
		// 人大金仓,神通分页支持两种:A为查询条数,B为查询起点
		// 1. SELECT * FROM TABLE1 LIMIT A ;
		// 2. SELECT * FROM TABLE1 LIMIT A OFFSET B; 
		
		default:
			ret = "not support db type, is " + dbType;
			break;
		}

		return ret;
	}

分页

1.JDBCTemplate

基本与jdbc连接数据库的代码差不多,要支持多数据库,最关键的是分页的语句。
解决思路:编写一个统一处理sql的方法,接收业务SQL,根据不同的数据库类型,返回最终处理好的分页SQL。

@Repository
public class BaseDao {
	public String searchByPage(String sql, int start, int end, List<Object> valueList) {
		// 如上
	}
}

2. mybatis之mapper动态代理

参考文档:
MyBatis-16MyBatis动态SQL之【支持多种数据库】
mybatis多数据库兼容改造

oracle,mysql都可以用concat()函数进行连接,oracle的concat()只能传入两个参数,而mysql的可以传入多个参数,sqlserver则不能用concat()函数 ,只能用+来进行字符串的连接。

  • 解决方式一:同一个mapper.xml,根据属性databaseId区分不同数据库
    缺点:同样的sql要写很多份,文件异常庞大臃肿。不建议
    升级:拆成多个xml。不建议

  • 解决方式二:根据不同的数据库,选用不同的mapper.xml
    mapper映射采用在mybatis的配置文件中指定路径进行批量映射。
    实现方式:通过配置文件读取到数据库类型,统一转换大小写,作为文件夹名称,每个数据库都有一个对应的存放mapper.xml的文件夹。
    缺点:这种办法导致mapper.xml好几份,差别不大,很浪费,不建议使用。
    升级:使用插件

  • 解决方式三:使用分页插件

    • PageHelper
      原理:通过拦截器,在SQL真正执行前拦截下来,重新改造(eg:改为分页SQL),再继续执行改造好的SQL。

参考文章

  1. https://www.cnblogs.com/vandusty/archive/2020/05/12/12872978.html
  2. https://www.csdn.net/gather_2f/MtjaIgwsNDA5MDMtYmxvZwO0O0OO0O0O.html
  3. https://blog.csdn.net/houysx/article/details/80229991

Mybatis

1. Mybatis

MyBatis分页提供的是逻辑分页,每次将所有数据查询出来,存储到内存中,然后根据请求的页码,进行逐页返回。

2. Mybatis-Plus

官方介绍:https://baomidou.com/guide
集成mybatisplus后,简单的CRUD就不用写了,如果没有特别的sql,就可以不用mapper的xml文件的。

<!--mybatis-plus自动的维护了mybatis以及mybatis-spring的依赖,
  在springboot中这三者不能同时的出现,避免版本的冲突,表示:跳进过这个坑-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.1.1</version>
</dependency>

2.1 dao

  1. dao层接口(UserMapper)继承BaseMapper接口
    继承BaseMapper接口之后,就可以使用mybatisPlus封装的一系列增删改查方法。

UserMapper.java

package com.example.mapper.auto;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.model.auto.User;

import java.util.List;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author astupidcoder
 * @since 2020-05-13
 */
public interface UserMapper extends BaseMapper<User> {
    public List<User> findAllUser();
}

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelect() {
        System.out.println(("----- selectAll method test ------"));
        // UserMapper 中的 selectList() 方法的参数为 MP 内置的条件封装器 Wrapper,所以不填写就是无任何条件
        List<User> userList = userMapper.selectList(null);
        Assert.assertEquals(5, userList.size());
        userList.forEach(System.out::println);
    }

}

2.2 Service接口

  1. service接口(IUserService)继承IService接口
package com.example.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.model.auto.User;

import java.util.List;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author astupidcoder
 * @since 2020-05-13
 */
public interface IUserService extends IService<User> {

    public List<User> findAllUser();
}

2.3 Service实现类

  1. service实现类(UserServiceImpl)继承ServiceImpl,并实现相应的service接口(IUserService)
package com.example.service.impl;

import com.example.model.auto.User;
import com.example.mapper.auto.UserMapper;
import com.example.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author astupidcoder
 * @since 2020-05-13
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public List<User> findAllUser() {
        return userMapper.findAllUser();
    }
}

2.4 实体类

实体类用到的注解:

  1. 表名注解@TableName(value = …)
  2. 字段注解@TableField(value = …)
// 当数据库名与实体类名不一致或不符合驼峰命名时,需要在此注解指定表名
@TableName(value = "sys_user")
public class sysUser {
	// 用来解决数据库中的字段和实体类的字段不匹配问题
	@TableField(value = "create_time")
	private Integet createTime;
	// 用来解决实体类中有的属性但是数据表中没有的字段
	@TableField(exist = false)  // 默认为true
	private Integet createTimeCn;
}

2.5 自定义sql

MP自带的条件构造器虽然很强大,有时候也避免不了写稍微复杂一点业务的sql,自定义的sql当然是写在XML文件中的啦,那么首先来定义xml文件的位置,在yml配置文件如下:

mybatis-plus:
  # 如果是放在src/main/java目录下 classpath:/com/*/*/mapper/*Mapper.xml
  # 如果是放在resource目录 classpath:/mapper/**.xml
  mapper-locations: classpath:/mapper/**.xml
使用注解实现

/**
 * @Auther: IT贱男
 * @Date: 2019/6/10 14:40
 * @Description: User对象持久层
 */
public interface UserMapper extends BaseMapper<User> {
 
    /**
     *
     * 如果自定义的方法还希望能够使用MP提供的Wrapper条件构造器,则需要如下写法
     *
     * @param userWrapper
     * @return
     */
    @Select("SELECT * FROM user ${ew.customSqlSegment}")
    List<User> selectByMyWrapper(@Param(Constants.WRAPPER) Wrapper<User> userWrapper);
 
    /**
     * 和Mybatis使用方法一致
     * @param name
     * @return
     */
    @Select("SELECT * FROM user where name = #{name}")
    List<User> selectByName(@Param("name") String name);
 
}
使用xml实现
/**
 * @Auther: IT贱男
 * @Date: 2019/6/10 14:40
 * @Description: User对象持久层
 */
public interface UserMapper extends BaseMapper<User> {
 
     /**
     * 和Mybatis使用方法一致
     * @param name
     * @return
     */
    List<User> selectByName(@Param("name") String name);
 
}

在xml文件中,namespace对应的是user模块的dao,id对应dao下面UserMapper接口的函数名,resultType定义返回的类型。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
 
    <select id="selectByName" resultType="com.example.demo.model.User">
        SELECT * FROM user where name = #{name}
    </select>
 
</mapper>

2.6 MyBatisPlus物理分页

官方介绍:https://baomidou.com/guide/page.html

3. 公共字段自动写入

例如创建时间、更新时间等字段。

package cn.com.infosec.netseal.webserver.config.mybatisPlus;

import cn.com.infosec.netseal.common.entity.po.BasePo;
import cn.com.infosec.netseal.common.util.DateUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

/**
 * @Description mybatisPlus 公共字段自动写入
 * @Date 2020/11/17 11:21
 */
@Component
public class CommonFieldAutoSetHandler implements MetaObjectHandler {

	@Override
	public void insertFill(MetaObject metaObject) {
		if (metaObject.getOriginalObject() instanceof BasePo) {
			long currentTime = DateUtil.getCurrentTime();
			this.setFieldValByName("createTime", currentTime, metaObject);
			this.setFieldValByName("updateTime", currentTime, metaObject);
		}
	}

	@Override
	public void updateFill(MetaObject metaObject) {
		if (metaObject.getOriginalObject() instanceof BasePo) {
			long currentTime = DateUtil.getCurrentTime();
			this.setFieldValByName("updateTime", currentTime, metaObject);
		}
	}
}

4. 禁止全表更新或删除

package cn.com.infosec.netseal.webserver.config.mybatisPlus;

import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.SqlExplainInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {


	/**
	 * 乐观锁插件
	 */
	@Bean
	public OptimisticLockerInterceptor optimisticLockerInterceptor() {
		return new OptimisticLockerInterceptor();
	}

	/**
	 * 如果是对全表的删除或更新操作,就会终止该操作
	 */
	@Bean
	public SqlExplainInterceptor sqlExplainInterceptor() {
		return new SqlExplainInterceptor();
	}

}

SpringBoot 整合PageHelper

maven依赖

<!-- pagehelper 分页插件 -->
			<dependency>
				<groupId>com.github.pagehelper</groupId>
				<artifactId>pagehelper-spring-boot-starter</artifactId>
				<version>1.3.0</version>
				<exclusions>
					<exclusion>
						<groupId>org.mybatis</groupId>
						<artifactId>mybatis</artifactId>
					</exclusion>
				</exclusions>
			</dependency>

依赖版本分析

pagehelper-spring-boot-starter pagehelper 支持的数据库及其方言
版本 1.3.0 5.2.0 mysql(mysql)
oracle(oracle)
sqlserver(sqlserver或sqlserver2012)
db2(db2)
达梦(dm)
南大通用Gbase(infomix)
人大金仓KingBase(无方言,暂用postgresql)

等常用数据库及其他
版本 1.3.1 5.2.1 优化项:

postgresql(postgresql)
神通(oscar)
优化项:

瀚高(higngo)
虚谷(xugu)

通过源码对比发现,和实践证明:人大金仓和神通两个库,都可以用mysql的分页方式

数据库 实现类 分页方式
5.2.0 人大金仓 HsqldbDialect SELECT * FROM TABLE1 LIMIT A OFFSET B;
5.2.1 人大金仓 PostgreSqlDialect SELECT * FROM TABLE1 LIMIT A ;
SELECT * FROM TABLE1 LIMIT A OFFSET B;
5.2.0 神通 MySqlDialect SELECT * FROM TABLE1 LIMIT B,A;
5.2.1 神通 PostgreSqlDialect SELECT * FROM TABLE1 LIMIT A ;
SELECT * FROM TABLE1 LIMIT A OFFSET B;

在这里插入图片描述

常见配置

PageHelper插件,不配置helperDialect时,默认从数据库连接里自动找方言,建议配置上helperDialect,避免自动找会出什么异常

#pageHelper 方言
# 不配置时,默认从数据库连接里自动找方言,配置值为上图里的方言,eg:mysql、oracle、db2、infomix、sqlserver、dm、oscar等。
helperDialect=oracle

#启用合理化,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页
reasonable=true

#为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,
#不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
params=count=countSql

#支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页
#在代码中直接调用:List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
supportMethodsArguments=true

#如果 pageSize=0 就会查询出全部的结果(相当于没有执行分页查询)
pageSizeZero=true

自定义实现分页

PageHelper的分页逻辑是写在了AbstractHelperDialect类中,而不同的数据库分页实现是通过继承实现抽象方法的方式来实现的。

PageHelper的分页方言,默认是从数据库连接url地址里取双冒号之间的值,例如jdbc:kingbase://10.20.61.86:54321/NETSEAL,方言即kingbase。会遍历已注册的方言,挨个来匹配url,若匹配不到就会报错。

若遇到PageHelper不支持的数据库,可以自己实现(基本都支持了)。

  1. 在启动类排除PageHelperAutoConfiguration的加载,自定义同名Bean,实现addPageInterceptor方法;
    创建PageInterceptor拦截器,指定数据库方言,添加进SqlSessionFactory拦截器链;
  2. 自定义数据库方言类,继承AbstractHelperDialect,在PageAutoDialect中注册该类,并重写processPageParameter、getPageSql;

@PostConstruct:
Java中该注解的说明:@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
https://blog.csdn.net/qq360694660/article/details/82877222

启动类排除默认PageHelper启动类

@SpringBootApplication(exclude = { PageHelperAutoConfiguration.class})
public class WebServerApplication extends SpringBootServletInitializer {
	public static void main(String[] args) {
		SpringApplication.run(WebServerApplication.class, args);
		System.out.println("服务已启动");
	}
}

自定义同名Bean,添加拦截器

package cn.com.infosec.netseal.webserver.config.datasource;

import cn.com.infosec.netseal.common.config.ConfigUtil;
import cn.com.infosec.netseal.common.exceptions.ToLogException;

import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;

/**
 * @Description 自定义分页插件自动装配策略,使用自定义配置文件
 * @Author lhx
 */
@Configuration
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

	@Autowired
	private List<SqlSessionFactory> sqlSessionFactoryList;

	@PostConstruct
	public void addPageInterceptor() {
		//pageHelper的sql分页拦截器
		PageInterceptor interceptor = new PageInterceptor();
		//读取自定义的配置文件
		try {
			Properties sqlDialect = ConfigUtil.getInstance().getSqlDialect();
	//		System.out.println(sqlDialect);
			interceptor.setProperties(sqlDialect);
		} catch (Exception e) {
			throw new ToLogException("读取配置发生错误", e);
		}
		//将分页拦截器添加到mybatis的sqlsession
		for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
			sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
		}
	}

}

自定义分页类示例

package cn.com.infosec.netseal.webserver.config.datasource;

import com.github.pagehelper.Page;
import com.github.pagehelper.dialect.AbstractHelperDialect;
import com.github.pagehelper.page.PageAutoDialect;
import com.github.pagehelper.util.MetaObjectUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @Description 示例人大金仓的分页,此处按mysql的写法
 *
 */
@Component
public class KingBaseDialect extends AbstractHelperDialect {
    public KingBaseDialect() {
        // alias 不能瞎起名字,要写连接url里双冒号之间的值,例如jdbc:kingbase://10.20.61.86:54321/NETSEAL,alias即kingbase
        PageAutoDialect.registerDialectAlias("kingbase", KingBaseDialect.class);
    }

    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put("First_PageHelper", page.getStartRow());
        paramMap.put("Second_PageHelper", page.getPageSize());
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList(boundSql.getParameterMappings());
            if (page.getStartRow() == 0L) {
                newParameterMappings.add((new ParameterMapping.Builder(ms.getConfiguration(), "Second_PageHelper", Integer.TYPE)).build());
            } else {
                newParameterMappings.add((new ParameterMapping.Builder(ms.getConfiguration(), "First_PageHelper", Long.TYPE)).build());
                newParameterMappings.add((new ParameterMapping.Builder(ms.getConfiguration(), "Second_PageHelper", Integer.TYPE)).build());
            }

            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }

        return paramMap;
    }

    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0L) {
            sqlBuilder.append("\n LIMIT ? ");
        } else {
            sqlBuilder.append("\n LIMIT ?, ? ");
        }

        return sqlBuilder.toString();
    }
}

人大金仓精简1:
直接使用mysql,不再定义分页语句

@Component
public class KingBaseDialect extends AbstractHelperDialect {
    public KingBaseDialect() {
        // alias 不能瞎起名字,要写连接url里双冒号之间的值,例如jdbc:kingbase://10.20.61.86:54321/NETSEAL,alias即kingbase
        PageAutoDialect.registerDialectAlias("kingbase", MySqlDialect.class);
    }
}

人大金仓精简2:
在PageHelperAutoConfiguration 类的addPageInterceptor方法里直接注册:

@PostConstruct
	public void addPageInterceptor() {
		PageAutoDialect.registerDialectAlias("kingbase", MySqlDialect.class);
	}
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐