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 {@Beanpublic 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">selectTO_CHAR (sysdate, 'yyyyMMdd') sys_date,TO_CHAR (sysdate, 'HH24miss') sys_timefrom dual
</select><select id = "getSysDateTime" databaseId="mysql">selectdate_format (now(), '%Y%m%d') sys_date,date_format (now(), '%H%i%s') sys_timefrom dual
</select>

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

<select id = "getUserInfo" resultType = "UserInfo">select user_name, user_agefrom 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会怎么做呢?

// XMLMapperBuilderprivate void buildStatementFromContext(List<XNode> list) {// 如果当前环境有DatabaseId,则以这个DatabaseId去加载对应的SQLif (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 两种情况去解析

// XMLStatementBuilderpublic 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 databaseIdMappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2return previous.getDatabaseId() == null;}

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

五、坑点

1. 避免歧义

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

<select id = "getSysDateTime" databaseId="mysql">selectdate_format (now(), '%Y%m%d') sys_date,date_format (now(), '%H%i%s') sys_timefrom dual
</select><select id = "getSysDateTime">selectTO_CHAR (sysdate, 'yyyyMMdd') sys_date,TO_CHAR (sysdate, 'HH24miss') sys_timefrom 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.mzph.cn/pingmian/14855.shtml

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

相关文章

【问题处理】maven一直提示artemis-http-client-1.1.8.jar报错(2024-05-25)

项目使用了视频监控&#xff0c;里面涉及到海康威视的视频监控。 问题&#xff1a; pom在导入maven时&#xff0c;报错“Could not find artifact com.artemis:http-client:jar:1.1.8 ” 原因&#xff1a; 根据平台提供的maven地址&#xff0c;填写进pom文件中&#xff0c;编…

汇编-16位汇编环境搭建

16位汇编环境 在学习16位汇编时&#xff0c;我选择的环境是在VMware中安装Windows XP虚拟机来学习&#xff1b;因为Windows XP提供了兼容的DOS环境&#xff0c;可以直接运行和调试16位汇编程序&#xff1b;在win10&#xff0c;win11环境中原生不支持直接运行 16 位程序&#x…

房地产画册制作成手机在线翻页效果

​随着科技的飞速发展&#xff0c;移动互联网已经深入到人们的日常生活中。在这个数字化的时代&#xff0c;房地产行业也紧跟潮流&#xff0c;将画册制作成手机在线翻页效果&#xff0c;以满足消费者的阅读习惯。 房地产画册制作成手机在线翻页效果&#xff0c;不仅能够满足消费…

mac清理软件推荐免费 mac清理系统数据怎么清理 cleanmymac和腾讯柠檬哪个好

macbook是苹果公司的一款高性能的笔记本电脑&#xff0c;受到了很多用户的喜爱。但是&#xff0c;随着使用时间的增长&#xff0c;macbook的系统也会积累一些垃圾文件&#xff0c;影响其运行速度和空间。那么&#xff0c;macbook系统清理软件推荐有哪些呢&#xff1f;macbook用…

263 基于matlab得到的频分复用(FDM,Frequency Division Multiplexing)实现

基于matlab得到的频分复用(FDM&#xff0c;Frequency Division Multiplexing)实现&#xff0c;仿真时录入三路声音信号进行处理&#xff0c;将用于传输信道的总带宽划分成三个子频带&#xff0c;经过复用以后再将录入的声音信号恢复出来。程序已调通&#xff0c;可直接运行。 2…

Docker | 基础指令

环境&#xff1a;centos8 参考&#xff1a; 安装 Docker | Docker 从入门到实践https://vuepress.mirror.docker-practice.com/install/ 安装Docker 卸载旧版本&#xff0c;安装依赖包&#xff0c;添加yum软件源&#xff0c;更新 yum 软件源缓存&#xff0c;安装 docker-ce…

AI助力农田作物智能化激光除草,基于轻量级YOLOv8n开发构建农田作物场景下常见20种杂草检测识别分析系统

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术在各个领域的应用愈发广泛&#xff0c;其中农业领域也不例外。近年来&#xff0c;AI助力农田作物场景下智能激光除草的技术成为了农业领域的一大亮点&#xff0c;它代表着农业智能化、自动化的新趋势。智…

基于地理坐标的高阶几何编辑工具算法(1)——目录

文章目录 背景目录效果相交面裁剪相离面吸附线分割面合并相交面合并相离面矩形绘制整形面 背景 在实际的地图编辑平台中&#xff0c;有一些场景是需要对几何面做修形操作&#xff0c;低效的做法是通过新增形点拖拽来实现。为了提高面几何的编辑效率&#xff0c;需要提供一些便…

Java开发大厂面试第23讲:说一下 JVM 的内存布局和运行原理?

JVM&#xff08;Java Virtual Machine&#xff0c;Java 虚拟机&#xff09;顾名思义就是用来执行 Java 程序的“虚拟主机”&#xff0c;实际的工作是将编译的 class 代码&#xff08;字节码&#xff09;翻译成底层操作系统可以运行的机器码并且进行调用执行&#xff0c;这也是 …

虹科案例丨VLAN不再难懂:一台转换器+交换机轻松解锁VLAN配置

来源&#xff1a;虹科汽车电子 虹科案例丨VLAN不再难懂&#xff1a;一台转换器交换机轻松解锁VLAN配置 原文链接&#xff1a;https://mp.weixin.qq.com/s/5cFLWniozlppQGD7RcvgxA 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; #VLAN #转换器 #交换机 导读 还在为…

【Numpy】深入解析numpy中的ravel方法

NumPy中的ravel方法&#xff1a;一维化数组的艺术 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#x1f393; 博主简…

实现复杂树结构返回(不含子树), 并且结点间建立关联

&#x1f4a1; 一句话结&#xff1a; 实现传感器和深度及采集的数值动态对应&#xff0c;将不规则的数据转变成固定列头的一行行数据。 &#x1f511; 关键信息点&#xff1a; 通过传感器编号和深度将传感器对应的数值与时间建立关联。使用SpringBootMyBatis框架实现动态查询…

RSA算法加解密

RSA算法的加密过程如下&#xff1a; 选择两个大素数①p和q&#xff0c;计算它们的乘积np*q计算欧拉函数φ(n)(p-1)*(q-1)选择一个整数e&#xff0c;满足1<e<φ(n)&#xff0c;且e与φ(n)互质计算e关于φ(n)的模逆元d&#xff0c;即满足e*d mod φ(n) 1的整数d②公钥为(…

【设计模式深度剖析】【2】【结构型】【装饰器模式】| 以去咖啡馆买咖啡为例 | 以穿衣服出门类比

&#x1f448;️上一篇:代理模式 目 录 装饰器模式定义英文原话直译如何理解呢&#xff1f;4个角色类图1. 抽象构件&#xff08;Component&#xff09;角色2. 具体构件&#xff08;Concrete Component&#xff09;角色3. 装饰&#xff08;Decorator&#xff09;角色4. 具体装饰…

2024电工杯数学建模A题Matlab代码+结果表数据教学

2024电工杯A题保姆级分析完整思路代码数据教学 A题题目&#xff1a;园区微电网风光储协调优化配置 以下仅展示部分&#xff0c;完整版看文末的文章 %A_1_1_A % 清除工作区 clear;clc;close all;warning off; %读取参数%正常读取 % P_LOADxlsread(附件1&#xff1a;各园区典…

前端 CSS 经典:SVG 描边动画

1. 原理 使用 css 中的 stroke 属性&#xff0c;用来描述描边的样式&#xff0c;其中重要的属性 stroke-dasharray、stroke-dashoffset。理解了这两个属性的原理&#xff0c;才能理解描边动画实现的原理。 stroke-dasharray&#xff1a;将描边线变成虚线、其中实线和虚线部分…

小程序丨公告栏功能,自动弹出提醒

发布查询时&#xff0c;您是否遇到这样的困扰&#xff1a; 1、查询发布时间未到&#xff0c;学生进入查询主页后发现未发布任何查询&#xff0c;不断咨询原因。 2、有些重要事项需要进入查询主页就进行强提醒&#xff0c;确保人人可见&#xff0c;用户需要反馈“我知道了”才…

【openlayers系统学习】3.4波段数学计算(计算NDVI)

四、波段数学计算&#xff08;计算NDVI&#xff09; 我们已经看到了如何使用 ol/source/GeoTIFF​ 源代码来渲染真彩色和假彩色合成。我们通过将缩放的反射率值直接渲染到红色、绿色或蓝色显示通道中的一个来实现这一点。还可以对来自GeoTIFF&#xff08;或其他数据瓦片源&…

Day48 Javascript详解

Day48 Javascript详解 文章目录 Day48 Javascript详解一、什么是javascript二、javascript特点三、 Javascript的历史四、Javascript vs Java五、JS的基本数据类型六、JS基本数据类型的特殊点七、数组 一、什么是javascript JavaScript是一种高级的、解释型的编程语言&#xf…

cmake编译redis6.0源码总结

1配置clion使用cygwin模拟linux环境&#xff0c;先下载cygwin后配置 2导入源码&#xff0c;配置cmake文件 由于redis是基于Linux上的Makefile&#xff0c;所以Windows上需要配置CMakeLists.txt使用cmake工具编译运行。github上已经有人尝试编写CMakeLists.txt文件&#xff0c…