可以实现 SQL 移植的技术
不同类型数据库的函数语法或多或少有些差异,要想让功能相同的SQL查询语句在不同类型的数据库中都能被顺利执行,就要把各数据库都有的那些差异化函数语法进行翻译,即SQL移植。本文将探讨几种SQL移植的技术并加以对比。
ORM技术
使用ORM技术能够将程序员书写的查询转换成不同数据库的SQL,相当于有一定的移植能力。但 ORM 技术只适合应对 OLTP 场景下的简单 SQL,难以实现 OLAP 场景下较复杂 SQL 的移植。
Hibernate和Mybatis是两种较常见的ORM技术。Mybatis被称为半自动ORM,很主要的一个原因是需要程序员自己编写原生态SQL,也就几乎没有可移植性,即使借助Mybatis-plus、Mybatis-ext这样的第三方增强扩展包来改善输入方式,其可移植性也远比不上Hibernate;Hibernate则可以使用HQL根据配置文件中的数据库方言自动生成对应的SQL,对于一些简单的情况(Hibernate中直接支持),具有一定的可移植性,例如:
HQL:
SELECT client, YEAR(orderDate), sum(amount), count(orderId) FROM OrdersEntity GROUP BY client, YEAR(orderDate) HAVING sum(amount)>2000
MySQL:
SELECT ordersenti0_.Client AS col_0_0_, YEAR(ordersenti0_.OrderDate) AS col_1_0_, sum(ordersenti0_.Amount) AS col_2_0_, count(ordersenti0_.OrderID) AS col_3_0_ FROM orders ordersenti0_ GROUP BY ordersenti0_.Client, YEAR(ordersenti0_.OrderDate) HAVING sum(ordersenti0_.Amount)>2000
Oracle:
SELECT ordersenti0_.Client AS col_0_0_, EXTRACT(YEAR FROM ordersenti0_.OrderDate) AS col_1_0_, sum(ordersenti0_.Amount) AS col_2_0_, count(ordersenti0_.OrderID) AS col_3_0_ FROM system.orders ordersenti0_ GROUP BY ordersenti0_.Client, EXTRACT(YEAR FROM ordersenti0_.OrderDate) HAVING sum(ordersenti0_.Amount)>2000
HQL生成的SQL中,对应MySQL时用了YEAR(d)函数,对应Oracle时用了EXTRACT(YEAR FROM d)函数。可以实现SQL的移植效果。
但是更复杂一些的情况,Hibernate也会失去可移植性。
例如使用HQL本身不直接支持的函数时,需要先根据当前使用的数据库注册自定义函数后再用HQL生成SQL,以MySQL为例:
注册自定义函数关键代码:
registerFunction("udf_dateadd", new SQLFunctionTemplate(DateType.INSTANCE,"date_add(?1,INTERVAL ?2 DAY)"));
HQL:
SELECT udf_dateadd (orderDate,3) FROM OrdersEntity
MySQL:
SELECT date_add(ordersenti0_.OrderDate,INTERVAL 3 DAY) AS col_0_0_ FROM orders ordersenti0_
但Oracle没有date_add函数,如果数据库变为Oracle,又需要再根据Oracle的函数语法,另外再注册新的自定义函数来使用,这就无法自动移植了。
还有些HQL无法描述的运算,例如不支持FROM中的子查询:
SELECT orderId, m FROM ( SELECT orderId, MONTH(orderDate) m FROM OrdersEntity) t1
这种情况通常会使用SQLQuery接口来解决,但这时执行的是原生SQL了,也失去移植性。
SQL转换工具
借助某些工具移植SQL,这种办法的优点是,可以直接将现有的源SQL(如Oracle SQL)翻译成目标SQL(如MySQL SQL),即使复杂情况下,如嵌套子查询,也可以完成转换。
例如在https://www.sqlines.com/online中:
源SQL的数据库类型选择MySQL,需要被翻译的SQL为:
SELECT O_YEAR , SUM(CASE WHEN NATION = 'CHINA' THEN VOLUME ELSE 0 END) / SUM(VOLUME) ASMKT_SHARE FROM ( SELECT YEAR (O_ORDERDATE) AS O_YEAR, L_EXTENDEDPRICE * (1 - L_DISCOUNT) AS VOLUME, N2.N_NAME AS NATION FROM PART, SUPPLIER, …
目标SQL的数据库类型选择Oracle,点击“转换”按钮,即可将源SQL转为目标数据库可以执行的SQL:
SELECT O_YEAR , SUM(CASE WHEN NATION = 'CHINA' THEN VOLUME ELSE 0 END) / SUM(VOLUME) ASMKT_SHARE FROM ( SELECT EXTRACT(YEAR FROM O_ORDERDATE) AS O_YEAR, L_EXTENDEDPRICE * (1 - L_DISCOUNT) AS VOLUME, N2.N_NAME AS NATION FROM PART, SUPPLIER, …
这种办法的缺点是,这类工具多为独立的小程序,仅支持在命令行或结果文件中输出目标SQL,缺少程序接口以便被各类开发工具集成。
集算器 SPL
SPL设计了一套标准的SQL查询语法,该语法内置大量函数(还在持续追加中),可描述更多常用运算。SPL中有一个sqltranslate函数,可以把这种标准SQL翻译成不同数据库的SQL,实现数据库的迁移。
比如这样的标准SQL:
SELECT CLIENT, YEAR(ORDERDATE), SUM(AMOUNT) , COUNT(ORDERID) FROM ORDERS GROUP BY CLIENT, YEAR(ORDERDATE) HAVING SUM(AMOUNT) > 2000
用.sqltranlate("MYSQL")翻译后就得到:
SELECT CLIENT, YEAR(ORDERDATE), SUM(AMOUNT) , COUNT(ORDERID) FROM ORDERS GROUP BY CLIENT, YEAR(ORDERDATE) HAVING SUM(AMOUNT) > 2000
而如果使用.sqltranslate("ORACLE")将返回:
SELECT CLIENT, EXTRACT(YEAR FROM ORDERDATE), SUM(AMOUNT) , COUNT(ORDERID) FROM ORDERS GROUP BY CLIENT, EXTRACT(YEAR FROM ORDERDATE) HAVING SUM(AMOUNT) > 2000
可以看到,标准函数能够正确地根据数据库选择相应的函数。
此外,对于上文中提及的HQL本身不支持的:INTERVAL n DAY,SPL也可以处理:
标准SQL:
SELECT ADDDAYS(ORDERDATE,3) FROM ORDERS
翻译成MySQL SQL:
SELECT ORDERDATE + INTERVAL 3 DAY FROM ORDERS
翻译成Oracle SQL:
SELECT ORDERDATE + NUMTODSINTERVAL(3,'DAY') FROM ORDERS
SPL实现SQL移植,采取的策略是只对标准SQL中的函数进行翻译,不翻译(原样照抄)语句,从而使标准SQL可描述更多的运算。比如,对于下面的子查询无论翻译成哪种数据库SQL都不会变,也都可以正常执行。
SELECT ORDERID, M FROM ( SELECT ORDERID, MONTH(ORDERDATE) M FROM ORDERS) T1
SPL很容易被JAVA 集成而使这个移植功能在应用程序中使用,进一步信息可参考:《Java 如何调用 SPL 脚本》。