9154afab6b
- 新增驱动抽象层 driver_adapter(go-ora/godror 双驱动切换) - 新增 Update/Delete/Query 回调,完善 RETURNING INTO 子句 - 修复 create.go 事务悬挂行 BUG、批量 RowsAffected、主键 WHERE 注入 - 版本感知体系:分页/自增/序列默认值/BOOLEAN/32k VARCHAR2 分级能力判定 - 11g 序列+触发器自增、ON UPDATE 触发器、默认值智能转换 - 升级 go-ora v2.8.19 → v2.9.0 - 单元测试+模块测试+集成测试共 96 个,真实 Oracle 11g 全绿 - 测试 DSN 密码移除,改为 ORACLE_DSN 环境变量注入
455 lines
12 KiB
Go
455 lines
12 KiB
Go
package oracle
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"gorm.io/gorm/utils"
|
||
|
||
// _ "github.com/godror/godror"
|
||
_ "github.com/sijms/go-ora/v2"
|
||
"github.com/thoas/go-funk"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/callbacks"
|
||
"gorm.io/gorm/clause"
|
||
"gorm.io/gorm/logger"
|
||
"gorm.io/gorm/migrator"
|
||
"gorm.io/gorm/schema"
|
||
|
||
"git.charlienet.top/go/oracle/driver_adapter"
|
||
)
|
||
|
||
const RowNumberAliasForOracle11 = "ROW_NUM"
|
||
|
||
// Oracle 版本主版本号常量(对应各版本引入的数据库特性)
|
||
const (
|
||
OracleVersion10 = 10 // Oracle 10g
|
||
OracleVersion11 = 11 // Oracle 11g(不含 IDENTITY 列、OFFSET/FETCH 分页)
|
||
OracleVersion12 = 12 // Oracle 12c(引入 IDENTITY 列、OFFSET/FETCH 分页;12.1 起支持 Extended 32k VARCHAR2)
|
||
OracleVersion18 = 18 // Oracle 18c(12.2 的再版)
|
||
OracleVersion19 = 19 // Oracle 19c
|
||
OracleVersion21 = 21 // Oracle 21c(引入原生 BOOLEAN 列类型)
|
||
OracleVersion23 = 23 // Oracle 23ai(引入 VECTOR 类型)
|
||
)
|
||
|
||
// oracleMajor 返回数据库版本的主版本号;解析失败返回 0。
|
||
// 支持格式如 "11.2.0.4.0"、"19.0.0.0.0"、"23.0.0.0.0"。
|
||
func oracleMajor(dbVer string) int {
|
||
major, _ := strconv.Atoi(strings.Split(dbVer, ".")[0])
|
||
return major
|
||
}
|
||
|
||
// supportsIdentity 是否支持 IDENTITY 列(12c+ 支持 GENERATED ... AS IDENTITY;
|
||
// 11g 及以下需用序列 + BEFORE INSERT 触发器模拟自增)
|
||
func supportsIdentity(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
|
||
|
||
// supportsFetchOffset 是否支持 OFFSET/FETCH 分页语法(12c+ 引入;
|
||
// 11g 需改写为 ROWNUM 分页)
|
||
func supportsFetchOffset(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
|
||
|
||
// supportsNativeBoolean 是否支持原生 BOOLEAN 列类型(21c+ 引入;
|
||
// 更早版本需用 NUMBER(1) 模拟)
|
||
func supportsNativeBoolean(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion21 }
|
||
|
||
// supportsExtendedString 是否支持 Extended 32k VARCHAR2(12.2+ 默认开启;
|
||
// 12.1 需 MAX_STRING_SIZE=EXTENDED)。保守判定:主版本 >= 12 视为可能支持,
|
||
// 具体是否生效依赖数据库参数。
|
||
func supportsExtendedString(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
|
||
|
||
// supportsVector 是否支持 VECTOR 类型(23ai 引入,用于 AI Vector Search)
|
||
func supportsVector(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion23 }
|
||
|
||
// isOracle11g 判断当前数据库是否低于 12c(11g 及以下不支持 IDENTITY 列)。
|
||
// 保留该函数以兼容既有调用,内部委托 supportsIdentity 取反。
|
||
func isOracle11g(dbVer string) bool {
|
||
return !supportsIdentity(dbVer)
|
||
}
|
||
|
||
type Config struct {
|
||
DriverName string
|
||
DSN string
|
||
Conn gorm.ConnPool //*sql.DB
|
||
DefaultStringSize uint
|
||
DBName string
|
||
DBVer string
|
||
DriverType driver_adapter.DriverType // 新增:驱动类型(go-ora 或 godror)
|
||
SkipQuoteIdentifiers bool // 新增:是否跳过标识符引用
|
||
}
|
||
|
||
type Dialector struct {
|
||
*Config
|
||
}
|
||
|
||
func Open(dsn string) gorm.Dialector {
|
||
return &Dialector{Config: &Config{DSN: dsn}}
|
||
}
|
||
|
||
func New(config Config) gorm.Dialector {
|
||
return &Dialector{Config: &config}
|
||
}
|
||
|
||
func (d Dialector) DummyTableName() string {
|
||
return "DUAL"
|
||
}
|
||
|
||
func (d Dialector) Name() string {
|
||
return "oracle"
|
||
}
|
||
|
||
func (d Dialector) Initialize(db *gorm.DB) (err error) {
|
||
|
||
db.NamingStrategy = Namer{
|
||
NamingStrategy: db.NamingStrategy,
|
||
DBName: d.DBName,
|
||
}
|
||
d.DefaultStringSize = 1024
|
||
|
||
// register callbacks
|
||
//callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{WithReturning: true})
|
||
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
|
||
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"},
|
||
UpdateClauses: []string{"UPDATE", "SET", "WHERE", "RETURNING"},
|
||
DeleteClauses: []string{"DELETE", "FROM", "WHERE", "RETURNING"},
|
||
})
|
||
|
||
// d.DriverName = "godror"
|
||
d.DriverName = "oracle"
|
||
|
||
// godror.Batch
|
||
|
||
if d.Conn != nil {
|
||
db.ConnPool = d.Conn
|
||
} else {
|
||
db.ConnPool, err = sql.Open(d.DriverName, d.DSN)
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
err = db.ConnPool.QueryRowContext(context.Background(), "select version from product_component_version where rownum = 1").Scan(&d.DBVer)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err = db.Callback().Create().Replace("gorm:create", Create); err != nil {
|
||
return
|
||
}
|
||
|
||
// 注册 Update 回调
|
||
if err = db.Callback().Update().Replace("gorm:update", Update); err != nil {
|
||
return
|
||
}
|
||
|
||
// 注册 Delete 回调
|
||
if err = db.Callback().Delete().Replace("gorm:delete", Delete); err != nil {
|
||
return
|
||
}
|
||
|
||
// 注册 Query 回调
|
||
if err = db.Callback().Query().Replace("gorm:query", Query); err != nil {
|
||
return
|
||
}
|
||
|
||
for k, v := range d.ClauseBuilders() {
|
||
db.ClauseBuilders[k] = v
|
||
}
|
||
return
|
||
}
|
||
|
||
func (d Dialector) ClauseBuilders() map[string]clause.ClauseBuilder {
|
||
dbver, _ := strconv.Atoi(strings.Split(d.DBVer, ".")[0])
|
||
if dbver > 0 && dbver < 12 {
|
||
return map[string]clause.ClauseBuilder{
|
||
"LIMIT": d.RewriteLimit11,
|
||
}
|
||
|
||
} else {
|
||
return map[string]clause.ClauseBuilder{
|
||
"LIMIT": d.RewriteLimit,
|
||
}
|
||
}
|
||
}
|
||
|
||
func (d Dialector) RewriteLimit(c clause.Clause, builder clause.Builder) {
|
||
if limit, ok := c.Expression.(clause.Limit); ok {
|
||
if stmt, ok := builder.(*gorm.Statement); ok {
|
||
if _, ok := stmt.Clauses["ORDER BY"]; !ok {
|
||
s := stmt.Schema
|
||
builder.WriteString("ORDER BY ")
|
||
if s != nil && s.PrioritizedPrimaryField != nil {
|
||
builder.WriteQuoted(s.PrioritizedPrimaryField.DBName)
|
||
builder.WriteByte(' ')
|
||
} else {
|
||
builder.WriteString("(SELECT NULL FROM ")
|
||
builder.WriteString(d.DummyTableName())
|
||
builder.WriteString(")")
|
||
}
|
||
}
|
||
}
|
||
|
||
if offset := limit.Offset; offset > 0 {
|
||
builder.WriteString(" OFFSET ")
|
||
builder.WriteString(strconv.Itoa(offset))
|
||
builder.WriteString(" ROWS")
|
||
}
|
||
|
||
v := 0
|
||
if limit.Limit != nil {
|
||
v = *limit.Limit
|
||
}
|
||
if v > 0 {
|
||
builder.WriteString(" FETCH NEXT ")
|
||
builder.WriteString(strconv.Itoa(v))
|
||
builder.WriteString(" ROWS ONLY")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Oracle11 Limit
|
||
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 {
|
||
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),
|
||
RowNumberAliasForOracle11,
|
||
strings.TrimSpace(stmt.SQL.String()),
|
||
RowNumberAliasForOracle11,
|
||
offsetRows+1,
|
||
offsetRows+limitRows,
|
||
)
|
||
|
||
stmt.SQL.Reset()
|
||
stmt.SQL.WriteString(subQuerySQL)
|
||
} else if hasLimit {
|
||
// 只有 Limit 的情况
|
||
subQuerySQL := fmt.Sprintf(
|
||
"SELECT * FROM (%s) WHERE ROWNUM <= %d",
|
||
strings.TrimSpace(stmt.SQL.String()),
|
||
limitRows,
|
||
)
|
||
// d.rewriteRownumStmt(stmt, builder, " <= ", limitRows)
|
||
|
||
stmt.SQL.Reset()
|
||
stmt.SQL.WriteString(subQuerySQL)
|
||
} else {
|
||
// 只有 Offset 的情况
|
||
// 偏移后取剩余所有记录
|
||
subQuerySQL := fmt.Sprintf(
|
||
"SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY %s) AS %s FROM (%s) T) WHERE %s > %d",
|
||
d.getOrderByColumns(stmt),
|
||
RowNumberAliasForOracle11,
|
||
strings.TrimSpace(stmt.SQL.String()),
|
||
RowNumberAliasForOracle11,
|
||
offsetRows+1,
|
||
)
|
||
|
||
stmt.SQL.Reset()
|
||
stmt.SQL.WriteString(subQuerySQL)
|
||
|
||
// d.rewriteRownumStmt(stmt, builder, " > ", offsetRows)
|
||
}
|
||
}
|
||
|
||
func (d Dialector) getOrderByColumns(stmt *gorm.Statement) string {
|
||
if orderByClause, ok := stmt.Clauses["ORDER BY"]; ok {
|
||
var orderBy clause.OrderBy
|
||
if orderBy, ok = orderByClause.Expression.(clause.OrderBy); ok && len(orderBy.Columns) > 0 {
|
||
orderByBuilder := strings.Builder{}
|
||
for i, column := range orderBy.Columns {
|
||
if i > 0 {
|
||
orderByBuilder.WriteString(", ")
|
||
}
|
||
orderByBuilder.WriteString(column.Column.Name)
|
||
if column.Desc {
|
||
orderByBuilder.WriteString(" DESC")
|
||
}
|
||
}
|
||
return orderByBuilder.String()
|
||
}
|
||
}
|
||
return "NULL"
|
||
}
|
||
|
||
func (d Dialector) getLimitRows(limit clause.Limit) (limitRows int, hasLimit bool) {
|
||
if l := limit.Limit; l != nil {
|
||
limitRows = *l
|
||
hasLimit = limitRows > 0
|
||
}
|
||
return
|
||
}
|
||
|
||
func (d Dialector) DefaultValueOf(*schema.Field) clause.Expression {
|
||
return clause.Expr{SQL: "VALUES (DEFAULT)"}
|
||
}
|
||
|
||
func (d Dialector) Migrator(db *gorm.DB) gorm.Migrator {
|
||
return Migrator{
|
||
Migrator: migrator.Migrator{
|
||
Config: migrator.Config{
|
||
DB: db,
|
||
Dialector: d,
|
||
CreateIndexAfterCreateTable: true,
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
func (d Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||
writer.WriteString(":")
|
||
writer.WriteString(strconv.Itoa(len(stmt.Vars)))
|
||
}
|
||
|
||
func (d Dialector) QuoteTo(writer clause.Writer, str string) {
|
||
if d.SkipQuoteIdentifiers {
|
||
writer.WriteString(str)
|
||
return
|
||
}
|
||
|
||
if str != "" && IsReservedWord(str) {
|
||
writer.WriteByte('"')
|
||
writer.WriteString(str)
|
||
writer.WriteByte('"')
|
||
} else {
|
||
writer.WriteString(str)
|
||
}
|
||
}
|
||
|
||
var numericPlaceholder = regexp.MustCompile(`:(\d+)`)
|
||
|
||
func (d Dialector) Explain(sql string, vars ...interface{}) string {
|
||
return logger.ExplainSQL(sql, numericPlaceholder, `'`, funk.Map(vars, func(v interface{}) interface{} {
|
||
switch v := v.(type) {
|
||
case bool:
|
||
if v {
|
||
return 1
|
||
}
|
||
return 0
|
||
default:
|
||
return v
|
||
}
|
||
}).([]interface{})...)
|
||
}
|
||
|
||
func (d Dialector) DataTypeOf(field *schema.Field) string {
|
||
delete(field.TagSettings, "RESTRICT")
|
||
|
||
var sqlType string
|
||
|
||
switch field.DataType {
|
||
case schema.Bool:
|
||
// Oracle 21c+ 支持原生 BOOLEAN 列;更早版本用 NUMBER(1) 模拟
|
||
if supportsNativeBoolean(d.DBVer) {
|
||
sqlType = "BOOLEAN"
|
||
} else {
|
||
sqlType = "NUMBER(1)"
|
||
}
|
||
case schema.Int, schema.Uint:
|
||
sqlType = "INTEGER"
|
||
if field.Size <= 8 {
|
||
sqlType = "SMALLINT"
|
||
}
|
||
// Oracle 12c+ 支持 IDENTITY 列;Oracle 11g 需要在迁移时创建序列 + 触发器
|
||
if field.AutoIncrement && supportsIdentity(d.DBVer) {
|
||
sqlType += " GENERATED BY DEFAULT AS IDENTITY"
|
||
}
|
||
case schema.Float:
|
||
sqlType = "FLOAT"
|
||
|
||
if val, ok := field.TagSettings["AUTOINCREMENT"]; ok && utils.CheckTruth(val) {
|
||
sqlType += " GENERATED BY DEFAULT AS IDENTITY"
|
||
}
|
||
case schema.String, "VARCHAR2":
|
||
size := field.Size
|
||
defaultSize := d.DefaultStringSize
|
||
|
||
if size == 0 {
|
||
if defaultSize > 0 {
|
||
size = int(defaultSize)
|
||
} else {
|
||
hasIndex := field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUE"] != ""
|
||
// TEXT, GEOMETRY or JSON column can't have a default value
|
||
if field.PrimaryKey || field.HasDefaultValue || hasIndex {
|
||
size = 191 // utf8mb4
|
||
}
|
||
}
|
||
}
|
||
|
||
// Oracle 12c+(Extended)支持最长 32767 字节的 VARCHAR2(32k 特性);
|
||
// 11g 及未开启 Extended 的库超过 4000 必须用 CLOB。
|
||
// 保守策略:
|
||
// - size 在 2000~4000 之间维持 CLOB 不变(保持历史行为)
|
||
// - size > 4000 且版本 >= 12 → VARCHAR2(size)(利用 32k 特性)
|
||
// - size > 4000 且 11g → CLOB(保持现状)
|
||
if size > 4000 {
|
||
if supportsExtendedString(d.DBVer) {
|
||
sqlType = fmt.Sprintf("VARCHAR2(%d)", size)
|
||
} else {
|
||
sqlType = "CLOB"
|
||
}
|
||
} else if size >= 2000 {
|
||
sqlType = "CLOB"
|
||
} else {
|
||
sqlType = fmt.Sprintf("VARCHAR2(%d)", size)
|
||
}
|
||
|
||
case schema.Time:
|
||
if field.Precision > 0 {
|
||
sqlType = fmt.Sprintf("TIMESTAMP(%d) WITH TIME ZONE", field.Precision)
|
||
} else {
|
||
sqlType = "TIMESTAMP WITH TIME ZONE"
|
||
}
|
||
|
||
case schema.Bytes:
|
||
sqlType = "BLOB"
|
||
default:
|
||
sqlType = string(field.DataType)
|
||
|
||
if strings.EqualFold(sqlType, "text") {
|
||
sqlType = "CLOB"
|
||
}
|
||
|
||
if sqlType == "" {
|
||
panic(fmt.Sprintf("invalid sql type %s (%s) for oracle", field.FieldType.Name(), field.FieldType.String()))
|
||
}
|
||
|
||
}
|
||
|
||
return sqlType
|
||
}
|
||
|
||
func (d Dialector) SavePoint(tx *gorm.DB, name string) error {
|
||
tx.Exec("SAVEPOINT " + name)
|
||
return tx.Error
|
||
}
|
||
|
||
func (d Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||
tx.Exec("ROLLBACK TO SAVEPOINT " + name)
|
||
return tx.Error
|
||
}
|
||
|
||
func (d Dialector) GetAdapter() driver_adapter.Adapter {
|
||
if d.DriverType == "" {
|
||
d.DriverType = driver_adapter.DriverGoOra
|
||
}
|
||
return driver_adapter.Get(d.DriverType)
|
||
}
|