Oracle 11g 的 Golang GORM 分页兼容解决方案

文章目录

    参考前文,Ubuntu 安装 Oracle Instant Client, 并测试 Golang Gorm 读取 Oracle 数据库 搞定了 Oracle Instant Client 和 GORM 连接 Oracle 数据库的环境后,我以为马上就能开始写逻辑了。但是,测试了一下,发现了一个严重的问题。GORM 默认的实现方案,并不支持低版本 Oracle 的分页语法,准确的说是不支持 Oracle 11g 及更低版本。

    FETCH NEXT xx ROWS ONLY

    在 admin 管理后台,实现分页查询是非常常见的需求。但是,如果使用 GORM 的分页语法:

    db.Select("*").
    	Limit(limit).
    	Offset((page - 1) * limit).
    	Find(&items)
    

    会报错:

    dpiStmt_execute: ORA-00933: SQL command not properly ended
    [1.344ms] [rows:0] SELECT * FROM "xxx"."some_table" ORDER BY id desc FETCH NEXT 20 ROWS ONLY
    

    FETCH NEXT 20 ROWS ONLY 是 Oracle 12c 及更高版本才引入的语法。Oracle 11g,无法识别这段代码,从而报出 ORA-00933。

    我尝试了让 AI 写一个分页查询的 SQL,我发现太麻烦了,而且可读性也很差。我感觉还是得用 ORM 框架来做。

    纯 Golang 实现的 godoes/gorm-oracle

    https://github.com/godoes/gorm-oracle

    这个库文档里明确说明了支持 Oracle 11g。可以看到配置里有个专门的配置:

    // RowNumberAliasForOracle11 is the alias for ROW_NUMBER() in Oracle 11g,
    // defaulting to ROW_NUM
    RowNumberAliasForOracle11: "ROW_NUM",
    

    看了一下 github 上的代码实现:

    https://github.com/godoes/gorm-oracle/blob/main/oracle.go#L290

    里面的 RewriteLimit11 方法,重写了 GORM 的分页语法,改成了 Oracle 11g 支持的分页语法。

    // RewriteLimit11 rewrite the LIMIT clause in the query to accommodate pagination requirements for Oracle 11g and lower database versions
    //
    // # Limit and Offset
    //
    //	SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY column) AS ROW_NUM FROM table_name T)
    //	WHERE ROW_NUM BETWEEN offset+1 AND offset+limit
    //
    // # Only Limit
    //
    //	SELECT * FROM table_name WHERE ROWNUM <= limit ORDER BY column
    //
    // # Only Offset
    //
    //	SELECT * FROM table_name WHERE ROWNUM > offset ORDER BY column
    func (d Dialector) RewriteLimit11(c clause.Clause, builder clause.Builder) {
    	limit, ok := c.Expression.(clause.Limit)
    	if !ok {
    		return
    	}
    	offsetRows := limit.Offset
    	hasOffset := offsetRows > 0
    	limitRows, hasLimit := d.getLimitRows(limit)
    	if !hasOffset && !hasLimit {
    		return
    	}
    
    	var stmt *gorm.Statement
    	if stmt, ok = builder.(*gorm.Statement); !ok {
    		return
    	}
    
    	if hasLimit && hasOffset {
    		// 使用 ROW_NUMBER() 和子查询实现分页查询
    		if d.RowNumberAliasForOracle11 == "" {
    			d.RowNumberAliasForOracle11 = "ROW_NUM"
    		}
    		subQuerySQL := fmt.Sprintf(
    			"SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY %s) AS %s FROM (%s) T) WHERE %s BETWEEN %d AND %d",
    			d.getOrderByColumns(stmt),
    			d.RowNumberAliasForOracle11,
    			strings.TrimSpace(stmt.SQL.String()),
    			d.RowNumberAliasForOracle11,
    			offsetRows+1,
    			offsetRows+limitRows,
    		)
    		stmt.SQL.Reset()
    		stmt.SQL.WriteString(subQuerySQL)
    	} else if hasLimit {
    		// 只有 Limit 的情况
    		d.rewriteRownumStmt(stmt, builder, " <= ", limitRows)
    	} else {
    		// 只有 Offset 的情况
    		d.rewriteRownumStmt(stmt, builder, " > ", offsetRows)
    	}
    }
    

    测试了一下,果然可以正常分页查询了!

    而 gorm 默认的 Oracle 驱动,是不支持 Oracle 11g 的分页语法的:

    github.com/oracle-samples/gorm-oracle

    从这个山寨的名字就感觉不太靠谱。

    大小写问题

    在 Oracle 中,当你给标识符(表名、列名、Schema 名)加上双引号时,Oracle 会变得严格区分大小写。因为 GORM 默认会给表名和列名加上双引号,所以你会遇到下面的问题:

    dpiStmt_execute: ORA-00942: table or view does not exist
    
    [14.539ms] [rows:0] SELECT count(*) FROM "some_schema"."some_table"
    

    解决方法就是,把表名和 Schema 名都改成大写。

    Oracle 11g 是哪年发布的?

    Oracle 11g 于 2007 年发布,我也是服了,公司买这套 MES 系统,也是祖传架构了。。。

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式