MyBatis?如何使項(xiàng)目兼容多種數(shù)據(jù)庫(kù)的解決方案
一、啟用數(shù)據(jù)庫(kù)識(shí)別
1. 調(diào)查數(shù)據(jù)庫(kù)產(chǎn)品名
要想做兼容多種數(shù)據(jù)庫(kù),那毫無(wú)疑問(wèn),我們首先得明確我們要兼容哪些數(shù)據(jù)庫(kù),他們的數(shù)據(jù)庫(kù)產(chǎn)品名稱是什么。得益于SPI設(shè)計(jì),java語(yǔ)言制定了一個(gè)DatabaseMetaData接口,要求各個(gè)數(shù)據(jù)庫(kù)的驅(qū)動(dòng)都必須提供自己的產(chǎn)品名。因此我們?nèi)绻胍嫒菽硵?shù)據(jù)庫(kù),只要在對(duì)應(yīng)的驅(qū)動(dòng)包中找到其對(duì)DatabaseMetaData的實(shí)現(xiàn)即可。
比如Mysql的驅(qū)動(dòng)包 mysql-connector-java 下的 DatabaseMetaData

Oracle 的驅(qū)動(dòng)包 com.oracle.ojdbc6 下的 OracleDatabaseMetaData

2. 啟用databaseId
既然各個(gè)驅(qū)動(dòng)都提供了產(chǎn)品名,那么接下來(lái)就是讓項(xiàng)目在啟動(dòng)中能夠識(shí)別這些數(shù)據(jù)庫(kù),并賦予以不同數(shù)據(jù)庫(kù)不同的id。MyBatis 其實(shí)有這項(xiàng)功能,但是這個(gè)功能默認(rèn)沒(méi)有被啟用,若要啟用我們首先得建立一個(gè)配置,即databaseIdProvider,可以在配置類里面加上這個(gè)Bean來(lái)實(shí)現(xiàn)
@Configuration //配置類
public class MyBatisConfig {
@Bean
public DatabaseIdProvider getDatabaseIdProvider() {
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
// Key值(即產(chǎn)品名)來(lái)源于數(shù)據(jù)庫(kù),需要提前查清楚 ,
// 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;
}
}完成了上述配置后,我們的項(xiàng)目就能主動(dòng)去識(shí)別數(shù)據(jù)庫(kù)類型了。
二、SQL語(yǔ)法鑒別
對(duì)于大部分SQL,因?yàn)橛蠸QL規(guī)范的限制,它們通常是通用的,一段SQL可以在不同的數(shù)據(jù)庫(kù)上跑。但是對(duì)于部分復(fù)雜SQL,就得針對(duì)不同數(shù)據(jù)庫(kù),來(lái)寫不同的SQL了,我們以Mysql 、 Oracle 為例,看一些常見功能的語(yǔ)法差異
1. 分頁(yè)查詢
MySQL中使用LIMIT關(guān)鍵字來(lái)實(shí)現(xiàn)分頁(yè)查詢,例如:
SELECT * FROM table_name LIMIT offset, count;
而Oracle中使用ROWNUM關(guān)鍵字來(lái)實(shí)現(xiàn)分頁(yè)查詢,例如:
SELECT *
FROM (SELECT t.*, ROWNUM AS rn
FROM table_name t
WHERE ROWNUM <= offset + count)
WHERE rn > offset;2. 獲取當(dāng)前時(shí)間
MySQL中可以使用NOW()函數(shù)來(lái)獲取當(dāng)前時(shí)間,例如:
SELECT NOW();
而Oracle中可以使用SYSDATE關(guān)鍵字來(lái)獲取當(dāng)前時(shí)間,例如:
SELECT SYSDATE FROM DUAL;
3. 獲取自增主鍵的值
MySQL中可以使用LAST_INSERT_ID()函數(shù)來(lái)獲取最后插入行的自動(dòng)生成的主鍵值,例如:
INSERT INTO table_name (column1, column2) VALUES(value1, value2); SELECT LAST_INSERT_ID();
而Oracle中可以使用SEQUENCE和CURRVAL來(lái)獲取自增主鍵的值,例如:
INSERT INTO table_name (column1, column2) VALUES(seq.nextval, value2); SELECT seq.currval from dual;
4. 轉(zhuǎn)換數(shù)據(jù)類型
MySQL 使用 CAST() 或 CONVERT() 函數(shù)轉(zhuǎn)換數(shù)據(jù)類型,例如:
SELECT CAST('123' AS SIGNED) AS converted_value;
-- 或者
SELECT CONVERT('123', SIGNED) AS converted_value;而Oracle使用 TO_NUMBER(), TO_CHAR(), TO_DATE() 等函數(shù)進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換,例如:
INSERT INTO table_name (column1, column2) VALUES(seq.nextval, value2); SELECT seq.currval from dual;
5. 字符串拼接
MySQL中可以使用CONCAT()函數(shù)來(lái)進(jìn)行字符串拼接,例如:
SELECT CONCAT(column1, column2) FROM table_name;
而Oracle中可以使用||運(yùn)算符來(lái)進(jìn)行字符串拼接,例如:
SELECT column1 || column2 FROM table_name;
6. 字符串截取
MySQL 使用 SUBSTRING() 函數(shù),例如:
SELECT SUBSTRING('Hello World', 1, 5) AS substring_result;而Oracle 使用 SUBSTR() 函數(shù),例如:
SELECT SUBSTR('Hello World', 1, 5) AS substring_result FROM DUAL;7. 判空函數(shù)
MySQL中可以使用IFNULL()函數(shù)來(lái)進(jìn)行字符串拼接,例如:
SELECT IFNULL(column1, "1") FROM table_name;
而Oracle中可以使用NVL()來(lái)進(jìn)行字符串拼接,例如:
SELECT NVL(column1, "1") FROM table_name;
8. 正則表達(dá)式
MySQL 使用 REGEXP 或 RLIKE 進(jìn)行正則表達(dá)式匹配,例如:
SELECT 'Hello World' REGEXP '^Hello' AS is_matched;
而Oracle 使用 REGEXP_LIKE, REGEXP_INSTR, REGEXP_SUBSTR, 和 REGEXP_REPLACE 等函數(shù),例如:
SELECT CASE WHEN REGEXP_LIKE('Hello World', '^Hello') THEN 'Matched' ELSE 'Not Matched' END AS is_matched FROM DUAL;9. 窗口函數(shù)
MySQL 低版本不支持窗口函數(shù),可以使用自連接模擬窗口函數(shù),例如:
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高版本則可以 使用 窗口函數(shù),例如:
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兼容處理
如果我們的項(xiàng)目有SQL語(yǔ)法不兼容的情況,如上面那些場(chǎng)景,那么我們就需要對(duì)這些SQL做特殊處理了,比如一個(gè)常用的功能,獲取當(dāng)前數(shù)據(jù)庫(kù)時(shí)間。我們需要在同一個(gè)XML文件中寫兩份,注意兩份SQL的 databaseId 是不同的,而不同數(shù)據(jù)庫(kù)的 databaseId 是什么,則依賴我們最開始維護(hù)的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>
而一些可以跑在所有平臺(tái)的SQL,則不需要改造,即databaseId不要填,如
<select id = "getUserInfo" resultType = "UserInfo"> select user_name, user_age from USERINFO </select>
四、運(yùn)行原理
做完上述步驟后,我們的項(xiàng)目就能在多種數(shù)據(jù)庫(kù)環(huán)境運(yùn)行了,而其內(nèi)部原理,其實(shí)也非常簡(jiǎn)答
1. 配置載入
在項(xiàng)目啟動(dòng)的時(shí)候,MyBatis 需要?jiǎng)?chuàng)建會(huì)話工廠,其中就有如下代碼,他的意義很明確,就是找到當(dāng)前連接的數(shù)據(jù)庫(kù),對(duì)應(yīng)的是什么databaseId。并且將這個(gè)值保存進(jìn)配置中。
// SqlSessionFactoryBean
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
// 省略無(wú)關(guān)代碼
if (this.databaseIdProvider != null) {
try {
targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException("Failed getting a databaseId", e);
}
}
// 省略無(wú)關(guān)代碼
}2. SQL選擇
我們?cè)?Mybatis之動(dòng)態(tài)SQL使用小結(jié)(全網(wǎng)最新) 中介紹過(guò)MyBatis的啟動(dòng)流程,其中就有對(duì)xml文件的解析,而我們現(xiàn)在在一個(gè)xml中寫了多個(gè)id相同的SQL,MyBatis會(huì)怎么做呢?
// XMLMapperBuilder
private void buildStatementFromContext(List<XNode> list) {
// 如果當(dāng)前環(huán)境有DatabaseId,則以這個(gè)DatabaseId去加載對(duì)應(yīng)的SQL
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 兜底,把某些沒(méi)有指明DatabaseId的SQL加載進(jìn)來(lái)
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);
}
}
}可以看到對(duì)于一個(gè)XML文件的解析,會(huì)先后以指定databaseId 和無(wú)指定databaseId 兩種情況去解析
// XMLStatementBuilder
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 省略無(wú)關(guān)代碼
}
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塊的時(shí)候,會(huì)判斷SQL上標(biāo)注的databaseId是否符合當(dāng)前數(shù)據(jù)庫(kù)環(huán)境,只有符合的才會(huì)被解析。
五、坑點(diǎn)
1. 避免歧義
不難發(fā)現(xiàn),因?yàn)槎档走壿嫷拇嬖?,有時(shí)可能會(huì)存在歧義,假設(shè)我們?cè)趍ysql環(huán)境,我們寫下這樣的代碼,是不是會(huì)把兩段都解析掉?
<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>
其實(shí)是不會(huì)的,因?yàn)樵诮馕鐾旰笪覀儠?huì)把解析的結(jié)果存入一個(gè)map中,它的key值就是每一塊的id,因?yàn)檫@個(gè)map是個(gè)內(nèi)部定義的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);
}不難發(fā)現(xiàn),一旦有兩個(gè)id沖突(同一個(gè)命名空間下)直接就會(huì)報(bào)錯(cuò),所以我們要知道,每一個(gè)id實(shí)際上只會(huì)被存儲(chǔ)一次,我們應(yīng)盡量避免出現(xiàn)歧義的寫法
2. 復(fù)雜數(shù)據(jù)庫(kù)場(chǎng)景
對(duì)于大部分場(chǎng)景,按照上面的做法就能解決,但是仍有部分場(chǎng)景是需要特殊處理的,比如同一個(gè)數(shù)據(jù)庫(kù)的不同版本。
比如說(shuō)都屬于 MySQL 族,但是 MySQL 下又分 5.7 或 8.0,有些語(yǔ)法在低版本上不支持,又或者與Percona 和 Maria-db 等不兼容

此時(shí)就需要使用通用性SQL來(lái)寫了,一般都是順著低版本來(lái)寫,但往往也是性能最差的寫法。
總結(jié)
本次我們講解了一套使項(xiàng)目兼容多種數(shù)據(jù)庫(kù)的方案,總體而言還是比較簡(jiǎn)單的,主要還是希望大家能學(xué)會(huì)原理,從而融會(huì)貫通
到此這篇關(guān)于MyBatis 使項(xiàng)目兼容多種數(shù)據(jù)庫(kù)的解決方案的文章就介紹到這了,更多相關(guān)MyBatis項(xiàng)目兼容多種數(shù)據(jù)庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java動(dòng)態(tài)規(guī)劃算法——硬幣找零問(wèn)題實(shí)例分析
這篇文章主要介紹了java動(dòng)態(tài)規(guī)劃算法——硬幣找零問(wèn)題,結(jié)合實(shí)例形式分析了java動(dòng)態(tài)規(guī)劃算法——硬幣找零問(wèn)題相關(guān)原理、實(shí)現(xiàn)方法與操作注意事項(xiàng),需要的朋友可以參考下2020-05-05
Java后臺(tái)接口開發(fā)初步實(shí)戰(zhàn)教程
下面小編就為大家分享一篇 Java后臺(tái)接口開發(fā)初步實(shí)戰(zhàn)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
關(guān)于springcloud集成nacos遇到的問(wèn)題
這篇文章主要介紹了關(guān)于springcloud集成nacos遇到的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
java 將byte中的有效長(zhǎng)度轉(zhuǎn)換為String的實(shí)例代碼
下面小編就為大家?guī)?lái)一篇java 將byte中的有效長(zhǎng)度轉(zhuǎn)換為String的實(shí)例代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11
mybatisPlus填坑之邏輯刪除的實(shí)現(xiàn)
本文主要介紹了mybatisPlus填坑之邏輯刪除的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
劍指Offer之Java算法習(xí)題精講數(shù)組與二叉樹
跟著思路走,之后從簡(jiǎn)單題入手,反復(fù)去看,做過(guò)之后可能會(huì)忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會(huì)發(fā)現(xiàn)質(zhì)的變化2022-03-03
SpringCloud Stream消息驅(qū)動(dòng)實(shí)例詳解
這篇文章主要介紹了SpringCloud Stream消息驅(qū)動(dòng)的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
概述java虛擬機(jī)中類的加載器及類加載過(guò)程
這篇文章主要介紹了概述java虛擬機(jī)中類的加載器及類加載過(guò)程,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04

