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

系列文章目录

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来写了,一般都是顺着低版本来写,但往往也是性能最差的写法。

总结

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

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

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

相关文章

SVN创建项目分支

目录 背景调整目录结构常规目录结构当前现状目标 调整SVN目录调整目录结构创建项目分支 效果展示 背景 当前自己本地做项目的时候发现对SVN创建项目不规范&#xff0c;没有什么目录结构&#xff0c;趁着创建目录分支的契机&#xff0c;顺便调整下SVN服务器上的目录结构 调整目…

Day36 代码随想录打卡|二叉树篇---翻转二叉树

题目&#xff08;leecode T226&#xff09;&#xff1a; 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 方法&#xff1a; 迭代法 翻转二叉树&#xff0c;即从根节点开始&#xff0c;一一交换每个节点的左右孩子节点&#xff0c;然后…

【Arthas】阿里的线上jvm监控诊断工具的基本使用

关于对运行中的项目做java监测的需求下&#xff0c;Arthas则是一个很好的解决方案。 我们可以用来 1.监控cpu 现成、内存、堆栈 2.排查cpu飚高 造成原因 3.接口没反应 是否死锁 4.接口慢优化 5.代码未按预期执行 是分支不对 还是没提交&#xff1f; 6.线上低级错误 能不能不重启…

伦敦金交易商压箱底的交易技法 居然是……

很多伦敦金交易商&#xff0c;也就是我们常说的伦敦金交易平台&#xff0c;或者伦敦金交易服务提供商&#xff0c;他们会和一些资深的市场分析师合作。另外&#xff0c;一般在这些伦敦金交易商内部&#xff0c;也会有一批高手&#xff0c;他们一边在交易&#xff0c;一边在平台…

【设计模式深度剖析】【3】【创建型】【抽象工厂模式】| 要和【工厂方法模式】对比加深理解

&#x1f448;️上一篇:工厂方法模式 | 下一篇:建造者模式&#x1f449;️ 目录 抽象工厂模式前言概览定义英文原话直译什么意思呢&#xff1f;&#xff08;以运动型车族工厂&#xff0c;生产汽车、摩托产品为例&#xff09; 类图4个角色抽象工厂&#xff08;Abstract Fac…

起底震网病毒的来龙去脉

2010年&#xff0c;震网病毒被发现&#xff0c;引起世界哗然&#xff0c;在后续的10年间&#xff0c;陆陆续续有更多关于该病毒的背景和细节曝光。今年&#xff0c;《以色列时报》和《荷兰日报》又披露了关于此事件的更多信息&#xff0c;基于这些信息&#xff0c;我们重新梳理…

使用 Docker 部署 Jenkins 并设置初始管理员密码

使用 Docker 部署 Jenkins 并设置初始管理员密码 每一次开始&#xff0c;我都特别的认真与胆怯&#xff0c;是因为我期待结局&#xff0c;也能够不会那么粗糙&#xff0c;不会让我失望&#xff0c;所以&#xff0c;就多了些思考&#xff0c;多了些拘束&#xff0c;所以&#xf…

软件测试:功能测试-接口测试-自动化测试-性能测试-验收测试

软件测试的主要流程 一、测试主要的四个阶段 1.测试计划设计阶段&#xff1a;产品立项之后&#xff0c;进行需求分析&#xff0c;需求评审&#xff0c;业务需求评级&#xff0c;绘制业务流程图。确定测试负责人&#xff0c;开始制定测试计划&#xff1b; 2.测试准备阶段&…

不小心丢失mfc140u.dll文件怎么办?mfc140u.dll丢失的解决办法

当您发现mfc140u.dll文件不见了或者受损&#xff0c;别担心&#xff0c;我们可以一起解决这个问题&#xff01;首先&#xff0c;您可能会注意到一个小提示&#xff0c;当您尝试打开某些程序时&#xff0c;屏幕上会跳出一个消息说“找不到mfc140u.dll”或者“mfc140u.dll文件缺失…

心识宇宙 x TapData:如何加速落地实时数仓,助力 AI 企业智慧决策

使用 TapData&#xff0c;化繁为简&#xff0c;摆脱手动搭建、维护数据管道的诸多烦扰&#xff0c;轻量代替 OGG、DSG 等同步工具&#xff0c;「CDC 流处理 数据集成」组合拳&#xff0c;加速仓内数据流转&#xff0c;帮助企业将真正具有业务价值的数据作用到实处&#xff0c…

Python的selenium爬取

1.selenium 1.1.前言 使用python的requests模块还是存在很大的局限性&#xff0c;例如&#xff1a;只发一次请求&#xff1b;针对ajax动态加载的网页则无法获取数据等等问题。特此&#xff0c;本章节将通过selenium模拟浏览器来完成更高级的爬虫抓取任务。 1.2.什么是seleniu…

学习单向链表带哨兵demo

一、定义 在计算机科学中&#xff0c;链表是数据元素的线性集合&#xff0c;其每个元素都指向下一个元素&#xff0c;元素存储上并不连续。 1.可以分三类为 单向链表&#xff0c;每个元素只知道其下一个元素是谁 双向链表&#xff0c;每个元素知道其上一个元素和下一个元素 …

抖音小店不能做无货源了吗?当然不是,而是玩法更先进了!

大家好&#xff0c;我是电商糖果 自从2023年抖音小店开始严查无货源&#xff0c;不少商家被平台处罚&#xff0c;被逼无奈退出抖音小店。 网上关于抖音小店不能做无货源的声音越来越多。 可是一年多过去&#xff0c;大家渐渐的发现&#xff0c;平台内还是有很多无货源商家&a…

Sping源码(八)—registerBeanPostProcessors

序言 之前我们用大量的篇幅介绍过invokeBeanFactoryPostProcessors()方法的执行流程。 而invokeBeanFactoryPostProcessors的主要逻辑就是遍历执行实现了BeanDefinitionRegistryPostProcesso类(主要是针对BeanDefinition的操作)和BeanFactoryPostProcessor(主要针对BeanFacrot…

spring-boot集成slf4j(二)logback配置详解

一、configuration 根节点&#xff1a;configuration&#xff0c;作为顶级标签&#xff0c; 可以用来配置一些lockback的全局属性&#xff0c;常见的属性如下&#xff1a; &#xff08;1&#xff09;scan“true” &#xff1a;scan是否开启自动扫描&#xff0c;监控配置文件更…

XShell-连接-Centos 7

XShell 连接Centos 7 一.准备 安装XShell XShell下载地址&#xff1a; 在虚拟机上安装Centos 7&#xff0c;具体操作自行学习 二.Centos 7的准备 1.网络适配器修改为NAT 2.获取IP 输入命令&#xff1a; ip addr我的Centos 7对外IP为192.168.174.129 三.XShell连接Cento…

Big Demo Day第十三期活动即将启幕,Web3创新项目精彩纷呈,PEPE大奖等你抽取

5月28号在香港数码港 Big Demo Day第十三期 活动即将拉开帷幕&#xff0c;活动将汇集众多Web3领域的创新项目&#xff0c;为参会者带来一场科技与智慧交融的盛宴。在这里&#xff0c;你不仅能深入了解区块链、AI等前沿技术的最新应用&#xff0c;还能有机会赢取丰厚的PEPE大奖。…

使用maven-helper插件解决jar包冲突

发现问题 maven-helper分析问题 如上所述&#xff0c;问题就是依赖版本冲突了&#xff0c;出现版本冲突的原因是因为由于Maven具有依赖传递性&#xff0c;所以当你引入一个依赖类的同时&#xff0c;其身后的依赖类也一起如过江之鲫纷至沓来了。 举个例子&#xff1a;   A依赖…

保护元件-详实的熔断器(保险丝)知识

目录&#xff1a; 一、汽车保险丝设计与选型 1、概述 2、构造及工作原理 1&#xff09;构造 2&#xff09;工作原理 3&#xff09;保险丝熔断及分断时间 4&#xff09;时间/电流特性曲线 5&#xff09;环境温度修正系数 3、熔化热能值I2t★ 4、三种电流模型 1&a…

废物回收机构|基于SprinBoot+vue的地方废物回收机构管理系统(源码+数据库+文档)

地方废物回收机构管理系统 目录 基于SprinBootvue的地方废物回收机构管理系统 一、前言 二、系统设计 三、系统功能设计 1管理员功能模块 2 员工功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍…