Oracle语法与mysql/pg异同&gorm如何支持oracle&支持达梦?

之前分享过mysql和postgresql差异?gorm如何同时兼容?此篇文章在上次基础上又对oracle进行了调研。目前来看,go对于oracle支持较不友好,文章中的驱动组件请不要轻易用在生产环境

1. 背景

接上篇文章背景来讲,除了可能支持postgresql外,oracle目前也是使用场景较多的一个数据库。此时我们需要考虑,如何让go项目在数据库切换为oracle情况下还能正常运作?除此之外,文中还对达梦数据库做了一些小研究,分析了与oracle的异同。另外,gorm目前并没有对oracle原生支持的驱动,只能通过开源的一些gorm-driver来使用。但是这些驱动我在看完源码并使用下来之后发现还是存在不少问题,并不能很好的使用于正常环境。

本文是基于go角度来对oracle数据库展开讨论,包括兼容性、适配等。

本文不讲述两者的选型优劣,仅描述常见的基本差异,如基本类型、语法、DDL/DML、字符集、SDK以及平时使用时存在的问题做举例和总结,避免在业务改造兼容两者时花精力又踩坑。

Mysql 5.7/oracle 19c为例

2. 差异

2.1 基本类型

类型 兼容类型 备注
数字类型 smallint、int、integer、decimal、double、float 在oracle中本质上数字类型只有NUMBER(precision,scale)类型,为了兼容SQL标准,新增了对上述类型的支持。 float和double只在10g开始支持的 decimal([1-65], [0-30])是number的同义词 对于smalint、int、integer映射为number(38, 0),实际使用时可以根据自己需要设置,如: tinyint=number(3,0) smallint=number(5,0) meduimint=number(7,0) int/integer=number(10,0) bigint(20,0)
字符类型 char(n)/varchar(n) 对于短字符类型,oracle只支持varchar,varchar2,char等可变字符集。其中推荐使用varchar2,因为varchar在空间利用率上没有varchar2好。但是varchar兼容性更高,具体看如何应用。 对于长字符类型,oracle只支持clob/nclob/long等几种类型。clob可存储4GB容量字符串,long可存储2GB字符串,另外clob的性能会比long更好;clob和nclob的主要区别是,nclob在存储特定字符的时候使用,比如国家字符集。 平时使用普通字符串,使用varchar2/clob可以满足绝大部分需求。
二进制类型 可以看出,oracle中二进制类型可用的是blob,这个与mysql一致,但是与pg的bytea不同。 raw:是二进制最基础类型,可存储2000字节。 long raw:可存储最长2GB,不推荐使用,由blob代替 bfile:存储二进制文件对象,比如图片文件等,主要存储二进制文件指针,指针指向具体的大文件。 blob:存储二进制内容,最大可存储4GB。
日期类型 timestamp 推荐使用timestamp,与pg一样,如果没有更高精度要求,推荐用timestamp(0)。 注意时区问题,建议在插入数据的时候手动赋值,不要使用默认的系统时间。
布尔类型 smallint oracle没有bool类型的值,只能用number来代替。
json支持 oracle对于该特性在12.1.0.2开始支持的 oracle没有直接json字段声明,需要对clob字段使用约束设置: constraint xxxx check (column is json)来设置

2.2 字符集

oracle的字符集默认与数据库有关系,数据库创建后无法更改。所以在创建数据库的时候可以指定字符集和国家字符集。默认为AL32UTF8,通用字符集。对于pg和mysql默认字符集分别为UTF-8和latin1,但是mysql在8.0之后默认为utf8mb4。

2.3 DDL

2.3.1 创建表

oracle在创建表中与mysql和pg有较多不同,下面内容oracle创建表的相关语法,如果要看mysql和pg相关建表语句,可以看本文引用的文章。

sql 复制代码
CREATE TABLE T1(
   id NUMBER(19) generated by default as identity,
   a_col NVARCHAR2(32),
   b_col CLOB,
   c_col NUMBER(2,0),
   d_col NVARCHAR2(128),
   e_col TIMESTAMP(0) DEFAULT SYSTIMESTAMP,
   f_col CLOB DEFAULT '{}',
   g_col NUMBER(1) DEFAULT 0,
   CONSTRAINT uniq_a_col UNIQUE (a_col),
   CONSTRAINT pk_id PRIMARY KEY (id),
   CONSTRAINT f_col_json CHECK ( f_col iS JSON )
);

-- 创建索引
CREATE INDEX idx_c_col_d_col ON t1(c_col, d_col);

-- 注释与pg十分相似
COMMENT ON TABLE T1 IS "T1表";
COMMENT ON COLUMN T1.id IS "id列";
COMMENT ON COLUMN T1.a_col IS "a_col列";
COMMENT ON COLUMN T1.b_col IS "b_col列";
COMMENT ON COLUMN T1.c_col IS "c_col列";
COMMENT ON COLUMN T1.d_col IS "d_col列";
COMMENT ON COLUMN T1.e_col IS "e_col列";
COMMENT ON COLUMN T1.f_col IS "f_col列";

注意 :可能会要求在创表时判断是否存在,oracle在23版本之前都没有类似CREATE TABLE .. IF NOT EXISTS语法的,所以这里需要用特殊的方式实现:

scss 复制代码
DECLARE v_cnt NUMBER;
BEGIN
    SELECT COUNT(1) INTO v_cnt FROM USER_TABLES WHERE TABLE_NAME = 'T1';
    IF v_cnt = 0 THEN
        EXECUTE IMMEDIATE
        'CREATE TABLE T1(
            id NUMBER(19) generated by default as identity,
            a_col NVARCHAR2(32),
            b_col CLOB,
            c_col NUMBER(2,0),
            d_col NVARCHAR2(128),
            e_col TIMESTAMP(0) DEFAULT SYSTIMESTAMP,
            f_col CLOB DEFAULT ''{}'', -- 转义
            g_col NUMBER(1) DEFAULT 0,
            CONSTRAINT uniq_a_col UNIQUE (a_col),
            CONSTRAINT pk_id PRIMARY KEY (id),
            CONSTRAINT f_col_json CHECK ( f_col iS JSON )
        )';
    END IF;

    -- 判断索引是否存在
    SELECT COUNT(1) INTO v_cnt FROM USER_IND_COLUMNS WHERE INDEX_NAME = UPPER('idx_c_col_d_col');
    IF v_cnt = 0 THEN
        EXECUTE IMMEDIATE
        'CREATE INDEX idx_c_col_d_col ON T1(c_col, d_col)';
    END IF;
END;

PS:

  • 在oracle中所有字段默认是大写的,即使创建表时写的是小写字段,创建成功后会转为大写。
  • oracle在12c之前,通常使用触发器来实现自增的。12c开始提供自增列。使用generated by default as identity生成自增长序列是由系统自动生成的,如果删除表系统会将与之绑定的sequence放入回收站,我们无法手动删除。此时使用PURGE RECYCLEBIN即可删除掉。

2.3.2 列操作

由于主要用mysql,所以以mysql进行对比,如果想看postgresql对于列操作的语法,可以看本文引用文章。

2.3.3 约束操作

2.3.4 索引操作

2.3.5 用户操作

2.3.6 小结

  • 在表创建上,mysql和oracle有许多语法上的不同。包括类型,设置注释,设置索引等。
  • 在列操作上,一般正常使用,语法其实和mysql没多大区别,基本类似。
  • 在约束操作上,oracle支持check约束,其余基本一样。
  • 在索引操作上,两者比较相似,不过mysql有create和alter两种语法,oracle只有create的方式。
  • 在用户操作上,两者是不同的,差异较大。

2.4 DML

2.4.1 插入数据

  • 语法

oracle在插入语句上,和标准的sql语句基本一样,且支持returning语句。但是唯一不同的是,oracle不支持VALUES的批量插入。

sql 复制代码
-- oracle单条插入
INSERT INTO table_name (column1, column2, column3, ...) VALUES (value1, value2, value3, ...) RETURNING ID INTO :xx;
  • INSERT ALL批量插入(不推荐)
sql 复制代码
-- 官方提供的一种批量插入
INSERT ALL
INTO table_name (column1, column2, column3, ...) VALUES (value1, value2, value3, ...)
INTO table_name (column1, column2, column3, ...) VALUES (value1, value2, value3, ...)
INTO table_name (column1, column2, column3, ...) VALUES (value1, value2, value3, ...)
SELECT 1 FROM DUAL;

PS:INSERT ALL用来给多张表插入数据,当然表可以是同一张。但是需要注意,这种批量插入方式需要自己设置自增ID的值,因为INSERT ALL并不会随着插入数据而自增ID,下面举一个例子:

sql 复制代码
INSERT ALL
    INTO T1 (A_COL,B_COL,C_COL,D_COL) values('1','你好1',1,'很好1')
    INTO T1 (A_COL,B_COL,C_COL,D_COL) values('2','你好2',2,'很好2')
    INTO T1 (A_COL,B_COL,C_COL,D_COL) values('3','你好3',3,'很好3')
SELECT 1 FROM DUAL;

但是报以下错误:

必须指定主键ID才可以插入成功。

  • INSERT INTO ... SELECT批量插入

使用该方式可以支持批量插入并且不需要指定ID,但是如果遇到冲突则无法ignore。此外,对于其他方式的批量插入,还有通过事务的方式一个一个进行插入,但是效率极其低下。

sql 复制代码
INSERT INTO T1(A_COL, B_COL, C_COL, D_COL, F_COL)
SELECT A_COL, B_COL, C_COL, D_COL, F_COL FROM (
    SELECT '11' A_COL, '11' B_COL, 11 C_COL, '11' D_COL, '{"nzx": 1}' F_COL FROM DUAL
    UNION
    SELECT '22' A_COL, '22' B_COL, 22 C_COL, '22' D_COL, '{"nzx": 2}' F_COL FROM DUAL
);
  • MERGE INTO 批量插入时冲突解决

MERGE INTO用来处理冲突时的结果,如果没有冲突则可以执行插入,根据这个特性可以实现批量插入,但是需要结合关键字UNION

冲突中WHEN MATCHED THEN可以不写,这样相当于ON CONFLICT DO NOTHING一样的效果。同理,WHEN NOT MATCHED THEN也可以不写,这样没有冲突也不会插入新数据。

sql 复制代码
-- 解决冲突
MERGE INTO T1
USING (SELECT
           0            AS ID,
           '11'         AS A_COL,
           '11'         AS B_COL,
           11           AS C_COL,
           '11'         AS D_COL,
           '{"nzx": 1}' AS F_COL
       FROM DUAL) excluded
ON (T1.ID = excluded.ID)
WHEN MATCHED THEN
    UPDATE
    SET
        B_COL=excluded.B_COL,
        C_COL=excluded.C_COL,
        D_COL=excluded.D_COL
WHEN NOT MATCHED THEN
    INSERT (A_COL, B_COL, C_COL, D_COL, F_COL)
    VALUES
        (excluded.A_COL, excluded.B_COL, excluded.C_COL, excluded.D_COL, excluded.F_COL);

-- 批量插入
MERGE INTO T1
USING (SELECT
           0            AS ID,
           '11'         AS A_COL,
           '11'         AS B_COL,
           11           AS C_COL,
           '11'         AS D_COL,
           '{"nzx": 1}' AS F_COL
       FROM
           DUAL
       UNION
       SELECT
           0            AS ID,
           '22'         AS A_COL,
           '22'         AS B_COL,
           22           AS C_COL,
           '22'         AS D_COL,
           '{"nzx": 2}' AS F_COL
       FROM
           DUAL) excluded
ON (T1.ID = excluded.ID)
WHEN MATCHED THEN
    UPDATE
    SET
        B_COL=excluded.B_COL,
        C_COL=excluded.C_COL,
        D_COL=excluded.D_COL
WHEN NOT MATCHED THEN
    INSERT (A_COL, B_COL, C_COL, D_COL, F_COL)
    VALUES
        (excluded.A_COL, excluded.B_COL, excluded.C_COL, excluded.D_COL, excluded.F_COL);
  • 对于 bool 类型问题

在oracle中没有bool/boolean类型,只能通过NUMBER代替。

  • 批量插入性能对比

目前插入有2种方式,一种通过一个一个插入方式,另一种为通过INSERT INTO ... SELECT方式插入。两者之间性能对比如下:

结果简直天差地别。

text 复制代码
BenchmarkBatchCreate-12            1000000000                 0.1309 ns/op
BenchmarkSingleCreate-12                    1            16524782331 ns/op

2.4.2 更新数据

  • 语法

更新数据的语法与mysql以及postgresql大同小异。

sql 复制代码
UPDATE table_name SET column1=value1, column2=value2 WHERE some_column=some_value;
  • RETURNING语法

oracle在更新数据时也支持return语句,但是需要定义一个变量并赋值,比如:

sql 复制代码
UPDATE T1 SET B_COL='11' WHERE 1=1 RETURNING ID INTO :ID;

其中上述:ID就是所谓变量,需要给其赋值才行,在PL / SQL 块中可以对其进行处理。在go中则需要使用sql.Out去接收,这一点与pg/mysql完全不同。pg本质是在执行更新后返回对应列,类似于查询结果返回;而oracle则是赋值,另外如果是多行更新 ,在go中无法有合适的类型接收,只能在PL/SQL 块中处理。所以,多行操作在go中是不支持RETURNING

2.4.3 删除数据

  • 语法

oracle删除的语法类似,如下:

sql 复制代码
DELETE FROM table_name WHERE some_column=some_value;

同时删除也支持RETURNING,与update一样的情况。在go中无法支持多行删除returning,只能在PL / SQL 块中处理

sql 复制代码
DELETE FROM table_name WHERE some_column=some_value RETURNING *;

2.4.4 查询数据

查询语句oracle与mysql和pg其实是类似的,这里主要简单介绍下连接查询的区别。

oracle mysql pg
INNER JOIN(default)、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN、NATURAL JOIN、CROSS JOIN INNER JOIN(default)、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN INNER JOIN(default)、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN、CROSS JOIN
自然连接类似于内连接,区别在于列名和值必须相等。
  • 分页

oracle的分页与mysql和postgresql完全不同,使用rownum控制。但是使用该隐藏字段,必须只能用</<=/=无法使用>,这就导致需要多层嵌套实现分页,这样在数据量多时会十分影响性能。不过在oracle11之后,支持了fetch方式实现,弥补了不足点,但是使用fetch必须搭配order by。下面分别举例:

ROWNUM

sql 复制代码
SELECT * FROM (SELECT ROWNUM rn,id,realname FROM (SELECT id,realname FROM T_USER)WHERE ROWNUM<=20) t2 WHERE T2.rn >=10;

FETCH

sql 复制代码
SELECT * FROM table ORDER BY column OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

2.4.5 空字符串与NULL

在Oracle中,空字符串会被自动解析为NULL,此时如果查询条件带有空字符串,将会出现错误。例如:select * from table where col = ''会被解析为col = NULL导致无法查到数据,及时col的确为空。这里需要解析为col IS NULL才是合理的,这也是oracle数据库与其他的关系型数据库差别所在。不过好在如今信创数据库时同时兼容TD模式和ORACLE模式,正常来说可以根据你的需要切换设置。

2.4.6 小结

  • oracle在插入数据上与mysql和postgresql有明显差别,这一点需要注意。
  • 使用oracle与使用pg一致,双引号代表的是列,单引号代表的是值;这和mysql不同,mysql反引号代表列,单引号代表字符串。
  • 在oracle中也有boolean类型,只能使用0/1代表。在pg中数字0或1是无法代表boolean类型的,但是mysql可以。

2.5 常用的内置视图语句

基本ALL开头表示所有表,USER开头表示当前用户拥有的。

sql 复制代码
-- 查询数据库版本
SELECT * FROM V$VERSION;
-- 查询当前数据库名,但是有版本限制(12c+)
SELECT ORA_DATABASE_NAME as "Current Database" FROM DUAL;
-- 查询当前使用的数据库名(oracle需要有权限)
SELECT NAME FROM V$DATABASE;
-- 查询当前数据库下所有表,sys用户或者dba可以查询
SELECT TABLE_NAME FROM ALL_TABLES;
-- 查询某个表下所有索引
select * from ALL_INDEXES where table_name='T1';

-- 查看表中列的字段信息,比如列名,类型等
DESC tablename;
-- 查询当前用户可以访问的所有表
SELECT TABLE_NAME FROM user_tables;
-- 当前用户拥有的表的所有索引信息
SELECT * FROM USER_INDEXES;
-- 当前用户拥有的表的索引中指向所有列信息
SELECT * FROM USER_IND_COLUMNS;
-- 当前用户拥有的表的所有索引信息
SELECT * FROM USER_SEQUENCES;
-- 显示用户定义的表的表示列信息,包括索引信息,序列信息等
select * from USER_TAB_IDENTITY_COLS;
-- 查询回收站内容你
-- 比如删除表时,sequence会放入回收站
SELECT * FROM RECYCLEBIN;
-- 清空回收站
PURGE RECYCLEBIN;

3. 与达梦差异

3.1 建表

目前达梦数据库市面上用最多的是dm8版本,且达梦数据库对于语法的兼容性很高,在建表语句上体现出来不会像oracle一样复杂。例如oracle在低版本需要结合BEGINEND通过捕捉错误方式来做到,但是达梦支持IF NOT EXISTS语句。不管是table还是index在dm8中都是支持的。

sql 复制代码
CREATE TABLE IF NOT EXISTS T1(
   id NUMBER(19) generated by default as identity,
   a_col NVARCHAR2(32),
   b_col CLOB,
   c_col NUMBER(2,0),
   d_col NVARCHAR2(128),
   e_col TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   f_col CLOB DEFAULT '{}',
   g_col NUMBER(1) DEFAULT 0,
   CONSTRAINT uniq_a_col UNIQUE (a_col),
   CONSTRAINT pk_id PRIMARY KEY (id),
   CONSTRAINT f_col_json CHECK ( f_col iS JSON )
);

-- 创建索引
CREATE INDEX IF NOT EXISTS idx_c_col_d_col ON t1(c_col, d_col);

3.2 插入

达梦的批量插入兼容oracle语法,同时也兼容mysql和pg的语法,即INSERT INTO table (col1, col2, ...) VALUES (v1, v2, ...)

如果需要指定id插入,则需要先执行SET identity_insert <tablename> ON,一个会话只能开启一个table,切换另一个之前的会自动置为OFF。

3.3 分页

达梦使用的驱动与oracle有些许不同,但是达梦对于语法的支持兼容性会更好,除了支持oracle的语法,同时也支持mysql等其他数据库的部分语法。对于分页的支持,在oracle中显得臃肿,所以达梦也支持mysql的limit分页语法。

达梦数据库在分页上支持3种方式,top/rownnum/limit,分别是sql server语法,oracle语法,mysql语法,都兼容。这里由于使用的是mysql/pg方式,所以在使用达梦时推荐使用limit实现分页。

3.4 权限

达梦中有几个内置角色权限,如DBA/RESOURCE/PUBLIC等。其中RESOURCE较为常用,通常赋予该权限即可拥有自己空间下的所有权限,其他用户无权限。DBA则是最大权限。

sql 复制代码
-- 赋予创建相关系统权限
GRANT CREATE TABLE, CREATE INDEX TO siney;

-- 服务resource权限
GRANT REOUSRCE TO siney;

3.5 其他

  • 达梦数据库支持returningreturn两个关键字,在插入一条数据时可以使用。这一点与oracle并无太大差异。
  • 返回字段需要注意。oracle和达梦本身对于大小写不敏感,oracle返回的列名是大写的,在达梦返回的是小写。但是在使用sdk的时候需要注意,尽量不要使用gorm:"column:xxxx"来指定,这样就无法同时兼容两者。

4. SDK

目前市面上支持一些oracle数据库驱动,但是基本都不维护了。先前也使用这些驱动进行了测试,其中存在不少bug和问题。如批量插入的方式为一条一条插入,性能很差;对于冲突解决和迁移代码存在问题等。另外对于代码迁移目前暂不支持,建议通过自己写DDL的方式来处理。

下面根据上述存在的问题重新写了驱动,支持普通的CRUD能力,感兴趣可以去看下源码(个人开发可能存在bug,请不要用于生产环境)。另外对于单条语句的插入也支持返回主键ID,但是批量插入则是不支持。

4.1 Oracle

Drivergithub.com/sineycoder/...

Using

go 复制代码
package main

import (
        oracle "github.com/sineycoder/gorm-oracle"
        "gorm.io/gorm"
)

func main() {
        // oracle://user:password@127.0.0.1:1521/service
        url := oracle.BuildUrl("127.0.0.1", "1521", "service", "user", "password", nil)
        db, err := gorm.Open(oracle.Open(url), &gorm.Config{})
        if err != nil {
                // panic error or log error info
        }

        // do somethings
}

4.2 达梦

Drivergithub.com/sineycoder/...

Using

go 复制代码
package main

import (
        dm "github.com/sineycoder/gorm-dm"
        "gorm.io/gorm"
)

func main() {
        // dm://user:password@127.0.0.1:1521?autoCommit=true
        url := dm.BuildDsn("127.0.0.1", 1521, "user", "password", nil)
        db, err := gorm.Open(dm.Open(url), &gorm.Config{})
        if err != nil {
                // panic error or log error info
        }

        // do somethings
}
相关推荐
苏三有春3 分钟前
五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)
git·go·github
小蜗牛慢慢爬行10 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
A小白590832 分钟前
Docker部署实践:构建可扩展的AI图像/视频分析平台 (脱敏版)
后端
goTsHgo39 分钟前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
求知若饥1 小时前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
左羊1 小时前
【代码备忘录】复杂SQL写法案例(一)
后端
gb42152872 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端