MyBatis实用方案,如何使项目兼容多种数据库

news2025/5/27 3:44:59

系列文章目录

MyBatis缓存原理
Mybatis plugin 的使用及原理
MyBatis+Springboot 启动到SQL执行全流程
数据库操作不再困难,MyBatis动态Sql标签解析
Mybatis的CachingExecutor与二级缓存
使用MybatisPlus还是MyBaits ,开发者应该如何选择?
巧用MybatisPlus的SQL注入器提升批量插入性能


在这里插入图片描述

上一次我们给大家详细讲解如何对接多数据源。不过对于一些toB的项目,因为客户指定的数据库不同,如何让自己的产品兼容多种数据库往往更迫切。本期我们就讲讲如何设置MyBatis,可以快速使项目兼容多种数据库

📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 MyBatis专栏 专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待


一、启用数据库识别

1. 调查数据库产品名

要想做兼容多种数据库,那毫无疑问,我们首先得明确我们要兼容哪些数据库,他们的数据库产品名称是什么。得益于SPI设计,java语言制定了一个DatabaseMetaData接口,要求各个数据库的驱动都必须提供自己的产品名。因此我们如果想要兼容某数据库,只要在对应的驱动包中找到其对DatabaseMetaData的实现即可。

比如Mysql的驱动包 mysql-connector-java 下的 DatabaseMetaData

在这里插入图片描述

Oracle 的驱动包 com.oracle.ojdbc6 下的 OracleDatabaseMetaData

在这里插入图片描述

2. 启用databaseId

既然各个驱动都提供了产品名,那么接下来就是让项目在启动中能够识别这些数据库,并赋予以不同数据库不同的id。MyBatis 其实有这项功能,但是这个功能默认没有被启用,若要启用我们首先得建立一个配置,即databaseIdProvider,可以在配置类里面加上这个Bean来实现

@Configuration //配置类
public class MyBatisConfig {
    @Bean
    public DatabaseIdProvider getDatabaseIdProvider() {
        DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        Properties properties = new Properties();
        // Key值(即产品名)来源于数据库,需要提前查清楚 ,
        // value值(即databaseId)可以随便填,你填“1” "2" "3"也行,但建议有明确意义,像下面这样
        properties.setProperty("0racle", "oracle");
        properties.setProperty("MySQL", "mysql");
        properties.setProperty("DB2", "db2");
        properties.setProperty("Derby", "derby");
        properties.setProperty("H2", "h2");
        properties.setProperty("HSQL", "hsql");
        properties.setProperty("Informix", "informix");
        properties.setProperty("MS-SQL", "ms-sql");
        properties.setProperty("PostgresqL", "racle");
        properties.setProperty("sybase", "sybase");
        properties.setProperty("Hana", "hana");
        databaseIdProvider.setProperties(properties);
        return databaseIdProvider;
    }
}

完成了上述配置后,我们的项目就能主动去识别数据库类型了。

二、SQL语法鉴别

对于大部分SQL,因为有SQL规范的限制,它们通常是通用的,一段SQL可以在不同的数据库上跑。但是对于部分复杂SQL,就得针对不同数据库,来写不同的SQL了,我们以Mysql 、 Oracle 为例,看一些常见功能的语法差异

1. 分页查询

MySQL中使用LIMIT关键字来实现分页查询,例如:

SELECT * FROM table_name LIMIT offset, count;

而Oracle中使用ROWNUM关键字来实现分页查询,例如:

SELECT * 
FROM (SELECT t.*, ROWNUM AS rn 
      FROM table_name t 
      WHERE ROWNUM <= offset + count) 
WHERE rn > offset;

2. 获取当前时间

MySQL中可以使用NOW()函数来获取当前时间,例如:

SELECT NOW();

而Oracle中可以使用SYSDATE关键字来获取当前时间,例如:

SELECT SYSDATE FROM DUAL;

3. 获取自增主键的值

MySQL中可以使用LAST_INSERT_ID()函数来获取最后插入行的自动生成的主键值,例如:

INSERT INTO table_name (column1, column2) VALUES(value1, value2);
SELECT LAST_INSERT_ID();

而Oracle中可以使用SEQUENCE和CURRVAL来获取自增主键的值,例如:

INSERT INTO table_name (column1, column2) VALUES(seq.nextval, value2);
SELECT seq.currval from dual;

4. 转换数据类型

MySQL 使用 CAST() 或 CONVERT() 函数转换数据类型,例如:

SELECT CAST('123' AS SIGNED) AS converted_value;  
-- 或者  
SELECT CONVERT('123', SIGNED) AS converted_value;

而Oracle使用 TO_NUMBER(), TO_CHAR(), TO_DATE() 等函数进行数据类型转换,例如:

SELECT TO_NUMBER('123') AS converted_value FROM DUAL;

5. 字符串拼接

MySQL中可以使用CONCAT()函数来进行字符串拼接,例如:

SELECT CONCAT(column1, column2) FROM table_name;

而Oracle中可以使用||运算符来进行字符串拼接,例如:

SELECT column1 || column2 FROM table_name;

6. 字符串截取

MySQL 使用 SUBSTRING() 函数,例如:

SELECT SUBSTRING('Hello World', 1, 5) AS substring_result;

而Oracle 使用 SUBSTR() 函数,例如:

SELECT SUBSTR('Hello World', 1, 5) AS substring_result FROM DUAL;

7. 判空函数

MySQL中可以使用IFNULL()函数来进行字符串拼接,例如:

SELECT IFNULL(column1, "1") FROM table_name;

而Oracle中可以使用NVL()来进行字符串拼接,例如:

SELECT NVL(column1, "1") FROM table_name;

8. 正则表达式

MySQL 使用 REGEXP 或 RLIKE 进行正则表达式匹配,例如:

SELECT 'Hello World' REGEXP '^Hello' AS is_matched;

而Oracle 使用 REGEXP_LIKE, REGEXP_INSTR, REGEXP_SUBSTR, 和 REGEXP_REPLACE 等函数,例如:

SELECT CASE WHEN REGEXP_LIKE('Hello World', '^Hello') THEN 'Matched' ELSE 'Not Matched' END AS is_matched FROM DUAL;

9. 窗口函数

MySQL 低版本不支持窗口函数,可以使用自连接模拟窗口函数,例如:

SELECT t1.*
FROM table_name t1
LEFT JOIN table_name t2
ON t1.column_name = t2.column_name AND t1.order_column > t2.order_column
WHERE t2.column_name IS NULL;

而Oracle 或 MySQL高版本则可以 使用 窗口函数,例如:

SELECT * 
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY column_name ORDER BY order_column) as rn 
      FROM table_name) as t
WHERE rn = 1;

三、SQL兼容处理

如果我们的项目有SQL语法不兼容的情况,如上面那些场景,那么我们就需要对这些SQL做特殊处理了,比如一个常用的功能,获取当前数据库时间。我们需要在同一个XML文件中写两份,注意两份SQL的 databaseId 是不同的,而不同数据库的 databaseId 是什么,则依赖我们最开始维护的databaseIdProvider 里的value值了

<select id = "getSysDateTime" databaseId="oracle">
	select
			TO_CHAR (sysdate, 'yyyyMMdd') sys_date,
			TO_CHAR (sysdate, 'HH24miss') sys_time
	from dual
</select>

<select id = "getSysDateTime" databaseId="mysql">
	select
			date_format (now(), '%Y%m%d') sys_date,
			date_format (now(), '%H%i%s') sys_time
	from dual
</select>

而一些可以跑在所有平台的SQL,则不需要改造,即databaseId不要填,如

<select id = "getUserInfo" resultType = "UserInfo">
	select user_name, user_age
	from USERINFO
</select>

四、运行原理

做完上述步骤后,我们的项目就能在多种数据库环境运行了,而其内部原理,其实也非常简答

1. 配置载入

在项目启动的时候,MyBatis 需要创建会话工厂,其中就有如下代码,他的意义很明确,就是找到当前连接的数据库,对应的是什么databaseId。并且将这个值保存进配置中。

// SqlSessionFactoryBean
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
	// 省略无关代码
    if (this.databaseIdProvider != null) {
      try {
        targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }
    // 省略无关代码
}

2. SQL选择

我们在 MyBatis+Springboot 启动到SQL执行全流程 中介绍过MyBatis的启动流程,其中就有对xml文件的解析,而我们现在在一个xml中写了多个id相同的SQL,MyBatis会怎么做呢?

// XMLMapperBuilder
  private void buildStatementFromContext(List<XNode> list) {
    // 如果当前环境有DatabaseId,则以这个DatabaseId去加载对应的SQL
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    // 兜底,把某些没有指明DatabaseId的SQL加载进来
    buildStatementFromContext(list, null);
  }
  
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

可以看到对于一个XML文件的解析,会先后以指定databaseId 和无指定databaseId 两种情况去解析

// XMLStatementBuilder
  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 省略无关代码
}

  private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      return requiredDatabaseId.equals(databaseId);
    }
    if (databaseId != null) {
      return false;
    }
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (!this.configuration.hasStatement(id, false)) {
      return true;
    }
    // skip this statement if there is a previous one with a not null databaseId
    MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
    return previous.getDatabaseId() == null;
  }

可以看到,在读取每一段SQL块的时候,会判断SQL上标注的databaseId是否符合当前数据库环境,只有符合的才会被解析。

五、坑点

1. 避免歧义

不难发现,因为兜底逻辑的存在,有时可能会存在歧义,假设我们在mysql环境,我们写下这样的代码,是不是会把两段都解析掉?

<select id = "getSysDateTime" databaseId="mysql">
	select
			date_format (now(), '%Y%m%d') sys_date,
			date_format (now(), '%H%i%s') sys_time
	from dual
</select>

<select id = "getSysDateTime">
	select
			TO_CHAR (sysdate, 'yyyyMMdd') sys_date,
			TO_CHAR (sysdate, 'HH24miss') sys_time
	from dual
</select>

其实是不会的,因为在解析完后我们会把解析的结果存入一个map中,它的key值就是每一块的id,因为这个map是个内部定义的StrictMap,如下

在这里插入图片描述

    @Override
    @SuppressWarnings("unchecked")
    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      return super.put(key, value);
    }

不难发现,一旦有两个id冲突(同一个命名空间下)直接就会报错,所以我们要知道,每一个id实际上只会被存储一次,我们应尽量避免出现歧义的写法

2. 复杂数据库场景

对于大部分场景,按照上面的做法就能解决,但是仍有部分场景是需要特殊处理的,比如同一个数据库的不同版本。

比如说都属于 MySQL 族,但是 MySQL 下又分 5.7 或 8.0,有些语法在低版本上不支持,又或者与Percona 和 Maria-db 等不兼容

在这里插入图片描述
此时就需要使用通用性SQL来写了,一般都是顺着低版本来写,但往往也是性能最差的写法。

总结

本次我们讲解了一套使项目兼容多种数据库的方案,总体而言还是比较简单的,主要还是希望大家能学会原理,从而融会贯通

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

民国漫画杂志《时代漫画》第21期.PDF

时代漫画21.PDF: https://url03.ctfile.com/f/1779803-1248634754-017e2b?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

YOLOv8改进 | 图像去噪篇 | 单阶段盲真实图像去噪网络RIDNet辅助YOLOv8图像去噪(全网独家首发)

一、本文介绍 本文给大家带来的改进机制是单阶段盲真实图像去噪网络RIDNet,RIDNet(Real Image Denoising with Feature Attention)是一个用于真实图像去噪的卷积神经网络(CNN),旨在解决现有去噪方法在处理真实噪声图像时性能受限的问题。通过单阶段结构和特征注意机制,…

SpringBoot入门教程:Word模板生成docx文件

一:处理docx 自己写一个docx文档,然后另存为选择格式 Word XML 文档(*.xml),然后使用thymeleaf语法将实际值使用表达式代替。 二:pom <dependency><groupId>org.springframework.boot</groupId>

基于EBAZ4205矿板的图像处理:07sobel边缘检测算法

基于EBAZ4205矿板的图像处理&#xff1a;07sobel边缘检测算法 项目文件 随后会上传项目全部文件&#xff0c;和之前一样免费下载 先看效果 如上所见&#xff0c;能够提取图像的边缘&#xff0c;这个sobel边缘检测算法的阈值&#xff08;认定是否为边缘的阈值&#xff09;一样…

AWPortrait1.4更新,人物的生成更加趋近真实感,将SD1.5人像的真实感提升到了一个新的高度

AWPortrait1.4更新&#xff0c;人物的生成更加趋近真实感&#xff0c;将SD1.5人像的真实感提升到了一个新的高度 经过5个月&#xff0c;AWPortrait终于迎来了1.4。 本次更新基于1.3训练&#xff0c;使得人物的生成更加趋近真实感&#xff0c;将SD1.5人像的真实感提升到了一个新…

【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt窗口 | 状态栏 | QStatusBar的使用及说明 文章编号&#xff1a;Qt 学…

【学习笔记】Windows GDI绘图(六)图形路径GraphicsPath详解(中)

上一篇【学习笔记】Windows GDI绘图(五)图形路径GraphicsPath详解(上)介绍了GraphicsPath类的构造函数、属性和方法AddArc添加椭圆弧、AddBezier添加贝赛尔曲线、AddClosedCurve添加封闭基数样条曲线、AddCurve添加开放基数样条曲线、基数样条如何转Bezier、AddEllipse添加椭圆…

营销短信XML接口对接发送示例

在现代社会中&#xff0c;通信技术日新月异&#xff0c;其中&#xff0c;短信作为一种快速、简便的通信方式&#xff0c;仍然在日常生活中占据着重要的地位。为了满足各种应用场景的需求&#xff0c;短信接口应运而生&#xff0c;成为了实现高能有效通信的关键。 短信接口是一种…

Linux--07---查看CPU、内存、磁盘

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 常用命令1.查看CPU使用率1.1 top 命令第一行是任务队列信息&#xff1a; top第二行为进程的信息 Tasks第三行为CPU的信息Mem:Swap 1.2 vmstat命令参数详解每个参数的…

(二)可视化面板:Grafana的安装

其他相关文章 &#xff08;一&#xff09;一套开源的系统监控报警框架&#xff1a;Prometheus安装 什么是grafana Grafana是一个面板(Dashboard),有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器。支持Graphite、zabbix、InfluxDB、Prometheus和OpenTSDB作为…

【ACM出版,下周召开】2024机器智能与数字化应用国际会议(MIDA2024)

会议日期&#xff1a;2024年5月30-31日 会议地点&#xff1a;中国-宁波 会议官网&#xff1a;https://www.iaast.cn/meet/home/Bx120sos 【大会主席】 Ljiljana Trajkovic 加拿大西蒙菲莎大学教授。 【论文出版与检索】 EI会议论文集-ACM出版 大会即日起围绕主题征集会…

K8s资源限制和三种探针

一 默写总结 1 pod 的组成 ① Pod 中有几种容器 init 初始化 &#xff0c;阻塞主容器运行&#xff0c;初始化后方可运行主容器 pause 基础容器&#xff1a; 提供network 的 namespace 和 共享存储 业务容器&#xff1a; 跑Pod 主应用 &#xff08;POD中跑什么&#xff1a;微…

22-LINUX--多线程and多进程TCP连接

一.TCP连接基础知识 1.套接字 所谓套接字(Socket)&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲&#xff0c;套接字上联应用进程…

学习前端第四十三天(样式和类、元素大小和滚动、Window大小和滚动、坐标)

一、样式和类 1、className&#xff0c;classList elem.className 对应于 "class" 特性 <div id"box" class"a b" style"height: 20px;">box</div><script>const box document.getElementById("box");…

uniapp gaid-item组件增加角标的方法以及最新版本已删除角标参数的注意事项

在uniapp项目的开发中&#xff0c;列表组件gaid-item大家应该都是经常用的&#xff0c;其实组件上的角标用法也是很方便的&#xff0c;但是不熟悉的新手朋友&#xff0c;特别是用最新版的朋友可能都找不到角标方法的使用地方&#xff1a; 1.首先官方说明&#xff0c;在最新版本…

【Unity】免费的高亮插件——QuickOutline

除了常见的HighLightSystem来实现的高亮功能&#xff0c;其实还有很多的方法实现物体的高亮。 在 Unity资源商店 搜索OutLine&#xff0c;就会有很多免费好用的高亮插件。 下面介绍一下 QuickOutline这个插件&#xff0c;在 Unity资源商店 搜索到后&#xff0c;点击进去就可以…

【QGIS入门实战精品教程】13.1:导入带地理标签的航测照片

文章目录 一、数据准备二、导入带地理标签的航测照片三、导出点位shp四、生成航线一、数据准备 本实验数据位于13.1:导入带地理标签的航测照片.rar中,如下: 查看照片及相机参数信息,航测照片都带有相机参数、部分POS及地理坐标信息,如下所示: 二、导入带地理标签的航测照…

Java - AbstractQueuedSynchronizer

AQS简介 AQS全称AbstractQueuedSynchronizer&#xff0c;抽象队列同步器&#xff0c;是一个实现同步组件的基础框架。AQS使用一个int类型的成员变量state维护同步状态&#xff0c;通过内置的同步队列&#xff08;CLH锁、FIFO&#xff09;完成线程的排队工作&#xff0c;底层主…

AI大模型如何赋能智能座舱

AI 大模型如何赋能智能座舱 从上海车展上&#xff0c;我们看到由于智能座舱配置性价比较高&#xff0c;已经成为车企的核心竞争点之一&#xff0c;随着座舱硬件规模化装车&#xff0c;蔚小理、岚图、极狐等新势力开始注重座舱多模态交互&#xff0c;通过集成语音/手势/触控打造…

Vue速成学习笔记

这两天速成了一下Vue&#xff0c;在这里记录一下相关的笔记&#xff0c;之后有时间详细学Vue的时候再来回顾一下&#xff01; 一、Vue理解 1、Vue的核心特征&#xff1a;双向绑定。 在网页中&#xff0c;存在视图和数据。在Vue之前&#xff0c;需要使用JavaScript编写复杂的逻…