beego-ORM 适配达梦

麒麟&达梦适配系列:

1.麒麟服务器上安装 DM8
2.配置 UnixODBC
3.beego-ORM 适配达梦



在选型设计的时候,主要考虑了以下几种 ORM:

ORM 名称 可行性 优势 劣势 说明
oci8 不可行 因为 DM 和 oracle 很像,所以尝试使用 oci 对接

测试时候发现发现:Open连接没有问题,但是Query的时候会报 Lost Connection

经在官网查询,及和达梦同学确认,发现这确实是不可行的死路
XORM 可行 好对接 内部没有使用经验
星少(6.2k)
未进行对接尝试
GORM@V1.0 可行 好对接
内部使用广泛
不再新增feature(已和作者确认) 进行过对接尝试:ORM 和 Raw SQL 均可行
GORM@V2.0 可行 星多(21.9k)
GORM@V1.0迁移过去的,质量有保障
内部同学曾尝试升级,单测不过,暂时搁置 未进行对接尝试
beego ORM 可行 星多(25.5k)
内部有使用经验
对接完成


因为我们打头阵,要和 DM 对接的项目本身用的是 beego ORM,所以 beego ORM 也是我们最终的选用方案。

项目已计划开源,请关注后续更新

项目本身使用的 DB 是 MySQL。所以,除了需要对接 ODBC 这种新驱动方式外,还需要做一些定制化的处理,来保障项目使用平滑:

1 自增 ID 的处理

MySQL 指定自增的方式是: AUTO_INCREMENT

DM 指定自增的方式是: IDENTITY

区别于 AUTO_INCREMENT 的可指定(insert)可修改(update),IDENTITY 默认是不可指定的,更不可修改。

使用 SET IDENTITY_INSERT SCHEMA.TABLE ON 命令,可以使得 在 insert 时指定自增列的值 (仅对当前连接生效)。

但是,没有命令可以实现:update 自增列,只能通过 delete + insert 方式绕过这个问题。



2 对于获取LastInsertID的处理

在原始项目中,我们使用 orm.Insert 来实现单条数据的插入,并获得返回值 LastInsertID 来做后续处理。

而我们使用的适配 DM8 的 odbc pkg 没有实现这部分功能,调用即报错。源码如下:

func (r *Result) LastInsertID() (int64, error) {
    // TODO(brainman): implement (*Result).LastInsertID
    return 0, errors.New("not implemented")
}

经过对 cgo 代码的分析,发现确实没有现成的变量可以实现这个功能。经查询文档,发现可以使用 SELECT SCOPE_IDENTITY() 来做封装实现:

func lastInsertID(q dbQuerier, mi *modelInfo) (lastInsertID int64, err error) {

    if &mi.fields.pk == nil {
        return
    }
    lidQuery := fmt.Sprintf("SELECT SCOPE_IDENTITY()")
    lid, err := q.Query(lidQuery)
    for lid.Next() {
        err = lid.Scan(&lastInsertID)
        if err != nil {
            return
        }
    }
    return
}




3 多种 insert 方式支持

beego ORM 实现了多种 insert 方式,我们用到的有 3 种:

  1. insert
  2. prepare insert
  3. multi-insert

其中,multi-insert 指的是这种:insert into tbl () values (),(),()...();

对于此种方式,ORM 默认返回影响行数,计算方式如下:

var sumRowCount int64
for {
    var c api.SQLLEN
    ret := api.SQLRowCount(s.os.h, &c)
    if IsError(ret) {
        return nil, NewError("SQLRowCount", s.os.h)
    }
    sumRowCount += int64(c)
    if ret = api.SQLMoreResults(s.os.h); ret == api.SQL_NO_DATA {
        break
    }
}

对于 insertprepare insert,ORM 返回 LastInsertID,获得方式见:“ 2 对于获取LastInsertID的处理 ”

说明:

  • 我们使用 prepare insert 是为了提高插入效率,而通过上述封装方式去获得 LastInsertID,相当于是 double 了交互,于是在此暂不进行上述方法的调用
  • insert 时正常调用该方法,获取 LastInsertID



4 大小写的问题

在大小写不敏感的环境下,MySQL 会默认将表名、列名等相关字符转化为小写。

而 DM 会将相关字符转化为大写。

解决办法:定义结构体时,做 column 指定,避免渲染出错。

示例:

type TblTest struct {
    Id             uint32 `orm:"column(ID)";pk`
    NameTest       uint32 `orm:"column(NAME_TEST);sequence(PK_APP)"`
    ...
}

func (t *TblTest) TableName() string {
    return "TBL_TEST"
}

说明:sequence 是为适配 DM 序列使用,封装的新 tag,详见开源代码。



5 反引号的使用

在 MySQL 中,常使用反引号 `` 来扩起表名和列名,避免因使用关键字定义了表或列,影响到使用。

但是 DM,它是不能解析反引号的,所以我们改用双引号扩起表名和列名。



6 用户和模式

区别于 MySQL,DM 的模式是创建在用户下的,一个用户可以拥有多个模式。

创建用户时,会默认创建同名模式。模式一经建成,无法修改所属用户。建议大家直接创建同名账号,进行对应的模式使用,而非反其道而行之。



7 语法差别

这里拿 MySQL 为例,与 DM8 语法做个类比:

MySQL DM8




8 系统表差别

这里拿 MySQL 为例,与 DM8 系统表做个类比:

MySQL DM8



因为 DM 是没办法在 MacOS 环境编译的,而 UnixODBC 又必须引用 DM 的 .so 文件,所以选择将 UnixODBC 部署在 arm 架构的 docker 中。

最终的结构示意图:

DM8 结构图.jpg

我个人是试用 GoLand + dlv 做远程调试的,配置方式参考:goland远程调试

注意:远程和本地项目路径必须一致,不然断点会调到莫名的位置


本文章为麒麟&达梦适配系列第三篇。

上一篇:配置 UnixODBC