shading-jdbc 4.1.1 + tk.mybatis + pagehelper 1.3.x +spring boot 2.x 使用注意事项
shading-jdbc 4.1.1 + tk.mybatis + pagehelper 1.3.x + spring boot 2.x 是一个很常用的组合,但在使用过程中可能会遇到一些小问题,记录于此:
一、pom依赖
主要有以下几个:

1 <properties>
2 <java.version>1.8</java.version>
3 <sharding-sphere.version>4.1.1</sharding-sphere.version>
4 <tk.mybatis.version>2.1.5</tk.mybatis.version>
5 <mybatis.starter.version>2.1.3</mybatis.starter.version>
6 <pagehelper.version>1.3.0</pagehelper.version>
7 </properties>
8
9 <dependencyManagement>
10 <dependencies>
11 <!-- spring-boot-->
12 <dependency>
13 <groupId>org.apache.shardingsphere</groupId>
14 <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
15 <version>${sharding-sphere.version}</version>
16 </dependency>
17
18 <!--tk.mybatis-->
19 <dependency>
20 <groupId>tk.mybatis</groupId>
21 <artifactId>mapper-spring-boot-starter</artifactId>
22 <version>${tk.mybatis.version}</version>
23 </dependency>
24
25 <dependency>
26 <groupId>org.mybatis.spring.boot</groupId>
27 <artifactId>mybatis-spring-boot-starter</artifactId>
28 <version>${mybatis.starter.version}</version>
29 </dependency>
30
31 <!--pagehelper-->
32 <dependency>
33 <groupId>com.github.pagehelper</groupId>
34 <artifactId>pagehelper-spring-boot-starter</artifactId>
35 <version>${pagehelper.version}</version>
36 </dependency>
37
38 </dependencies>
39 </dependencyManagement>
二、application.properties

1 #数据源
2 spring.shardingsphere.datasource.names=ds0
3 spring.shardingsphere.sharding.default-data-source-name=ds0
4 spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
5 spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
6 spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
7 spring.shardingsphere.datasource.ds0.username=root
8 spring.shardingsphere.datasource.ds0.password=***
9 #分表
10 spring.shardingsphere.sharding.tables.t_order_logic.actual-data-nodes=ds0.t_order_$->{0..1}
11 spring.shardingsphere.sharding.tables.t_order_logic.table-strategy.inline.sharding-column=order_id
12 spring.shardingsphere.sharding.tables.t_order_logic.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 2}
13 spring.shardingsphere.props.sql.show=true
14 #精确路由
15 spring.shardingsphere.sharding.tables.t_order_logic_0.actual-data-nodes=ds0.t_order_0
16 spring.shardingsphere.sharding.tables.t_order_logic_1.actual-data-nodes=ds0.t_order_1
17 #mybatis
18 mybatis.mapper-locations=classpath*:mapper/*.xml
注:
* 这里只演示了单库分表的最小配置
* 注意里面有一个“精确路由”的配置,有时候我们明确知道数据就在某个具体分表上,但是sql的where条件中又不包含sharding-key,就可以参考上述配置,当查询t_order_logic_0时,直接路由到ds0.t_order_0这张表
t_order_0/t_order_1表结构如下:

1 CREATE TABLE `t_order_0` (
2 `order_id` bigint(20) unsigned NOT NULL,
3 `order_name` varchar(255) NOT NULL,
4 `order_date` varchar(255) NOT NULL,
5 PRIMARY KEY (`order_id`)
6 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
7
8 CREATE TABLE `t_order_1` (
9 `order_id` bigint(20) unsigned NOT NULL,
10 `order_name` varchar(255) NOT NULL,
11 `order_date` varchar(255) NOT NULL,
12 PRIMARY KEY (`order_id`)
13 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
三、mybatis自动生成tk.mybatis风格的代码
3.1 先配置plugin

<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<configuration>
<configurationFile>src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
</dependencies>
</plugin>
3.2 再配置generatorConfig.xml

1 <?xml version="1.0" encoding="UTF-8"?>
2
3 <!DOCTYPE generatorConfiguration
4 PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
5 "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
6
7 <generatorConfiguration>
8
9 <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
10 <property name="beginningDelimiter" value="`"/>
11 <property name="endingDelimiter" value="`"/>
12
13 <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
14 <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
15 <property name="caseSensitive" value="true"/>
16 <property name="useActualColumnNames" value="true"/>
17 <property name="beginningDelimiter" value="`"/>
18 <property name="endingDelimiter" value="`"/>
19 </plugin>
20
21
22 <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
23 connectionURL="jdbc:mysql://127.0.0.1:3306/testdb"
24 userId="root"
25 password="***"/>
26
27 <javaModelGenerator
28 targetPackage="test.entity"
29 targetProject="src/test/java"/>
30
31 <sqlMapGenerator targetPackage="mapper" targetProject="src/test/resources"/>
32
33 <javaClientGenerator targetPackage="test.mapper"
34 targetProject="src/test/java"
35 type="XMLMAPPER">
36 <property name="enableSubPackages" value="true"/>
37 </javaClientGenerator>
38
39 <!-- 注:生成的OrderEntity,实际对应的表名为t_order_0,并非逻辑表t_order_logic-->
40 <!-- 解决办法有2个:-->
41 <!-- 1. 生成后,手动修改@Table(name = "`t_order_0`")为@Table(name = "`t_order_logic`")-->
42 <!-- 2. 直接在db中创建一个t_order_logic的表,仅用于生成代码-->
43 <table tableName="t_order_0" domainObjectName="OrderEntity"
44 enableCountByExample="false" enableUpdateByExample="false"
45 enableDeleteByExample="false" enableSelectByExample="false"
46 selectByExampleQueryId="false">
47 <generatedKey column="order_id" sqlStatement="MYSQL" identity="true"/>
48 </table>
49
50 </context>
51 </generatorConfiguration>
四、分布式id的问题
sharding-jdbc内置了snowflake算法,但是集成tk.mybatis生成记录后,并不能马上返回自动生成的id值,如下图:

从输出的sql语句上看,sharding-jdbc改写了sql语句,附加了order_id字段,并用snowflake算法生成了新id,但是insert成功后,entity的orderId仍为null。
tips: 要开启sharding-jdbc的snowflake功能,需要修改下面2点
1. application.properties中必须指定snowflake

1 spring.shardingsphere.sharding.tables.t_order_logic.key-generator.column=order_id
2 spring.shardingsphere.sharding.tables.t_order_logic.key-generator.type=SNOWFLAKE
2. 表结构上的自增主键id,需要把entity类的自动生成主键注释掉

如果insert成功后,要拿到新的id值,建议id字段在insert前就手动赋值,参考下面的做法,直接调用内置的snowflake生成器:
创建一个IdService服务:

1 package com.cnblogs.yjmyzz.sharding.jdbc.demo.service;
2
3 import com.cnblogs.yjmyzz.sharding.jdbc.demo.utils.NetworkUtils;
4 import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
5 import org.springframework.beans.factory.InitializingBean;
6 import org.springframework.stereotype.Service;
7
8 import java.math.BigInteger;
9 import java.util.Properties;
10 import java.util.concurrent.atomic.AtomicInteger;
11
12 @Service("idService")
13 public class IdService implements InitializingBean {
14 private AtomicInteger serverIndex = new AtomicInteger(0);
15
16 private static final SnowflakeShardingKeyGenerator keyGenerator = new SnowflakeShardingKeyGenerator();
17
18 public long nextId() {
19 return (Long) keyGenerator.generateKey();
20 }
21
22 @Override
23 public void afterPropertiesSet() throws Exception {
24 BigInteger ipAddrBigInt = NetworkUtils.ipAddressToBigInt(NetworkUtils.getLocalHostAddress());
25 //假设服务器最多512台
26 BigInteger base = new BigInteger("512");
27 serverIndex.set(ipAddrBigInt.mod(base).intValue());
28 synchronized (this) {
29 Properties prop = keyGenerator.getProperties();
30 prop.setProperty("worker.id", serverIndex.toString());
31 keyGenerator.setProperties(prop);
32 }
33 }
34 }
注:snowflake算法要求设置1个唯一的worker.id,防止多个服务器生成相同的id,这里我们取服务器的ip地址,转换成数字,再对集群节点数取模,保证整个集群内唯一。
这里有一个小工具类,用于取服务器ip地址:

1 package com.cnblogs.yjmyzz.sharding.jdbc.demo.utils;
2
3
4 import java.math.BigInteger;
5 import java.net.InetAddress;
6 import java.net.NetworkInterface;
7 import java.net.UnknownHostException;
8 import java.util.Enumeration;
9
10 public final class NetworkUtils {
11 public NetworkUtils() {
12 }
13
14 public static String getLocalHostAddress() {
15 InetAddress addr = null;
16
17 try {
18 addr = getLocalHostLANAddress();
19 } catch (UnknownHostException var2) {
20 return "";
21 }
22
23 return addr != null ? addr.getHostAddress() : "";
24 }
25
26 public static InetAddress getLocalHostLANAddress() throws UnknownHostException {
27 try {
28 InetAddress candidateAddress = null;
29 Enumeration ifaces = NetworkInterface.getNetworkInterfaces();
30
31 while (ifaces.hasMoreElements()) {
32 NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
33 Enumeration inetAddrs = iface.getInetAddresses();
34
35 while (inetAddrs.hasMoreElements()) {
36 InetAddress inetAddr = (InetAddress) inetAddrs.nextElement();
37 if (!inetAddr.isLoopbackAddress()) {
38 if (inetAddr.isSiteLocalAddress()) {
39 return inetAddr;
40 }
41
42 if (candidateAddress == null) {
43 candidateAddress = inetAddr;
44 }
45 }
46 }
47 }
48
49 if (candidateAddress != null) {
50 return candidateAddress;
51 } else {
52 InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
53 if (jdkSuppliedAddress == null) {
54 throw new UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null.");
55 } else {
56 return jdkSuppliedAddress;
57 }
58 }
59 } catch (Exception var5) {
60 UnknownHostException unknownHostException = new UnknownHostException("Failed to determine LAN address: " + var5);
61 unknownHostException.initCause(var5);
62 throw unknownHostException;
63 }
64 }
65
66 public static BigInteger ipAddressToBigInt(String ipInString) {
67 ipInString = ipInString.replace(" ", "");
68 byte[] bytes;
69 if (ipInString.contains(":")) {
70 bytes = ipv6ToBytes(ipInString);
71 } else {
72 bytes = ipv4ToBytes(ipInString);
73 }
74
75 return new BigInteger(bytes);
76 }
77
78 public static String bigIntToIpAddress(BigInteger ipInBigInt) {
79 byte[] bytes = ipInBigInt.toByteArray();
80 byte[] unsignedBytes = bytes;
81
82 try {
83 String ip = InetAddress.getByAddress(unsignedBytes).toString();
84 return ip.substring(ip.indexOf(47) + 1).trim();
85 } catch (UnknownHostException var4) {
86 throw new RuntimeException(var4);
87 }
88 }
89
90 private static byte[] ipv6ToBytes(String ipv6) {
91 byte[] ret = new byte[17];
92 ret[0] = 0;
93 int ib = 16;
94 boolean comFlag = false;
95 if (ipv6.startsWith(":")) {
96 ipv6 = ipv6.substring(1);
97 }
98
99 String[] groups = ipv6.split(":");
100
101 for (int ig = groups.length - 1; ig > -1; --ig) {
102 if (groups[ig].contains(".")) {
103 byte[] temp = ipv4ToBytes(groups[ig]);
104 ret[ib--] = temp[4];
105 ret[ib--] = temp[3];
106 ret[ib--] = temp[2];
107 ret[ib--] = temp[1];
108 comFlag = true;
109 } else {
110 int temp;
111 if ("".equals(groups[ig])) {
112 for (temp = 9 - (groups.length + (comFlag ? 1 : 0)); temp-- > 0; ret[ib--] = 0) {
113 ret[ib--] = 0;
114 }
115 } else {
116 temp = Integer.parseInt(groups[ig], 16);
117 ret[ib--] = (byte) temp;
118 ret[ib--] = (byte) (temp >> 8);
119 }
120 }
121 }
122
123 return ret;
124 }
125
126 private static byte[] ipv4ToBytes(String ipv4) {
127 byte[] ret = new byte[5];
128 ret[0] = 0;
129 int position1 = ipv4.indexOf(".");
130 int position2 = ipv4.indexOf(".", position1 + 1);
131 int position3 = ipv4.indexOf(".", position2 + 1);
132 ret[1] = (byte) Integer.parseInt(ipv4.substring(0, position1));
133 ret[2] = (byte) Integer.parseInt(ipv4.substring(position1 + 1, position2));
134 ret[3] = (byte) Integer.parseInt(ipv4.substring(position2 + 1, position3));
135 ret[4] = (byte) Integer.parseInt(ipv4.substring(position3 + 1));
136 return ret;
137 }
138
139 public static boolean ipCheck(String tip, String[][] myRange) {
140 boolean flag = false;
141 BigInteger tbig = ipAddressToBigInt(tip);
142 int rangeLength = myRange.length;
143
144 for (int i = 0; i < rangeLength; ++i) {
145 for (int j = 0; j < myRange[i].length; ++j) {
146 BigInteger sbig = ipAddressToBigInt(myRange[i][j]);
147 ++j;
148 BigInteger ebig = ipAddressToBigInt(myRange[i][j]);
149 if (tbig.compareTo(sbig) == 0) {
150 flag = true;
151 break;
152 }
153
154 if (tbig.compareTo(sbig) == 1 && tbig.compareTo(ebig) == -1) {
155 flag = true;
156 break;
157 }
158 }
159 }
160
161 return flag;
162 }
163
164 class IpRange {
165 private String[][] ipRange;
166
167 public IpRange(String[][] ip) {
168 this.ipRange = ip;
169 }
170
171 public String getIpAt(int row, int column) {
172 return this.ipRange[row][column];
173 }
174 }
175 }
有了IdService后,在insert前,调用nextId()生成新id,直接赋值给entity即可。

五、MapperScan包名问题

MapperScan有2个,1个是mybatis自带的,1个是tk.mybatis的,集成tk.mybatis时,注意要使用tk.mybatis的MapperScan
六、PageHelper的count问题
PageHelper为了算总记录条数,会改写原始sql,做1次count,比如:
select order_id, order_name, order_date from t_order_logic_0 where order_date='2020-09-06';
会首先被改写为:
select count(0) from t_order_logic_0 where order_date='2020-09-06';
然后再由sharding-jdbc改写成:(精确路由)
select count(0) from t_order_0 where order_date='2020-09-06';
对于简单语句,这样没什么问题。但是如果原始语句上,有一些聚合函数或group by,比如下面这样:

如上图,加了group by 后,下面的语句
SELECT count(0) FROM (SELECT order_id FROM t_order_logic WHERE order_date = ? GROUP BY order_id) table_count
sharding-jdbc并不能正确解析为t_order_0,仍然还是t_order_logic
SELECT count(0) FROM (SELECT order_id FROM t_order_logic WHERE order_date = ? GROUP BY order_id) table_count ::: [2020-09-06]
解决办法:pagehelper对于count语句,允许用户自定义,只要在原来的语句id,加上“_COUNT”


1 <select id="selectList2" resultMap="BaseResultMap">
2 select order_id from t_order_logic${tableIndex} where order_date=#{orderDate} group by order_id
3 </select>
4
5 <select id="selectList2_COUNT" resultType="long">
6 select count(1) from t_order_logic${tableIndex} where order_date=#{orderDate} group by order_id
7 </select>
再运行一下:

现在就正常了。
示例代码已经上传到github,地址:https://github.com/yjmyzz/shardingjdbc-mybatis-pagehelper-springboot-demo
shading-jdbc 4.1.1 + tk.mybatis + pagehelper 1.3.x +spring boot 2.x 使用注意事项的更多相关文章
- MyBatis初级实战之一:Spring Boot集成
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 使用mybatis-generator插件结合tk.mybatis自动生成mapper二三事
本篇文章将介绍使用spring boot框架,引入mybatis-generator插件,结合tk.mybatis自动生成Mapper和Entity的一整套流程,其中包括最重要的踩坑与填坑. ...
- 62. mybatis 使用PageHelper不生效【从零开始学Spring Boot】
[从零开始学习Spirng Boot-常见异常汇总] 在Spirng Boot中集成了PageHelper,然后也在需要使用分页的地方加入了如下代码: PageHelper.startPage(1,1 ...
- Spring Boot集成MyBatis的2种方式
目录 写在前面 准备工作 配置数据库驱动 配置数据源 原生集成MyBatis 依赖配置 注册MyBatis核心组件 定义并使用映射器 通过MyBatis-Spring-Boot-Starter集成 默 ...
- Spring Boot 2.X(二):集成 MyBatis 数据层开发
MyBatis 简介 概述 MyBatis 是一款优秀的持久层框架,支持定制化 SQL.存储过程以及高级映射.它采用面向对象编程的方式对数据库进行 CRUD 的操作,使程序中对关系数据库的操作更方便简 ...
- spring boot集成mybatis(1)
Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...
- Spring Boot 整合 Mybatis 实现 Druid 多数据源详解
摘要: 原创出处:www.bysocket.com 泥瓦匠BYSocket 希望转载,保留摘要,谢谢! “清醒时做事,糊涂时跑步,大怒时睡觉,独处时思考” 本文提纲一.多数据源的应用场景二.运行 sp ...
- spring boot 整合 mybatis 以及原理
同上一篇文章一样,spring boot 整合 mybatis过程中没有看见SqlSessionFactory,sqlsession(sqlsessionTemplate),就连在spring框架整合 ...
- spring boot配置mybatis和事务管理
spring boot配置mybatis和事务管理 一.spring boot与mybatis的配置 1.首先,spring boot 配置mybatis需要的全部依赖如下: <!-- Spri ...
- Spring Boot 2.x基础教程:使用MyBatis访问MySQL
之前我们已经介绍了两种在Spring Boot中访问关系型数据库的方式: 使用spring-boot-starter-jdbc 使用spring-boot-starter-data-jpa 虽然Spr ...
随机推荐
- ServletContext相关
简介 如何得到对象 有什么作用 1.获取全局配置参数 2.获取web工程中的资源 3.存取数据,servlet间共享数据 域对象 ServlerContext的生命周期 ServletContext ...
- idea创建类时默认添加头部注释信息
- Lynx-字节跳动跨平台框架多端兼容Android, iOS, Web 原生渲染
介绍 字节跳动近期开源的跨平台框架Lynx被视为一项重要的技术创新.相较于市场上已有的解决方案如React Native (RN) 和Flutter,Lynx具有独特的特性. 首先,Lynx采用轻量级 ...
- Python单元测试标准库unittest简单学习
1.背景 当需要测试较为复杂的module,class或者系统的功能时,如果一个一个的去测试就会显得很麻烦,如果每项测试又有一定的配置或者设置的话,比如每个测试都要新建一个对象之类的,那就更麻烦了.单 ...
- Solon Ai Flow 编排开发框架发布预告(效果预览)
Solon Ai 在推出 Solon Ai Mcp 后,又将推出 Solon Ai Flow. 1.Solon Ai Flow 是个啥? Solon Ai Flow 是一个智能体编排开发框架(基于 s ...
- An internal error occurred during: "Polling news feeds". javax/xml/bind/JAXBContext
WindowPerferences取消Enable automatic news polling的勾选
- Github如何创建添加开源许可license
我们点击项目上方的 Add file,选择 creat new file 名称填写 LICENSE, 右侧便会出现按钮 Choose a license template 选择你要的证书,填写 年份 ...
- .NET外挂系列:5. harmony 中补丁参数的有趣玩法(下)
一:背景 1. 讲故事 开局一张表,故事全靠编,为了能够承上启下,先把参数列表放出来. 参数名 说明 __instance 访问非静态方法的实例(类似 this). __result 获取/修改返回值 ...
- WindowsPE文件格式入门11.资源表
https://www.bpsend.net/thread-411-1-1.html 资源表 资源的管理方式采用windows资源管理器目录的管理方式,一般有三层目录. 根目录 结构体IMAGE_RE ...
- CRM 价格更新
FUNCTION zcrm_reprice_bt. *"------------------------------------------------------------------- ...