Files
gf/contrib/drivers/oracle/oracle_do_insert.go
Lance Add 73211707fb refactor(container): add default nil checker, rename RegisterNilChecker to SetNilChecker, migrate instance containers to type-safe generics (#4630)
## 变更说明

本 PR 主要对代码库进行了重构,以提升类型安全性和优化连接管理实现。

### 详细变更

#### 1. 数据库连接管理优化
- 修改 `RegisterNilChecker`方法返回实例以支持链式调用,涉及
`KVMap`、`ListKVMap`、`TSet`、`AVLKVTree`、`BKVTree`、`RedBlackKVTree`
等多个容器类型
- 更新 `Core`结构体中 `links`字段类型为类型安全的 `KVMap[ConfigNode, *sql.DB]`
- 添加专门的链接检查器函数用于连接池管理
- 使用泛型 `KVMap`替代原始 map 类型提升类型安全性
- 简化连接关闭逻辑并移除不必要的类型断言
- 优化统计功能中的迭代器实现提高性能

#### 2. 数据库驱动类型安全增强
- 将 dm、gaussdb、mssql、oracle 驱动中的 `conflictKeySet` 从 `gset.New`修改为
`gset.NewStrSet`
- 统一使用字符串集合类型以提高类型安全性

#### 3. 配置文件适配器类型安全改进
- 将 `jsonMap`从 `StrAnyMap` 类型更改为泛型 `KVMap[string, *gjson.Json]` 类型
- 添加 `jsonMapChecker` 函数用于 JSON 对象验证
- 使用 `NewKVMapWithChecker` 替代 `NewStrAnyMap` 提高类型安全性
- 简化数据库链接关闭日志中的键值转换逻辑

## 影响范围

- 数据库连接管理模块
- 多个数据库驱动实现
- 配置文件管理系统

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: John Guo <john@johng.cn>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-23 16:37:38 +08:00

279 lines
8.6 KiB
Go

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package oracle
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
const (
internalPrimaryKeyInCtx gctx.StrKey = "primary_key_field"
)
// DoInsert inserts or updates data for given table.
// The list parameter must contain at least one record, which was previously validated.
func (d *Driver) DoInsert(
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
switch option.InsertOption {
case gdb.InsertOptionSave:
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionReplace:
// Oracle does not support REPLACE INTO syntax, use SAVE instead.
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionIgnore:
// Oracle does not support INSERT IGNORE syntax, use MERGE instead.
return d.doInsertIgnore(ctx, link, table, list, option)
case gdb.InsertOptionDefault:
// For default insert, set primary key field in context to support LastInsertId.
// Only set it when the primary key is not provided in the data, for performance reason.
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
if err == nil && len(list) > 0 {
for _, field := range tableFields {
if strings.EqualFold(field.Key, "pri") {
// Check if primary key is provided in the data.
pkProvided := false
for key := range list[0] {
if strings.EqualFold(key, field.Name) {
pkProvided = true
break
}
}
// Only use RETURNING when primary key is not provided, for performance reason.
if !pkProvided {
pkField := *field
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
}
break
}
}
}
default:
}
var (
keys []string
values []string
params []any
)
// Retrieve the table fields and length.
var (
listLength = len(list)
valueHolder = make([]string, 0)
)
for k := range list[0] {
keys = append(keys, k)
valueHolder = append(valueHolder, "?")
}
var (
batchResult = new(gdb.SqlResult)
charL, charR = d.GetChars()
keyStr = charL + strings.Join(keys, charL+","+charR) + charR
valueHolderStr = strings.Join(valueHolder, ",")
)
// Format "INSERT...INTO..." statement.
// Note: Use standard INSERT INTO syntax instead of INSERT ALL to ensure triggers fire
for i := 0; i < listLength; i++ {
for _, k := range keys {
if s, ok := list[i][k].(gdb.Raw); ok {
params = append(params, gconv.String(s))
} else {
params = append(params, list[i][k])
}
}
values = append(values, valueHolderStr)
// Execute individual INSERT for each record to trigger row-level triggers
r, err := d.DoExec(ctx, link, fmt.Sprintf(
"INSERT INTO %s(%s) VALUES(%s)",
table, keyStr, valueHolderStr,
), params...)
if err != nil {
return r, err
}
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
batchResult.Affected += n
}
params = params[:0]
}
return batchResult, nil
}
// doSave support upsert for Oracle
func (d *Driver) doSave(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, true)
}
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database.
// It only inserts records when there's no conflict on primary/unique keys.
func (d *Driver) doInsertIgnore(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, false)
}
// doMergeInsert implements MERGE-based insert operations for Oracle database.
// When withUpdate is true, it performs upsert (insert or update).
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
func (d *Driver) doMergeInsert(
ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
) (result sql.Result, err error) {
// If OnConflict is not specified, automatically get the primary key of the table
conflictKeys := option.OnConflict
if len(conflictKeys) == 0 {
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for table`,
)
}
foundPrimaryKey := false
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
}
if foundPrimaryKey {
break
}
}
if !foundPrimaryKey {
return nil, gerror.NewCodef(
gcode.CodeMissingParameter,
`Replace/Save/InsertIgnore operation requires conflict detection: `+
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
table,
)
}
// TODO consider composite primary keys.
conflictKeys = primaryKeys
}
var (
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be upsert
// queryValues: Handle data that need to be upsert
// insertKeys: Handle valid keys that need to be inserted
// insertValues: Handle values that need to be inserted
// updateValues: Handle values that need to be updated
queryHolders = make([]string, oneLen)
queryValues = make([]any, oneLen)
insertKeys = make([]string, oneLen)
insertValues = make([]string, oneLen)
updateValues []string
)
// conflictKeys slice type conv to set type
for _, conflictKey := range conflictKeys {
conflictKeySet.Add(gstr.ToUpper(conflictKey))
}
index := 0
for key, value := range one {
keyWithChar := charL + key + charR
queryHolders[index] = fmt.Sprintf("? AS %s", keyWithChar)
queryValues[index] = value
insertKeys[index] = keyWithChar
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
// Build updateValues only when withUpdate is true
// Filter conflict keys and soft created fields from updateValues
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
)
}
index++
}
var (
batchResult = new(gdb.SqlResult)
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
)
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
if err != nil {
return r, err
}
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
batchResult.Affected += n
}
return batchResult, nil
}
// parseSqlForMerge generates MERGE statement for Oracle database.
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
// Examples:
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
func parseSqlForMerge(table string,
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
) (sqlStr string) {
var (
queryHolderStr = strings.Join(queryHolders, ",")
insertKeyStr = strings.Join(insertKeys, ",")
insertValueStr = strings.Join(insertValues, ",")
duplicateKeyStr string
)
// Build ON condition
for index, keys := range duplicateKey {
if index != 0 {
duplicateKeyStr += " AND "
}
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
}
// Build SQL based on whether UPDATE is needed
pattern := gstr.Trim(
`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN ` +
`NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
)
if len(updateValues) > 0 {
// Upsert: INSERT or UPDATE
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
return fmt.Sprintf(
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
strings.Join(updateValues, ","),
)
}
// Insert Ignore: INSERT only
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
}