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
}
相关推荐
风象南几秒前
SpringBoot 自研「轻量级 API 防火墙」:单机内嵌,支持在线配置
后端
液态不合群1 分钟前
下划线字段在golang结构体中的应用
go
Victor35617 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor35618 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学19 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bobz9659 小时前
小语言模型是真正的未来
后端
DevYK10 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
一只叫煤球的猫10 小时前
🕰 一个案例带你彻底搞懂延迟双删
java·后端·面试
冒泡的肥皂10 小时前
MVCC初学demo(一
数据库·后端·mysql
颜如玉11 小时前
ElasticSearch关键参数备忘
后端·elasticsearch·搜索引擎