在使用 DataGrip 通过自定义驱动链接人大金仓 kingbase R9 数据库时，出现了时间不对的问题，select now()查询出的时间比北京时间少 8 小时，并且通过show timezone查询出的时区也是UTC。但是在数据库的设置文件kingbase.conf中，已经设置了时区为Asia/Shanghai，以及使用官方工具 KSutdio 2.0 进行链接时，时间和时区也是对的。一番搜索后发现是 DataGrip(以及 IDEA 中 Database 插件) 工具自身的连接方式有问题。修改方式新版本DataGrip以默认时区取世界标准时间。要想时间显示正常，需要将时区变为上海时区，可手动在连接配置里设置参数。如下图：操作步骤：右键打开你想要修改的数据库连接的 Properties 菜单；点击 Advanced 按钮；在 VM options 后面写入-Duser.timezone=Asia/Shanghai，就可以啦；断开数据库连接，重新执行一条SQL（也可刷新数据库）就可以看到效果了。设置方式转自：DataGrip设置时区
Python 使用 sqlalchemy 链接 openGauss（高斯）出错解决
sqlalchemy 有版本验证，需要 hack 获取版本的函数：from sqlalchemy.dialects.postgresql.base import PGDialect # 直接返回 9.2（高斯基于 postgresql 9.2.4 版本） PGDialect._get_server_version_info = lambda *args: (9, 2)stackoverflow 地址：SQLAlchemy raises an exception for unknown version while connecting to GaussDB (for openGauss)
问：有哪些常见的SQL优化手段？这也是个高频面试题，并且并非面试造火箭的那种问题，实际项目中也会有非常多的地方需要进行SQL优化避免使用 Select *select *中，无用字段会增加网络带宽消耗，特别是varchar、blob、text等大字段select *无法使用Mysql优化器覆盖索引的优化。覆盖索引：即一个查询可以完全通过索引来满足，不需要回表回表：根据非聚集索引查询到主键，然后根据主键查询其他数据。分页优化Mysql分页时，并不是跳过 offset，而是取 offset + N 行，然后丢弃 offset行，只返回N行。并且，offset 越大、N 越大、select出的字段越多，耗时就越久。数据量少时，分页耗时还是比较少的。但是上数据量上百万甚至千万的时候，直接limit 1000000, 20是非常慢的。优化方案：先根据排序规则及limit条件查询出主键，然后根据主键关联到表本身。即把select t.* form the_table where 条件 limit 1000000, 20 修改为：select t1.* from the_table t1, (select id from the_table where 条件 limit 1000000, 20) as t2 where t1.id = t2.id并且条件种的字段尽量创建了索引。尽量避免多表Joinjoin 关联的效率不算很高，多个表关联时会使用嵌套循环，如果关联字段没有索引，会直接使用笛卡尔积实现 join，导致全表扫描，效率很低。两种解决方式：单表查询，根据查询结果进行二次查询，最后在 Service 层进行关联。数据冗余：把重要字段冗余在表中，尽可能避免关联查询。推荐使用第一种方式，因为数据库的计算资源更加宝贵，Service层可以很方便的水平扩展。并且如果数据量非常大的时候也会进行分库分表，这也会限制join。建议不要使用外键与级联外键概念应该在应用层解决。选择合适的字段类型存储字节越小，占用空间就越小，性能也就越好。尽量使用 union all 代替 unionunion会把结果集进行去重操作，更耗时。批量操作操作数据时，能批量操作尽量批量处理，减少请求数据库的次数。使用Explain分析SQL正确使用索引适合作为索引的字段不为null的字段：索引字段应尽量不为null，对于数据为null的字段，数据库比较难优化。如果字段频繁被查询，又无法避免为null，可以使用-1、0、true、false等语义清晰地短字符作为代替。被频繁查询的字段被作为条件查询的字段频繁需要排序的字段频繁用于外连接的字段被频繁更新的字段应慎重建立索引虽然索引能带来查询上的效率，但是维护索引的成本也不小。 如果字段不经常被查询，反而经常被修改，那不应该在这种字段上创建索引。尽可能创建联合索引，而不是单列索引索引需要占用空间，如果是联合索引，多个字段在一个索引上，可以节省空间，并且修改数据时效率也会提升。避免冗余索引考虑在字符串类型上使用前缀索引代替普通索引前缀索引只适用于字符串类型，比普通索引占用更少的空间避免索引失效使用 select *查询创建了联合索引，但是没有遵循最左匹配原则在索引列上进行计算、函数、类型转换等操作以%开头的like查询，比如like '%abc'发生隐式转换查询时使用条件1 or 条件2，如果条件1或者条件2中存在未创建索引的字段，就不会使用索引
数据库 Tips时间和日期互转数据库当前时间字符串转时间时间转字符串格式dual 表MySQLNOW()STR_TO_DATEDATE_FORMAT%Y-%m-%d %H:%i:%s可选OracleSYSTIMESTAMP、SYSDATETO_TIMESTAMP、TO_DATETO_CHARYYYY-MM-DD HH24:MI:SS必须用PostgreSQLCURRENT_DATE、CURRENT_TIME、CURRENT_TIMESTAMPTO_DATE、TO_TIMESTAMPTO_CHARYYYY-MM-DD HH24:MI:SS不能用数据库迁移迁移到 PostgreSQL可以使用 pgloader，有 Docker 镜像：dimitri/pgloader图形化工具a5m2：全称「A5:SQL MK-2」，一个日本人开发的图形化工具，直接在数据库上右键，然后选择Data Transfer...就可以了，可以选择不同的数据库类型。navicat：只能迁移相同类型的数据库，比如 mysql -> mysql，或者 pgsql -> pgsql。MySQL创建数据库并指定字符集并不难，只不过用的比较多，记下来CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE DATABASE `test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;时间和字符串互转STR_TO_DATE和DATE_FORMAT，转换格式：%Y-%m-%d %H:%i:%sOracle(+) 符号这种是旧写法，oracle 已经不再推荐详情可见https://stackoverflow.com/a/47008794/17746262在 Oracle 中，(+) 表示 JOIN 中的“可选”表。SELECT a.id, b.id, a.col_2, b.col_2, ... FROM a,b WHERE a.id=b.id(+)现代写法就是：SELECT a.id, b.id, a.col_2, b.col_2, ... FROM a LEFT JOIN b ON a.id=b.id创建序列-- 创建序列 create sequence seq_newsId -- 删除序列 DROP SEQUENCE seq_newsIdPostgreSQL判断时间重合使用tsrange函数：select * from example where tsrange(start_time, end_time, '()') && tsrange(start_time2, end_time2, '()')解释：tsrange 可以把时间转换成时间段然后使用&&判断是否重合，第三个参数代表边界范围，即开区间还是闭区间，可选(和[，前者代表开区间，即不包含边界，后者代表闭区间，包含边界，第一个括号代表开始时间，第二个括号代表结束时间。持续更新中……
最近学习了 sharding sphere 中的jdbc组件，此文作为学习记录方便以后复习。中间件官网：Apache Sharding Sphere学习准备快速开始分表创建库表：CREATE DATABASE `db_device_0` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `tb_device_0` ( `device_id` BIGINT NOT NULL AUTO_INCREMENT, `device_type` INT DEFAULT NULL, PRIMARY KEY ( `device_id` ) ) AUTO_INCREMENT = 9 DEFAULT CHARSET = utf8mb4; CREATE TABLE `tb_device_1` ( `device_id` BIGINT NOT NULL AUTO_INCREMENT, `device_type` INT DEFAULT NULL, PRIMARY KEY ( `device_id` ) ) AUTO_INCREMENT = 10 DEFAULT CHARSET = utf8mb4;引入依赖：因为是demo项目，所以直接使用了JDK17pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.1</version> </parent> <groupId>fun.psgame</groupId> <artifactId>sharding-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sharding-demo</name> <description>sharding-demo</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 添加配置文件：spring: shardingsphere: props: sql: # 开启SQL显示 show: true # 配置真实数据源 datasource: names: ds1 ds1: # 这里会报红，但是不影响 jdbc-url: jdbc:mysql://localhost:3306/db_device_0?serverTimezone=Asia/Shanghai type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root sharding: tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds1.tb_device_$->{0..1} table-strategy: inline: # 配置分表策略：根据device_id作为分片的依据（分片键） sharding-column: device_id # 分片策略 algorithm-expression: tb_device_$->{device_id%2}创建实体类：@Getter @Setter public class TbDevice { private Long deviceId; private int deviceType; }创建mapper：public interface DeviceMapper extends BaseMapper<TbDevice> { }配置springboot启动类@SpringBootApplication @MapperScan("fun.psgame.demo.mapper") public class ShardingDemoApplication { public static void main(String[] args) { SpringApplication.run(ShardingDemoApplication.class, args); } }添加测试类进行初步测试： @SpringBootTest class ShardingDemoApplicationTests { @Autowired private DeviceMapper deviceMapper; @Test void initData() { for (int i = 0; i < 10; i++) { TbDevice device = new TbDevice(); device.setDeviceId((long) i); device.setDeviceType(i); deviceMapper.insert(device); } } }执行测试类发现出错：错误信息org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field private static final long java.lang.Number.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @1324409e ### The error may exist in fun/psgame/demo/mapper/DeviceMapper.java (best guess) ### The error may involve fun.psgame.demo.mapper.DeviceMapper.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO tb_device ( device_id, device_type ) VALUES ( ?, ? ) ### Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field private static final long java.lang.Number.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @1324409e at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441) at jdk.proxy2/jdk.proxy2.$Proxy73.insert(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272) at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:59) at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148) at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89) at jdk.proxy2/jdk.proxy2.$Proxy76.insert(Unknown Source) at fun.psgame.demo.ShardingDemoApplicationTests.initData(ShardingDemoApplicationTests.java:20) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84) at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)这是由于jdk版本太高，一种解决方法是直接换jdk 1.8，不过既然是学习，就是要折腾，只需要在启动参数上添加--add-opens=java.base/java.lang=ALL-UNNAMED就可以了初步执行发现10条数据偶数device_id的数据都在tb_device_0，奇数的都在tb_device_1里面。分库然后创建第二个数据库：CREATE DATABASE `db_device_1` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE `tb_device_0` ( `device_id` BIGINT NOT NULL AUTO_INCREMENT, `device_type` INT DEFAULT NULL, PRIMARY KEY ( `device_id` ) ) AUTO_INCREMENT = 9 DEFAULT CHARSET = utf8mb4; CREATE TABLE `tb_device_1` ( `device_id` BIGINT NOT NULL AUTO_INCREMENT, `device_type` INT DEFAULT NULL, PRIMARY KEY ( `device_id` ) ) AUTO_INCREMENT = 10 DEFAULT CHARSET = utf8mb4;然后需要修改配置文件spring: shardingsphere: props: sql: # 开启SQL显示 show: true # 配置真实数据源 datasource: names: ds0,ds1 ds0: jdbcUrl: jdbc:mysql://localhost:3306/db_device_0?serverTimezone=Asia/Shanghai type: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver username: root password: root ds1: jdbcUrl: jdbc:mysql://localhost:3306/db_device_1?serverTimezone=Asia/Shanghai type: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver username: root password: root sharding: # 分库的分片策略 default-database-strategy: inline: sharding-column: device_id algorithm-expression: ds$->{device_id%2} tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds$->{0..1}.tb_device_$->{0..1} table-strategy: inline: # 配置分表策略：根据device_id作为分片的依据（分片键） sharding-column: device_id # 分片策略 algorithm-expression: tb_device_$->{device_id%2}和刚才相比主要是添加了一个新的数据库，并且配置了分库策略。测试写两个test：@Test void queryDeviceByID() { QueryWrapper<TbDevice> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("device_id", 1L); List<TbDevice> deviceList = deviceMapper.selectList(queryWrapper); System.out.println(deviceList); } @Test void queryDeviceByRange(){ QueryWrapper<TbDevice> queryWrapper = new QueryWrapper<>(); queryWrapper.between("device_id",1,10); List<TbDevice> deviceList = deviceMapper.selectList(queryWrapper); System.out.println(deviceList); }会发现第一个查询可以查到，第二个查询报错了，错误说明是inline方式的分片策略不支持范围查询。分库分表核心知识核心概念了解分片策略之前首先需要知道几个重点概念：逻辑表、真实表、数据节点、绑定表、⼴播表逻辑表⽔平拆分的数据库（表）的相同逻辑和数据结构表的总称。 例：订单数据根据主键尾数拆分为10张表，分别是 t_order_0 到 t_order_9 ，他们的逻辑表名为 t_order 。真实表在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9数据节点数据分片的最⼩单元。由数据源名称和数据表组成，例： ds_0.t_order_0 。绑定表指分片规则⼀致的主表和⼦表。例如： t_order 表和 t_order_item 表，均按照 order_id 分片，则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联，关联查询效率将⼤⼤提升。举例说明，如果SQL为：SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);在不配置绑定表关系时，假设分片键 order_id 将数值10路由至第0片，将数值11路由至第1片，那么路由后的SQL应该为4条，它们呈现为笛卡尔积：SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);在配置绑定表关系后，路由的SQL应该为2条：SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);即 订单表t_order_0关联的数据都应该在t_order_item_0。其中 t_order 在FROM的最左侧， ShardingSphere将会以它作为整个绑定表的主表。 所有路 由计算将会只使⽤主表的策略，那么 t_order_item 表的分片计算将会使⽤ t_order 的条件。 故绑定表之间的分区键要完全相同。⼴播表指所有的分片数据源中都存在的表，表结构和表中的数据在每个数据库中均完全⼀致。适⽤于数据量不⼤且需要与海量数据的表进⾏关联查询的场景，例如：字典表。分片及分片策略分片键⽤于分片的数据库字段，是将数据库(表)⽔平拆分的关键字段，shardingSphere既支持单分片字段，也支持多分片字段。分片算法通过分片算法将数据分片，将支持sql中的=、>=、<=、>、<、BETWEEN和IN。具体分片算法需要自行实现。目前有四类分片算法，没有具体实现，可根据需要自行继承实现：精确分片算法对应PreciseShardingAlgorithm，⽤于处理使⽤单⼀键作为分片键的=与·进⾏分片的场景。需要配合StandardShardingStrategy使⽤。范围分片算法对应RangeShardingAlgorithm，⽤于处理使⽤单⼀键作为分片键的BETWEEN AND、>、<、>=、<=进⾏分片的场景。需要配合StandardShardingStrategy使⽤。复合分片算法对应ComplexKeysShardingAlgorithm，⽤于处理使⽤多键作为分片键进⾏分片的场景，包含多个分片键的逻辑较复杂，需要应⽤开发者⾃⾏处理其中的复杂度。需要配合ComplexShardingStrategy使⽤。Hint分片算法对应HintShardingAlgorithm，⽤于处理使⽤Hint⾏分片的场景。需要配合HintShardingStrategy使⽤。分片策略分片策略包含分片键和分片算法，⽬前提供5种分片策略：标准分片策略对应StandardShardingStrategy。提供对SQL语句中的= > < >= <= IN BETWEEN AND的分片操作⽀持。 StandardShardingStrategy只⽀持单分片键，提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的，⽤于处理=和IN的分片。 RangeShardingAlgorithm是可选的，⽤于处理BETWEEN AND, >, <, >=, <=分片，如果不配置RangeShardingAlgorithm， SQL中的BETWEEN AND将按照全库路由处理。复合分片策略对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作⽀持。 ComplexShardingStrategy⽀持多分片键，由于多分片键之间的关系复杂，因此并未进⾏过多的封装，⽽是直接将分片键值组合以及分片操作符透传至分片算法，完全由应⽤开发者实现，提供最⼤的灵活度。⾏表达式分片策略对应InlineShardingStrategy。使⽤Groovy的表达式，提供对SQL语句中的=和IN的分片操作⽀持，只⽀持单分片键。对于简单的分片算法，可以通过简单的配置使⽤，从⽽避免繁琐的Java代码开发，如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8，⽽分成8张表，表名称为 t_user_0 到 t_user_7 。Hint分片策略对应HintShardingStrategy。通过Hint指定分片值⽽⾮从SQL中提取分片值的⽅式进⾏分片的策略。不分片策略对应NoneShardingStrategy。不分片的策略。分片策略的实现Standard标准分片策略的精准分片配置分库的精确分片将spring.shardingsphere.sharding做如下修改： sharding: # 分库的分片策略 default-database-strategy: # inline: # sharding-column: device_id # algorithm-expression: ds$->{device_id%2} standard: sharding-column: device_id # 精准查询算法 precise-algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseStandardPreciseAlgorithm也就是把inline分库策略修改为自定义的精准分库策略，上例中模2的等价替代如下package fun.psgame.demo.algorithm.database; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import java.util.Collection; /** * 数据库精准分片策略 */ public class MyDatabaseStandardPreciseAlgorithm implements PreciseShardingAlgorithm<Long> { /** * 精准分片 * * @param availableTargetNames 可用物理库表名称 * @param shardingValue 分片参数 * @return 定位到的数据库 */ @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) { // 逻辑表名称 String logicTableName = shardingValue.getLogicTableName(); // 分片列名 String columnName = shardingValue.getColumnName(); // 分片值 Long value = shardingValue.getValue(); String databaseName = "ds" + (value % 2); if (!availableTargetNames.contains(databaseName)) { throw new UnsupportedOperationException("数据源" + databaseName + "不存在"); } return databaseName; } } 即可以根据自己的需要进行数据库定位。表的分片策略也是同样： tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds$->{0..1}.tb_device_$->{0..1} table-strategy: # inline: # # 配置分表策略：根据device_id作为分片的依据（分片键） # sharding-column: device_id # # 分片策略 # algorithm-expression: tb_device_$->{device_id%2} standard: # 分片键 sharding-column: device_id # 自定义标准分片策略 precise-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardPreciseAlgorithm对应的类也类似库的策略类：package fun.psgame.demo.algorithm.table; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import java.util.Collection; public class MyTableStandardPreciseAlgorithm implements PreciseShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) { Long value = shardingValue.getValue(); String tableName = shardingValue.getLogicTableName() + "_" + (value % 2); if (!availableTargetNames.contains(tableName)) { throw new UnsupportedOperationException("表" + tableName + "不存在"); } return tableName; } } Standard标准分片策略的范围分片修改配置 sharding: # 分库的分片策略 default-database-strategy: # inline: # sharding-column: device_id # algorithm-expression: ds$->{device_id%2} standard: sharding-column: device_id # 精准查询算法 precise-algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseStandardPreciseAlgorithm # 范围查询算法 range-algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseStandardRangeAlgorithm tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds$->{0..1}.tb_device_$->{0..1} table-strategy: # inline: # # 配置分表策略：根据device_id作为分片的依据（分片键） # sharding-column: device_id # # 分片策略 # algorithm-expression: tb_device_$->{device_id%2} standard: # 分片键 sharding-column: device_id # 自定义标准分片策略 precise-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardPreciseAlgorithm range-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardRangeAlgorithm对应的数据库分片类、表分片类：package fun.psgame.demo.algorithm.database; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import java.util.Collection; public class MyDatabaseStandardRangeAlgorithm implements RangeShardingAlgorithm<Long> { /** * * @param availableTargetNames available data sources or tables's names * @param shardingValue sharding value * @return */ @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) { // 要查询的范围信息 Range<Long> range = shardingValue.getValueRange(); // 直接返回数据源 return availableTargetNames; } } package fun.psgame.demo.algorithm.table; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import java.util.Collection; public class MyTableStandardRangeAlgorithm implements RangeShardingAlgorithm<Long> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) { return availableTargetNames; } } 此时发现已然支持BETWEEN AND查询了。Complex复合分片策略问题：如果参与分片的键不止一个，那么就可以使用复合分片@Test void queryDeviceByRangeAndDeviceType() { QueryWrapper<TbDevice> queryWrapper = new QueryWrapper<>(); queryWrapper.between("device_id", 1, 10); queryWrapper.eq("device_type", 5); List<TbDevice> deviceList = deviceMapper.selectList(queryWrapper); System.out.println(deviceList); }上例中，device_type的值也与库表有关，且逻辑与device_id都是对2取模，那么在不使用复合分片策略执行查询时，查询结果虽然是正确的，但是会把每个库表都查一边（因为device_id条件是BETWEEN AND），但是device_type为5的数据只存在于一张表，造成了部分无效查询：INFO 5879 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT device_id,device_type FROM tb_device_0 WHERE device_id BETWEEN ? AND ? AND device_type = ? ::: [1, 10, 5] INFO 5879 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT device_id,device_type FROM tb_device_1 WHERE device_id BETWEEN ? AND ? AND device_type = ? ::: [1, 10, 5] INFO 5879 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT device_id,device_type FROM tb_device_0 WHERE device_id BETWEEN ? AND ? AND device_type = ? ::: [1, 10, 5] INFO 5879 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT device_id,device_type FROM tb_device_1 WHERE device_id BETWEEN ? AND ? AND device_type = ? ::: [1, 10, 5]因此可以使用复合分片策略来进行分片yml配置： sharding: # 分库的分片策略 default-database-strategy: # inline: # sharding-column: device_id # algorithm-expression: ds$->{device_id%2} # standard: # sharding-column: device_id # # 精准查询算法 # precise-algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseStandardPreciseAlgorithm # # 范围查询算法 # range-algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseStandardRangeAlgorithm complex: sharding-columns: device_id,device_type algorithm-class-name: fun.psgame.demo.algorithm.database.MyDatabaseComplexAlgorithm tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds$->{0..1}.tb_device_$->{0..1} table-strategy: # inline: # # 配置分表策略：根据device_id作为分片的依据（分片键） # sharding-column: device_id # # 分片策略 # algorithm-expression: tb_device_$->{device_id%2} # standard: # # 分片键 # sharding-column: device_id # # 自定义标准分片策略 # precise-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardPreciseAlgorithm # range-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardRangeAlgorithm complex: sharding-columns: device_id,device_type algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableComplexAlgorithm 对应的库分片类为：package fun.psgame.demo.algorithm.database; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue; import java.util.*; public class MyDatabaseComplexAlgorithm implements // 因为参与分片的既有Long又有Integer，因此泛型只能使用其父类Comparable<?> ComplexKeysShardingAlgorithm<Comparable<?>> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) { Map<String, Range<Comparable<?>>> columnNameAndRangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap(); Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap(); Collection<Comparable<?>> deviceTypes = columnNameAndShardingValuesMap.get("device_type"); List<Integer> deviceTypeList = deviceTypes.stream() .filter(s -> s instanceof Integer) .map(s -> (Integer) s) .toList(); Set<String> resultSet = new HashSet<>(); for (Integer deviceType : deviceTypeList) { String lastStr = String.valueOf(deviceType % 2); for (String availableTargetName : availableTargetNames) { if (availableTargetName.endsWith(lastStr)) { resultSet.add(availableTargetName); } } } return resultSet; } } 对应的表分片类为：package fun.psgame.demo.algorithm.table; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue; import java.util.*; public class MyTableComplexAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> { /** * * @param availableTargetNames available data sources or tables's names * @param shardingValue sharding value * @return */ @Override public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) { Map<String, Range<Comparable<?>>> columnNameAndRangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap(); Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap(); Collection<Comparable<?>> deviceTypes = columnNameAndShardingValuesMap.get("device_type"); List<Integer> deviceTypeList = deviceTypes.stream() .filter(s -> s instanceof Integer) .map(s -> (Integer) s) .toList(); Set<String> resultSet = new HashSet<>(); for (Integer deviceType : deviceTypeList) { String lastStr = String.valueOf(deviceType % 2); for (String availableTargetName : availableTargetNames) { if (availableTargetName.endsWith(lastStr)) { resultSet.add(availableTargetName); } } } return resultSet; } } Hint强制路由策略hint可以不根据sql语句特性，强制路由到某个库的某个表中。yml配置 tables: tb_device: # 配置物理表 逻辑表 -> 物理表，groovy语法，tb_device_0、tb_device_1 actual-data-nodes: ds$->{0..1}.tb_device_$->{0..1} table-strategy: # inline: # # 配置分表策略：根据device_id作为分片的依据（分片键） # sharding-column: device_id # # 分片策略 # algorithm-expression: tb_device_$->{device_id%2} # standard: # # 分片键 # sharding-column: device_id # # 自定义标准分片策略 # precise-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardPreciseAlgorithm # range-algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableStandardRangeAlgorithm # complex: # sharding-columns: device_id,device_type # algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableComplexAlgorithm hint: algorithm-class-name: fun.psgame.demo.algorithm.table.MyTableHintAlgorithmhint只需要配置策略类就行，不关心具体的分片键对应策略类：package fun.psgame.demo.algorithm.table; import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm; import org.apache.shardingsphere.api.sharding.hint.HintShardingValue; import java.util.Collection; import java.util.List; public class MyTableHintAlgorithm implements HintShardingAlgorithm<Long> { /** * * @param availableTargetNames available data sources or tables's names * @param shardingValue sharding value * @return */ @Override public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Long> shardingValue) { Collection<Long> values = shardingValue.getValues(); String logicTableName = shardingValue.getLogicTableName(); String tableName = logicTableName + "_" + values.toArray()[0]; if (!availableTargetNames.contains(tableName)) { throw new UnsupportedOperationException("表" + tableName + "不存在"); } return List.of(tableName); } } 这样就可以强制指定某张表。绑定表绑定表解决的问题是，进行分表的表，在进行join关联查询时，会造成不必要的查询，查询结果也会因为笛卡尔积而重复。例子：创建 tb_device_info 表在两个库里面分别执行：表的主键不能自增，因为每条数据都需要有一个唯一ID 这里没有主键策略，mybatis-plus会使用自身的随机主键，测试用足够了，实际生产可以是有雪花算法生成唯一ID-- 清除tb_device表的数据 truncate table tb_device_0; truncate table tb_device_1; -- 创建tb_device_info表 DROP TABLE if EXISTS tb_device_info_0; CREATE TABLE `tb_device_info_0` ( `id` BIGINT PRIMARY KEY, `device_id` BIGINT DEFAULT NULL, `device_intro` VARCHAR(255) COLLATE utf8mb4_general_ci DEFAULT NULL ); DROP TABLE if EXISTS tb_device_info_1; CREATE TABLE `tb_device_info_1` ( `id` BIGINT primary key, `device_id` BIGINT DEFAULT NULL, `device_intro` VARCHAR(255) COLLATE utf8mb4_general_ci DEFAULT NULL )然后创建对应的实体类和Mapper（此步骤不再赘述）接着插入数据：@Test void testInsertType() { for (int i = 0; i < 10; i++) { TbDevice device = new TbDevice(); device.setDeviceId((long) i); device.setDeviceType(i); deviceMapper.insert(device); TbDeviceInfo deviceInfo = new TbDeviceInfo(); deviceInfo.setDeviceId((long) i); deviceInfo.setDeviceIntro("" + i); deviceInfoMapper.insert(deviceInfo); } }然后给添加一个关联查询的方法：ps：多行文本是java 15才有的特性，java8请自行拼接字符串public interface DeviceInfoMapper extends BaseMapper<TbDeviceInfo> { @Select(""" select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info a left join tb_device b on a.device_id = b.device_id """) List<TbDeviceInfo> queryDeviceInfo(); }然后添加测试方法：@Test void testQueryDeviceInfo() { List<TbDeviceInfo> deviceInfos = deviceInfoMapper.queryDeviceInfo(); deviceInfos.forEach(System.out::println); }执行查询之后发现进行了一些不必要的关联：2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_1 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_1 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_1 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 16:59:34.566 INFO 20800 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_1 b on a.device_id = b.device_id TbDeviceInfo(id=1562725321956294657, deviceId=0, deviceIntro=0) TbDeviceInfo(id=1562725323428495361, deviceId=2, deviceIntro=2) TbDeviceInfo(id=1562725325781499905, deviceId=4, deviceIntro=4) TbDeviceInfo(id=1562725328981753857, deviceId=6, deviceIntro=6) TbDeviceInfo(id=1562725330357485569, deviceId=8, deviceIntro=8) TbDeviceInfo(id=1562725321956294657, deviceId=0, deviceIntro=0) TbDeviceInfo(id=1562725323428495361, deviceId=2, deviceIntro=2) TbDeviceInfo(id=1562725325781499905, deviceId=4, deviceIntro=4) TbDeviceInfo(id=1562725328981753857, deviceId=6, deviceIntro=6) TbDeviceInfo(id=1562725330357485569, deviceId=8, deviceIntro=8) TbDeviceInfo(id=1562725322774183937, deviceId=1, deviceIntro=1) TbDeviceInfo(id=1562725324024086530, deviceId=3, deviceIntro=3) TbDeviceInfo(id=1562725327215951874, deviceId=5, deviceIntro=5) TbDeviceInfo(id=1562725329636065281, deviceId=7, deviceIntro=7) TbDeviceInfo(id=1562725331007602689, deviceId=9, deviceIntro=9) TbDeviceInfo(id=1562725322774183937, deviceId=1, deviceIntro=1) TbDeviceInfo(id=1562725324024086530, deviceId=3, deviceIntro=3) TbDeviceInfo(id=1562725327215951874, deviceId=5, deviceIntro=5) TbDeviceInfo(id=1562725329636065281, deviceId=7, deviceIntro=7) TbDeviceInfo(id=1562725331007602689, deviceId=9, deviceIntro=9)因为关联时笛卡尔积的存在造成结果不准确，绑定表就是解决这种问题的。进行绑定表配置：spring: shardingsphere: sharding: # 配置绑定表 binding-tables: - tb_device,tb_device_info再次测试发现查询结果就是正常的2022-08-25 21:00:12.760 INFO 7316 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 21:00:12.760 INFO 7316 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_1 b on a.device_id = b.device_id 2022-08-25 21:00:12.760 INFO 7316 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_0 a left join tb_device_0 b on a.device_id = b.device_id 2022-08-25 21:00:12.760 INFO 7316 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select a.id,a.device_id,a.device_intro,b.device_type from tb_device_info_1 a left join tb_device_1 b on a.device_id = b.device_id TbDeviceInfo(id=1562725321956294657, deviceId=0, deviceIntro=0) TbDeviceInfo(id=1562725323428495361, deviceId=2, deviceIntro=2) TbDeviceInfo(id=1562725325781499905, deviceId=4, deviceIntro=4) TbDeviceInfo(id=1562725328981753857, deviceId=6, deviceIntro=6) TbDeviceInfo(id=1562725330357485569, deviceId=8, deviceIntro=8) TbDeviceInfo(id=1562725322774183937, deviceId=1, deviceIntro=1) TbDeviceInfo(id=1562725324024086530, deviceId=3, deviceIntro=3) TbDeviceInfo(id=1562725327215951874, deviceId=5, deviceIntro=5) TbDeviceInfo(id=1562725329636065281, deviceId=7, deviceIntro=7) TbDeviceInfo(id=1562725331007602689, deviceId=9, deviceIntro=9)广播表现在有这么⼀个场景， device_type列对应的tb_device_type表中的数据，不应该被分表，两个库中都应该有全量的该表的数据，这种表就是广播表。表结构如下：drop table if exists tb_device_type; CREATE TABLE `tb_device_type` ( `type_id` int NOT NULL AUTO_INCREMENT, `type_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, PRIMARY KEY (`type_id`) );配置广播表：spring: shardingsphere: sharding: tables: # 广播表主键生成策略 t_dict: key-generator: column: type_id type: SNOWFLAKE # 配置广播表 broadcast-tables: - tb_device_type 通过查看执行日志可以发现插入方法在两张表里面都进行了插入：2022-08-25 21:21:06.816 INFO 20652 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: INSERT INTO tb_device_type ( type_id, type_name ) VALUES (?, ?) ::: [1, ⼈脸考勤] 2022-08-25 21:21:06.816 INFO 20652 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: INSERT INTO tb_device_type ( type_id, type_name ) VALUES (?, ?) ::: [1, ⼈脸考勤] ... 2022-08-25 21:21:06.913 INFO 20652 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: INSERT INTO tb_device_type ( type_id, type_name ) VALUES (?, ?) ::: [2, ⼈脸通道] 2022-08-25 21:21:06.913 INFO 20652 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: INSERT INTO tb_device_type ( type_id, type_name ) VALUES (?, ?) ::: [2, ⼈脸通道]实现读写分离笔者未完全进行试验，此处仅仅给出配置文件# 配置真实数据源 spring.shardingsphere.datasource.names=m0,s0 # 配置主数据源 spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDa taSource spring.shardingsphere.datasource.m0.driver-classname=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://172.16.253.73:3306 /db_device?serverTimezone=Asia/Shanghai spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=123456 # 配置从数据源 spring.shardingsphere.datasource.s0.type=com.alibaba.druid.pool.DruidDa taSource spring.shardingsphere.datasource.s0.driver-classname=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.s0.url=jdbc:mysql://172.16.253.74:3306 /db_device?serverTimezone=Asia/Shanghai spring.shardingsphere.datasource.s0.username=root spring.shardingsphere.datasource.s0.password=123456 # 分配读写规则 spring.shardingsphere.sharding.master-slave-rules.ds0.master-datasource-name=m0 spring.shardingsphere.sharding.master-slave-rules.ds0.slave-datasource-names[0]=s0 # 确定实际表 spring.shardingsphere.sharding.tables.tb_user.actual-datanodes=ds0.tb_user # 确定主键⽣成策略 spring.shardingsphere.sharding.tables.t_dict.key-generator.column=id spring.shardingsphere.sharding.tables.t_dict.keygenerator.type=SNOWFLAKE # 开启显示sql语句 spring.shardingsphere.props.sql.show = true测试写数据@Test void testInsertUser(){ for (int i = 0; i < 10; i++) { TbUser user = new TbUser(); user.setName(""+i); userMapper.insert(user); } }测试读数据@Test void testQueryUser(){ List<TbUser> tbUsers = userMapper.selectList(null); tbUsers.forEach( tbUser -> System.out.println(tbUser)); }可以看到数据在主库中写，然后同步到了从库，读取只会从从库中读。连接模式及实现原理To Be Continued
Spring Data Jpa 动态表处理Jpa 分表问题现在有一张学生表t_stu按年份进行了处理，物理表分别是t_stu_2020、t_stu_2021、t_stu_2022这样 如果是mybatis，可以直接把表后缀传入sql，然后使用t_stu_${year}对表名进行拼接 但是对于Jpa这个全自动化ORM框架来说不太好处理，因为Jpa的表名是写在实体类的注解上的，在运行时不能修改：// 实体类 @Entity @Table(name = "t_stu") // 这里的表名定义是无法在运行时修改的 @Getter @Setter public class TVisionStu { private static final long serialVersionUID = 1L; @Id @Column(name = "id") private String id; @Column(name = "name") private String name; } // dao 层 @Repository public interface TVisionStuDao extends JpaRepository<TVisionStu, String>, JpaSpecificationExecutor<TVisionStu> {}解决方案Hibernate 插件方式Hibernate 插件可以在sql执行前对sql进行修改，从而达到动态表的功能import org.apache.commons.lang3.StringUtils; import org.hibernate.EmptyInterceptor; import org.springframework.stereotype.Component; @Component public class HibernateInterceptor extends EmptyInterceptor { @Override public String onPrepareStatement(String sql) { String schoolYear = ReqContextHolder.getSchoolYear(); // 这里对表名进行替换 if (StringUtils.isNotBlank(schoolYear)) { return sql.replaceAll("t_stu", "t_stu_" + schoolYear); } return super.onPrepareStatement(sql); } }其中的ReqContextHolder是一个线程安全的类，用来保存要查找的实际年份public class ReqContextHolder { private ReqContextHolder() { } protected static ThreadLocal<String> schoolYearThreadLocal = new ThreadLocal<>(); public static void setSchoolYear(String schoolYear) { schoolYearThreadLocal.set(schoolYear); } public static String getSchoolYear() { return schoolYearThreadLocal.get(); } public static void remove() { schoolYearThreadLocal.remove(); } }然后在application.yml中对插件进行配置：spring: # 设置jpa插件，可以实现分表 jpa: properties: hibernate: ejb: # 注册自定义的插件类 interceptor: com.power.aop.HibernateInterceptor实际使用的话就像这样： @RestController @RequestMapping("/stu") public class StuController{ @GetMapping("/query") public List<TVisionStu> query(@RequestParam("schoolYear") String schoolYear) { ReqContextHolder.setSchoolYear(schoolYear); List<TVisionStu> list = this.stuDao.findAll(); ReqContextHolder.remove(); return list; } }然后就可以到sql可以正常执行了。如果觉得每个方法都需要设置学年非常麻烦，可以写一个拦截器进行处理，这里就不再赘述了。sharding jdbc 方式sharding sphere 是专门解决分库分表问题的，不用写Hibernate插件这种比较hack的方式，只需要引入一个jdbc的jar包然后进行配置就行了To Be Continued...
Oracle 生僻字处理(mybatis、mybatis-plus、通用mapper)
问题：姓名中的生僻字查询显示问号?再现环境：数据库：Oracle Database 12c后端：JDK 1.8，mybatis姓名字段使用的是VARCHAR2保存生僻字䶮，数据库可视化工具显示为?，mybatis查询出来同样显示?解决方法（已验证）修改保存姓名的字段类型为NVARCHAR2：alter table xxx表 modify NAME NVARCHAR2(500);为mybatis添加NVARCHAR2的类型处理器：NVarcharTypeHandler类package com.power.isolated.aop; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * NVARCHAR汉字生僻字乱码问题。 * 主要是 NStringTypeHandler 中，没有setNString() */ @MappedJdbcTypes(JdbcType.NVARCHAR) public class NVarcharTypeHandler extends BaseTypeHandler<String> { /** * 重点，使用 setNString 设置 * @param ps * @param i * @param parameter * @param jdbcType * @throws SQLException */ @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setNString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } } 在 mybatis 配置文件mybatis-config.xml中配置类型处理器：<typeHandlers> <typeHandler handler="com.power.isolated.aop.NVarcharTypeHandler"/> </typeHandlers>注意标签的顺序，顺序不正可能导致启动出错。mybatis配置文件标签顺叙 properties-->settings-->typeAliases-->typeHandlers-->objectFactory-->objectWrapperFactory-->reflectorFactory-->plugins-->environments-->databaseIdProvider-->mappers修改mapper.xml文件中字段的类型，即 resultMap 中的 jdbcType 和参数中的 jdbcType：例如：<result column="NAME" property="isolatedName" jdbcType="NVARCHAR"/>或者select * from xxx where NAME = #{name, jdbcType=NVARCHAR}mybatis-plusmybatis-plus 除了需要上面的步骤以外，还需要修改实体类的注解：/** * 姓名 */ @TableField(value = "NAME", jdbcType = JdbcType.NVARCHAR, typeHandler = NVarcharTypeHandler.class) private String name;通用mapper通用mapper类似，修改实体类的注解：/** * 姓名 */ @ColumnType(column = "NAME", jdbcType = JdbcType.NVARCHAR, typeHandler = NVarcharTypeHandler.class) private String name;注：原本的@Column注解是javax.persistence包下面的，需要修改为tk.mybatis.mapper.annotation包下面的@ColumnType注解。经验证可以正确存取生僻字。
Spring中@Transactional失效Spring中的声明式注解@Transactional很大程度的方便了开发者进行DB数据保存。但是在一些特殊情况下，可能会造成注解不是按想定的方式生效，这里说几种可能造成的几种情况。常见的几种情况：异常被捕获这是一种比较简单不过稍不注意也可能会犯的情况。 Spring中事务提交还是回滚是根据调用的方法是否抛出异常来决定的，因此如果把异常捕获之后又不抛出的话，即使出了问题，事务还是会提交。@Autowired private ClassB b; @Autowired private ClassC c; @Transactional public void methodA(){ try { b.methodB(); c.methodC(); } catch (Exception e) { } }上例中，想要的结果是b.methodB()和c.methodC()同时提交或回滚，但是由于异常被捕获，即使在执行方法C的时候出现了异常，方法B的操作仍旧会生效。 （如果方法A是一次转账，方法B是转账中的加钱操作，方法C是减钱操作，B和C只执行其中一个的话会导致总金额就发生了变化）@Transactional修饰了非public方法这种也是有可能犯的一种情况。 @Transactional只能用于 public 的方法上，否则事务不会失效，如果要用在非 public 方法上，可以开启AspectJ 代理模式。（默认代理模式CGlib） CGlib是使用继承进行动态代理的，所以理论上protect方法无修饰符时应该也可以？没有测试，有空试一下。同一个类中的方法调用这是一种非常容易犯，又不容易察觉的情况。@Component public class A{ @Transactional public void methodA() { methodB(); } @Transactional public void methodB() { // do something } }上例中，methodB的事务是不生效的，因为这里是同一个类中的调用（更确切的说是同一个类同一个对象中），Spring事务的原理是调用时检查@Transactional注解，然后生成代理类进行事务管理，但是内部调用时不会生成代理类（或者说默认不会），因此也就无法进行事务管理。这种情况有好几种解决方法，下面会说到。非常见的情况非常见是指对@Transactional进行了一些属性的配置导致不生效。@Transactional 注解属性 rollbackFor 设置错误rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常（继承自 RuntimeException 的异常）或者 Error才回滚事务；其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常，但却期望 Spring 能够回滚事务，就需要指定rollbackFor属性。// 希望自定义的异常进行回滚 @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)@Transactional 注解属性 propagation 设置错误这种失效是由于配置错误，若是错误的配置以下三种 propagation，事务将不会发生回滚。PROPAGATION.SUPPORTS：如果当前存在事务，则加入该事务；如果当前没有事务，则以非事务的方式继续运行。PROPAGATION.NOT_SUPPORTED：以非事务方式运行，如果当前存在事务，则把当前事务挂起。PROPAGATION.NEVER：以非事务方式运行，如果当前存在事务，则抛出异常。propagation默认值PROPAGATION.REQUIRED：没有事务时创建事务。有事务了则加入该事务（相当于使用一个session）同一个类中事务方法调用解决方法方法拆分这是一种最简单的方法，也就是把上面例子中的methodB拆分到一个单独的类里面，这样就是一般情况下的事务调用。下面三种方法都是在methodB上添加@Transactional(propagation = Propagation.REQUIRES_NEW)之后进行的测试使用AspectJ代理具体操作就是，application.yml中需要配置spring.aop.auto ：true，然后在启动类开启AspectJ代理，并暴露代理类：@EnableAspectJAutoProxy(exposeProxy = true)这样的话就可以在调用的时候获取到代理类，并进行方法调用：((TestClassA)AopContext.currentProxy()).insertB();从ApplicationContext获取Bean这个原理应该是和上面一样的，直从ApplicationContext中获取到当前Bean，然后再调用方法：// applicationContext 可以自动注入 applicationContext.getBean(TestClassA.class).insertB();注入自身@Component public class TestClassA { @Autowired private TestClassA testClassA; }用这种方法也可以使methodB的事务生效，但是需要注意的是，第一次生成的代理类和自动注入的代理类不是一个对象，也就是代码中this和testClassA不是同一个对象，并且testclassA中不会再次自动注入。所以如果methodB插入数据时需要当前对象的属性，这种方法便不能再使用总之，同类事务不生效是一种很容易疏忽的情况，具体怎么处理，还是要结合业务。
MongoDB基础传统关系型数据库在面对在面对高并发、数据量大、高扩展和高可用时有些力不从心，MongoDB则比较容易应对以上情况。存储结构SQL术语/概念MongoDB术语/概念解释/说明databasedatabase数据库tablecollection数据库表/集合rowdocument数据记录行/文档columnfield数据字段/域indexindex索引table joins 表连接,MongoDB不支持 嵌入文档MongoDB通过嵌入式文档来替代多表连接primary keyprimary key主键,MongoDB自动将_id字段设置为主键数据类型shell即在控制台与Mongodb进行交互MongoDB中一行数据是以JSON格式进行存储的。数据类型详表数据类型描述举例字符串UTF-8字符串都可表示为字符串类型的数据{"x" : "foobar"}对象id对象id是文档的12字节的唯一 ID{"X" :ObjectId() }布尔值真或者假：true或者false{"x":true}数组值的集合或者列表可以表示成数组{"x" ： ["a", "b", "c"]}32位整数类型不可用。JavaScript仅支持64位浮点数，所以32位整数会被自动转换。shell是不支持该类型的，shell中默认会转换成64位浮点数64位整数不支持这个类型。shell会使用一个特殊的内嵌文档来显示64位整数shell是不支持该类型的，shell中默认会转换成64位浮点数64位浮点数shell中的数字就是这一种类型{"x"：3.14159，"y"：3}null表示空值或者未定义的对象{"x":null}undefined文档中也可以使用未定义类型{"x":undefined}符号shell不支持，shell会将数据库中的符号类型的数据自动转换成字符串 正则表达式文档中可以包含正则表达式，采用JavaScript的正则表达式语法{"x" ： /foobar/i}代码文档中还可以包含JavaScript代码{"x" ： function() { / …… / }}二进制数据二进制数据可以由任意字节的串组成，不过shell中无法使用 最大值/最小值BSON包括一个特殊类型，表示可能的最大值。shell中没有这个类型。 安装Windows安装从官网下载zip包 版本命名规则：x.y.zy为奇数时表示当前版本为开发版，如：1.5.2、4.1.13y为偶数时表示当前版本为稳定版，如：1.6.3、4.0.10z是修正版本号，数字越大越好解压后在目录内手动建立一个目录用于存放数据文件，如data/db；然后进入bin目录，在此处打开控制台执行：mongod --dbpath=..\data\db即可看到启动，默认端口为27017，也可以通过--port指定端口配置文件方式启动可以在data目录中创建一个config文件夹，创建一个mongod.conf文件，写入内容：storage: #The directory where the mongod instance stores its data.Default Value is "\data\db" on Windows. dbPath: ../data/db然后在bin目录控制台执行mongod -f ../data/config/mongod.conf #或者 mongod --config ../data/config/mongod.conf即可启动。Linux安装先到官网下载压缩包mongod-linux-x86_64-4.0.10.tgz。上传到Linux并解压：tar -xvf mongodb-linux-x86_64-4.0.10.tgz移动：mv mongodb-linux-x86_64-4.0.10 /usr/local/mongodb创建目录：#数据存储目录 mkdir -p /mongodb/single/data/db #日志存储目录 mkdir -p /mongodb/single/log新建并修改配置文件：vim /mongodb/single/mongod.conf，文件内容如下systemLog: #MongoDB发送所有日志输出的目标指定为文件 destination: file #mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径 path: "/mongodb/single/log/mongod.log" #当mongos或mongod实例重新启动时，mongos或mongod会将新条目附加到现有日志文件的末尾。 logAppend: true storage: #mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod。 dbPath: "/mongodb/single/data/db" journal: #启用或禁用持久性日志以确保数据文件保持有效和可恢复。 enabled: true processManagement: #启用在后台运行mongos或mongod进程的守护进程模式。 fork: true net: #服务实例绑定的IP，默认是localhost bindIp: localhost,192.168.40.141 #bindIp #绑定的端口，默认是27017 port: 27017然后启动服务：[root@linux single]# /usr/local/mongodb/bin/mongod -f /mongodb/single/mongod.conf about to fork child process, waiting until server is ready for connections. forked process: 90384 child process started successfully, parent exiting关闭服务连接Shell：在bin目录执行mongo 正常关闭：#连接到服务器（有可能需要认证） #切换到admin库 use admin #关闭服务 db.shutdownServer()快捷方法（不推荐，有可能数据损坏）：Windows下在控制台Ctrl + C即可。 Linux下，使用kill命令如果数据损坏删除lock文件：rm -f /mongodb/single/data/db/*.lock修复数据：/usr/local/mongdb/bin/mongod --repair --dbpath=/mongodb/single/data/db可视化客户端在官网下载官方客户端，然后安装即可。（Navicat也可以连接MongoDB）简单使用首先给出一个如下结构的数据库：数据库：articledb专栏文章评论comment 字段名称字段含义字段类型备注_idIDObjectId或StringMongo的主键的字段articleid文章IDString content评论内容String userid评论人IDString nickname评论人昵称String createdatetime评论的日期时间Date likenum点赞数Int32 replynum回复数Int32 state状态String0：不可见；1：可见；parentid上级IDString如果为0表示文章的顶级评论数据库操作创建数据库选择数据库，如果数据库不存在则自动创建，例如，以下语句创建spitdb数据库：use articledb查看所有数据库：show dbs #或 show databases注意: 在 MongoDB 中，集合只有在内容插入后才会创建! 就是说，创建集合(数据表)后要再插入一个文档(记录)，集合才会真正创建，因为此时use的库是在内存中，show的库是在硬盘上，所以上述命令只能看到默认的三个库。查看当前正在使用的数据库命令：dbMongoDB 中默认的数据库为 test，如果你没有选择数据库，集合将存放在 test 数据库中。另外：数据库名可以是满足以下条件的任意UTF-8字符串。不能是空字符串（"")。不得含有' '（空格)、.、$、/、\和\0 (空字符)。应全部小写。最多64字节。有一些数据库名是保留的，可以直接访问这些有特殊作用的数据库。admin： 从权限的角度来看，这是"root"数据库。要是将一个用户添加到这个数据库，这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行，比如列出所有的数据库或者关闭服务器。local: 这个数据永远不会被复制，可以用来存储限于本地单台服务器的任意集合config: 当Mongo用于分片设置时，config数据库在内部使用，用于保存分片的相关信息。删除数据库db.dropDatabase()主要用来删除已经持久化的数据库集合操作显式创建集合（一般不用）db.createCollection(集合名)查看集合：show collections 或 show tables集合的命名规范：集合名不能是空字符串""。集合名不能含有\0字符（空字符)，这个字符表示集合名的结尾。集合名不能以"system."开头，这是为系统集合保留的前缀。用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含，这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合，否则千万不要在名字里出现$。 隐式创建集合：当向一个集合中插入一个文档的时候，如果集合不存在，则会自动创建集合（通常使用这种方式）。删除集合db.集合名.drop()文档的CRUD文档的数据结构和JSON基本一致，但存储时用的是BSON，可以理解为二进制JSON。文档插入单个文档的插入使用insert() 或 save() 方法向集合中插入文档：db.collection.insert( <document or array of documents>, { writeConcern: <document>, ordered: <boolean> } )ParameterTypeDescriptiondocumentdocument or array要插入到集合中的文档或文档数组。（json格式）writeConcerndocument写入关注（即数据写入后是否确认，即确认程度），等级越高，数据越安全，但同时效率也更第orderedboolean可选。如果为真，则按顺序插入数组中的文档，如果其中一个文档出现错误，MongoDB将返回而不处理数组中的其余文档。如果为假，则执行无序插入，如果其中一个文档出现错误，则继续处理数组中的主文档。在版本2.6+中默认为true后面两个参数一般不用写。示例：db.comment.insert({"articleid":"100000","content":"今天天气真好，阳光明媚","userid":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null})提示：comment集合如果不存在，则会隐式创建mongo中的数字，默认情况下是double类型，如果要存整型，必须使用函数NumberInt(整型数字)，否则取出来就有问题了。插入当前日期使用new Date()插入的数据没有指定_id，会自动生成主键值如果某字段没值，可以赋值为null，或不写该字段。执行后，提示如下，说明插入一个数据成功了WriteResult({ "nInserted" : 1 })注意：文档中的键/值对是有序的。文档中的值不仅可以是在双引号里面的字符串，还可以是其他几种数据类型（甚至可以是整个嵌入的文档)。MongoDB区分类型和大小写。MongoDB的文档不能有重复的键。文档的键是字符串。除了少数例外情况，键可以使用任意UTF-8字符。文档键命名规范：键不能含有\0 (空字符)。这个字符用来表示键的结尾。.和$有特别的意义，只有在特定环境下才能使用。以下划线"_"开头的键是保留的(不是严格要求的)。批量插入db.collection.insertMany( [ <document 1> , <document 2>, ... ], { writeConcern: <document>, ordered: <boolean> } )下面是一个多个插入的例子例子db.comment.insertMany([ {"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上，健康很重要，一杯温水幸福你我他。","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-08-05T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"}, {"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水，冬天喝温开水","userid":"1005","nickname":"伊人憔悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"}, {"_id":"3","articleid":"100001","content":"我一直喝凉开水，冬天夏天都喝。","userid":"1004","nickname":"杰克船长","createdatetime":new Date("2019-08-06T01:05:06.321Z"),"likenum":NumberInt(666),"state":"1"}, {"_id":"4","articleid":"100001","content":"专家说不能空腹吃饭，影响健康。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T08:18:35.288Z"),"likenum":NumberInt(2000),"state":"1"}, {"_id":"5","articleid":"100001","content":"研究表明，刚烧开的水千万不能喝，因为烫嘴。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T11:01:02.521Z"),"likenum":NumberInt(3000),"state":"1"} ]);经测试insertMany()和insert()没啥区别，只是返回值不同。插入时指定了_id，则主键就是该值。文档查询db.collection.find(<query>, [projection])ParameterTypeDescriptionquerydocument可选。使用查询运算符指定选择筛选器。若要返回集合中的所有文档，请省略此参数或传递空文档({})。projectiondocument可选。指定要在与查询筛选器匹配的文档中返回的字段（投影）。若要返回匹配文档中的所有字段，请省略此参数。简单来说，第一个参数是查询条件，第二个参数是查询的字段。例如//查询userid为1003的数据 db.comment.find({userid:'1003'}) //查询userid为1003的userid和nickname两个字段的数据 db.comment.find({userid:'1003'},{userid:1,nickname:1}) //默认_id会显示， //查询结果只显示userid、nickname，不显示_id db.comment.find({userid:'1003'},{userid:1,nickname:1,_id:0}) //查询所有数据，但只显示_id、userid、nickname db.comment.find({},{userid:1,nickname:1})文档的修改db.collection.update(query, update, options) //或 db.collection.update( <query>, <update>, { upsert: <boolean>, multi: <boolean>, writeConcern: <document>, collation: <document>, arrayFilters: [ <filterdocument1>, ... ], hint: <document|string> // Available starting in MongoDB 4.2 } )这里主要关注前四个参数：<query>：更新的条件，相当于SQL中WHERE后面的条件。<update>：更新的内容及规则。<upsert>：（可选）True时未找到文档将创建，默认为False<multi>：（可选）True时更新符合条件的所有文档，默认为False，只更新找到的第一条数据。示例：文档的覆盖修改：db.comment.update({_id:"1"},{likenum:NumberInt(1001)})此时，_id为1的该文档除去likenum字段外，其他字段消失，因为是覆盖修改。局部修改一般情况下，都是使用局部修改：db.comment.update({_id:"2"},{$set:{likenum:NumberInt(889)}})$set代表设置为该值（最常用）。类似的还有：{$inc:{field:value}}：对数字类型的field增加value{$unset:{field:1}}：删除该字段{$push:{field:value}}：把value追加到field里。field只能是数组类型，如果field不存在，会自动插入一个数组类型{$pushAll:{field:value_array}}：用法同$push一样，只是$pushAll可以一次追加多个值到一个数组字段内{$addToSet:{field:value}}：加一个值到数组内，而且只有当这个值在数组中不存在时才增加$pop：删除数组内第一个值：{$pop:{field:-1}}、删除数组内最后一个值：{$pop:{field:1}}{$pull:{field:_value}}：从数组field内删除一个等于_value的值{$pullAll:value_array}：用法同$pull一样，可以一次性删除数组内的多个值{$rename:{old_field_name:new_field_name}}：对字段进行重命名删除文档db.集合名称.remove(query)查询文档统计查询db.collection.count(query, options)ParameterTypeDescriptionquerydocument查询选择条件。optionsdocument可选。用于修改计数的额外选项。示例：//统计comment集合的所有的记录数： db.comment.count() //统计userid为1003的记录条数 db.comment.count({userid:"1003"})分页查询可以使用limit()指定展示多少条，使用skip()方法来跳过指定数量的数据：db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)例如：db.comment.find().limit(2).skip(1) //如果find()查询到5条，则上述语句会展示第2、3条，因为第1条被跳过排序查询sort()方法可以通过参数指定排序的字段，并使用 1 和 -1 来指定排序的方式，其中 1 为升序排列，而 -1 是用于降序排列。例如对userid降序排列，并对likenum进行升序排列db.comment.find().sort({userid:-1,likenum:1})skip(), limilt(), sort()三个放在一起执行的时候，执行的顺序是先 sort(), 然后是 skip()，最后是显示的 limit()，和命令编写顺序无关。正则查询MongoDB的模糊查询是通过正则表达式的方式实现的：db.collection.find({field:/正则表达式/})比较查询<, <=, >, >= 这个操作符也是很常用的：db.collection_name.find({"field":{ $gt:value}})其中$gt代表大于，类似的还有$gt：大于$lt：小于$gte：大于等于$lte：小于等于$ne：不等于包含查询包含使用$in操作符：//查询评论的集合中userid字段包含1003或1004的文档 db.comment.find({userid:{$in:["1003","1004"]}})条件连接查询如果需要查询同时满足两个以上条件，需要使用$and操作符将条件进行关联。（相当于SQL的and）$and:[{条件1},{条件2},{条件3}]例如：db.comment.find({$and:[{likenum:{$gte:NumberInt(700)}},{likenum:{$lt:NumberInt(2000)}}]})同理如果是或的关系，使用$or：$or:[{条件1},{条件2},{条件3}]常用命令小结：选择切换数据库：use articledb插入数据：db.comment.insert({bson数据})查询所有数据：db.comment.find()条件查询数据：db.comment.find({条件})查询符合条件的第一条记录：db.comment.findOne({条件})查询符合条件的前几条记录：db.comment.find({条件}).limit(条数)查询符合条件的跳过的记录：db.comment.find({条件}).skip(条数)修改数据：db.comment.update({条件},{修改后的数据}) 或db.comment.update({条件},{$set:{要修改部分的字段:数据})修改数据并自增某字段值：db.comment.update({条件},{$inc:{自增的字段:步进值}})删除数据：db.comment.remove({条件})统计查询：db.comment.count({条件})模糊查询：db.comment.find({字段名:/正则表达式/})`条件比较运算：`db.comment.find({字段名:{$gt:值}})包含查询：db.comment.find({字段名:{$in:[值1，值2]}})或db.comment.find({字段名:{$nin:[值1，值2]}})条件连接查询：db.comment.find({$and:[{条件1},{条件2}]})或db.comment.find({$or:[{条件1},{条件2}]})索引MongoDB索引使用B树数据结构（确切的说是B-Tree，MySQL是B+Tree）单字段索引MongoDB支持在文档的单个字段上创建用户定义的升序/降序索引，称为单字段索引（Single Field Index）。比如{score: 1} Index： 复合索引MongoDB还支持多个字段的用户定义索引，即复合索引（Compound Index）。复合索引中列出的字段顺序具有重要意义。例如，如果复合索引由{ userid: 1, score: -1 }组成，则索引首先按userid正序排序，然后在每个userid的值内，再在按score倒序排序。其他索引地理空间索引（Geospatial Index）、文本索引（Text Indexes）、哈希索引（Hashed Indexes）。地理空间索引（Geospatial Index）为了支持对地理空间坐标数据的有效查询，MongoDB提供了两种特殊的索引：返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。文本索引（Text Indexes）MongoDB提供了一种文本索引类型，支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词（例如“the”、“a”、“or”），而将集合中的词作为词干，只存储根词。哈希索引（Hashed Indexes）为了支持基于散列的分片，MongoDB提供了散列索引类型，它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机，但只支持相等匹配，不支持基于范围的查询。索引的管理操作查看索引返回一个集合中的所有索引的数组db.collection.getIndexes()运行该命令后的返回结果是：//执行 db.comment.getIndexes() //结果 [ { "v": NumberInt("2"), "key": { "_id": NumberInt("1") }, "name": "_id_", "ns": "articledb.comment" } ]结果显示默认字段_id的索引，名称为_id_。默认_id索引： MongoDB在创建集合的过程中，在_id字段上创建一个唯一的索引，默认名字为_id_，该索引可防止客户端插入两个具有相同值的文档，您不能在_id字段上删除此索引。 注意：该索引是唯一索引，因此值不能重复，即_id值不能重复的。在分片集群中，通常使用_id作为片键。创建索引db.collection.createIndex(keys, options)ParameterTypeDescriptionkeysdocument包含字段和值对的文档，其中字段是索引键，值描述该字段的索引类型。对于字段上的升序索引，请指定值1；对于降序索引，请指定值-1。比如：{字段:1或-1}，其中1 为指定按升序创建索引，如果你想按降序来创建索引指定为 -1 即可。另外，MongoDB支持几种不同的索引类型，包括文本、地理空间和哈希索引。optionsdocument可选。包含一组控制索引创建的选项的文档。有关详细信息，请参见选项详情列表。详情列表Options选项ParameterTypeDescriptionbackgroundBoolean建索引过程会阻塞其它数据库操作，background可指定以后台方式创建索引，即增加 "background" 可选参数。 "background" 默认值为false。uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.namestring索引的名称。如果未指定，MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。dropDupsBoolean3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false.sparseBoolean对文档中不存在的字段数据不启用索引；这个参数需要特别注意，如果设置为true的话，在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.expireAfterSecondsinteger指定一个以秒为单位的数值，完成 TTL设定，设定集合的生存时间。vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。weightsdocument索引权重值，数值在 1 到 99,999 之间，表示该索引相对于其他索引字段的得分权重。default_languagestring对于文本索引，该参数决定了停用词及词干和词器的规则的列表。 默认为英语language_overridestring对于文本索引，该参数指定了包含在文档中的字段名，语言覆盖默认的language，默认值为 language.单字段索引创建示例//对userid创建索引 db.comment.createIndex({userid:1}) //结果 { "createdCollectionAutomatically": false, "numIndexesBefore": NumberInt("1"), "numIndexesAfter": NumberInt("2"), "ok": 1 }复合索引创建示例：//对userid和nickname同时创建复合索引 db.comment.createIndex({userid:1,nickname:-1}) //返回结果 { "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 3, "ok" : 1 }移除索引移除指定索引//指定要删除的索引。可以通过索引名称或索引规范文档指定索引。若要删除文本索引，请指定索引名称。 db.collection.dropIndex(index)示例//删除comment集合中userid字段上的升序索引 db.comment.dropIndex({userid:1}) //返回结果 { "nIndexesWas" : 3, "ok" : 1 }移除所有索引db.collection.dropIndexes()索引的使用执行计划分析查询性能（Analyze Query Performance）通常使用执行计划（解释计划、Explain Plan）来查看查询的情况，如查询耗费的时间、是否基于索引查询等。 通常，我们想知道，建立的索引是否有效，效果如何，都需要通过执行计划查看。db.collection.find(query,options).explain(options)比如：根据userid查询db.comment.find({userid:"1003"}).explain() //返回结果 { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "articledb.comment", "indexFilterSet" : false, "parsedQuery" : { "userid" : { "$eq" : "1003" } }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "userid" : { "$eq" : "1003" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "9ef3740277ad", "port" : 27017, "version" : "4.0.10", "gitVersion" : "c389e7f69f637f7a1ac3cc9fae843b635f20b766" }, "ok" : 1 }关键点看："stage" : "COLLSCAN",表示全集合扫描然后对userid建立索引db.comment.createIndex({userid:1})再次执行上面的查询可以看到："stage" : "IXSCAN"，基于索引的扫描。涵盖查询（Covered Queries）当查询条件和查询的投影仅包含索引字段时，MongoDB直接从索引返回结果，而不扫描任何文档或将文档带入内存。 这些覆盖的查询可以非常有效。Spring结合MongoDB现有数据库：articledbarticledb专栏文章评论comment 字段名称字段含义字段类型备注_idIDObjectId或StringMongo的主键的字段articleid文章IDString content评论内容String userid评论人IDString nickname评论人昵称String createdatetime评论的日期时间Date likenum点赞数Int32 replynum回复数Int32 state状态String0：不可见；1：可见；parentid上级IDString如果为0或置空表示文章的顶级评论技术选型选择使用SpringDataMongoDB，SpringData家族成员之一，用于操作MongoDB的持久层框架，封装了底层的mongodb-driver（相当于mysql的jdbc）。创建SpringBoot工程首先创建一个maven工程，引入依赖如下：pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>article</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>然后创建启动类以及application.yml文件：启动类和application.ymlpackage com.example.article; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ArticleApplication { public static void main(String[] args) { SpringApplication.run(ArticleApplication.class, args); } }spring: #数据源配置 data: mongodb: # 主机地址 host: localhost # 数据库 database: articledb # 默认端口是27017 port: 27017 #也可以使用uri连接 #uri: mongodb://192.168.40.134:27017/articledb然后创建实体类：Commentpackage com.example.article.po; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Date; /** * 文章评论实体类 */ //把一个java类声明为mongodb的文档，可以通过collection参数指定这个类对应的文档。 //@Document(collection="mongodb 对应 collection 名") // 若未加 @Document ，该 bean save 到 mongo 的 comment collection // 若添加 @Document ，则 save 到 comment collection @Document(collection="comment")//可以省略，如果省略，则默认使用类名小写映射集合 //复合索引 @CompoundIndex( def = "{'userid': 1, 'nickname': -1}") @Data public class Comment implements Serializable { //主键标识，该属性的值会自动对应mongodb的主键字段"_id"，如果该属性名就叫“id”,则该注解可以省略，否则必须写 @Id private String id;//主键 //该属性对应mongodb的字段的名字，如果一致，则无需该注解 @Field("content") private String content;//吐槽内容 private Date publishtime;//发布日期 //添加了一个单字段的索引 @Indexed private String userid;//发布人ID private String nickname;//昵称 private LocalDateTime createdatetime;//评论的日期时间 private Integer likenum;//点赞数 private Integer replynum;//回复数 private String state;//状态 private String parentid;//上级ID private String articleid; @Override public String toString() { return "Comment{" + "id='" + id + '\'' + ", content='" + content + '\'' + ", publishtime=" + publishtime + ", userid='" + userid + '\'' + ", nickname='" + nickname + '\'' + ", createdatetime=" + createdatetime + ", likenum=" + likenum + ", replynum=" + replynum + ", state='" + state + '\'' + ", parentid='" + parentid + '\'' + ", articleid='" + articleid + '\'' + '}'; } }其中可以在MongoDB中添加字段索引。然后创建repository接口：CommentRepositorypackage com.example.article.dao; import com.example.article.po.Comment; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; //评论的持久层接口 public interface CommentRepository extends MongoRepository<Comment,String> { } 然后是service层CommentServicepackage com.example.article.service; import com.example.article.dao.CommentRepository; import com.example.article.po.Comment; import com.example.article.utils.IdWorker; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.util.List; //评论的业务层 @Service public class CommentService { //注入dao @Autowired private CommentRepository commentRepository; /** * 保存一个评论 * @param comment */ public void saveComment(Comment comment){ //如果需要自定义主键，可以在这里指定主键；如果不指定主键，MongoDB会自动生成主键 //设置一些默认初始值。。。 //调用dao commentRepository.save(comment); } /** * 更新评论 * @param comment */ public void updateComment(Comment comment){ //调用dao commentRepository.save(comment); } /** * 根据id删除评论 * @param id */ public void deleteCommentById(String id){ //调用dao commentRepository.deleteById(id); } /** * 查询所有评论 * @return */ public List<Comment> findCommentList(){ //调用dao return commentRepository.findAll(); } /** * 根据id查询评论 * @param id * @return */ public Comment findCommentById(String id){ //调用dao return commentRepository.findById(id).get(); } } 然后在service类上使用idea快捷键Ctrl + Shift + T可以快速创建测试类：测试类package com.example.article.service; import com.example.article.ArticleApplication; import com.example.article.po.Comment; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDateTime; import java.util.List; //测试评论的业务层 //SpringBoot的Junit集成测试 @RunWith(SpringRunner.class) //SpringBoot的测试环境初始化，参数：启动类 @SpringBootTest(classes = ArticleApplication.class) public class CommentServiceTest { //注入Service @Autowired private CommentService commentService; /** * 保存一个评论 */ @Test public void testSaveComment(){ Comment comment=new Comment(); comment.setArticleid("100000"); comment.setContent("测试添加的数据"); comment.setCreatedatetime(LocalDateTime.now()); comment.setUserid("1003"); comment.setNickname("凯撒大帝"); comment.setState("1"); comment.setLikenum(0); comment.setReplynum(0); commentService.saveComment(comment); } /** * 查询所有数据 */ @Test public void testFindAll(){ List<Comment> list = commentService.findCommentList(); System.out.println(list); } /** * 测试根据id查询 */ @Test public void testFindCommentById(){ Comment comment = commentService.findCommentById("5d6a27b81b8d374798cf0b41"); System.out.println(comment); } } 接着便可以进行简单的查询测试。根据上级ID查询文章评论的分页列表CommentRepository新增方法定义分页方法//根据父id，查询子评论的分页列表 Page<Comment> findByParentid(String parentid, Pageable pageable);//Page、Pageable都是spring.data.domain下的类CommentService新增方法调用分页方法/** * 根据父id查询分页列表 * @param parentid * @param page * @param size * @return */ public Page<Comment> findCommentListPageByParentid(String parentid,int page ,int size){ //为什么要page-1？因为参数穿进去的是zero-based page index.，基于0开始的索引的页码 return commentRepository.findByParentid(parentid, PageRequest.of(page-1,size)); }测试类中添加测试方法：测试方法/** * 测试根据父id查询子评论的分页列表 */ @Test public void testFindCommentListPageByParentid(){ Page<Comment> pageResponse = commentService.findCommentListPageByParentid("3", 1, 2); System.out.println("----总记录数："+pageResponse.getTotalElements()); System.out.println("----当前页数据："+pageResponse.getContent()); }插入一条测试数据：插入测试数据db.comment.insert({"id":"33", "content":"你年轻，火力大", "publishtime":null, "userid":"1003", "nickname":"凯撒大帝", "createdatetime":null, "likenum":null, "replynum":null, "state":"null", "parentid":"3", "articleid":"100001"})执行测试。MongoTemplate实现评论点赞简单实现（效率低）：/** * 点赞-效率低 * @param id */ public void updateCommentThumbupToIncrementingOld(String id){ Comment comment = CommentRepository.findById(id).get(); comment.setLikenum(comment.getLikenum()+1); CommentRepository.save(comment); }但是我们只需要将点赞数加1就可以了，没必要查询出所有字段修改后再更新所有字段。可以使用MongoTemplate来实现：CommentService：//注入MongoTemplate @Autowired private MongoTemplate mongoTemplate; /** * 点赞数+1 * @param id */ public void updateCommentLikenumThumbup(String id){ //查询对象 Query query=Query.query(Criteria.where("_id").is(id)); //更新对象 Update update=new Update(); //局部更新，相当于$set //update.set(key,value) //递增$inc //update.inc("likenum",1); update.inc("likenum"); //参数1：查询对象 //参数2：更新对象 //参数3：集合的名字或实体类的类型Comment.class mongoTemplate.updateFirst(query,update,"comment"); }添加测试方法：/** * 点赞数+1 */ @Test public void testUpdateCommentLikenumThumbup(){ //对3号文档的点赞数+1 commentService.updateCommentLikenumThumbup("3"); }即可发现测试成功。