news 2026/4/12 7:13:16

gorm调用postgresql存贮过程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gorm调用postgresql存贮过程

我来详细介绍如何使用 GORM 调用 PostgreSQL 存储过程。GORM 本身不直接支持 `CALL` 语法,但可以通过 `Exec` 和 `Raw` 方法实现。

一、基础配置

```go
package main

import (
"fmt"
"log"

"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func initDB() *gorm.DB {
dsn := "host=localhost user=postgres password=secret dbname=mydb port=5432 sslmode=disable"

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal("数据库连接失败:", err)
}

return db
}
```

二、调用无参存储过程

```go
// 调用无参数、无返回值的存储过程
func callSimpleProcedure(db *gorm.DB) error {
// 使用 Exec 执行 CALL
result := db.Exec("CALL sp_cleanup_logs()")
if result.Error != nil {
return result.Error
}

fmt.Printf("影响行数: %d\n", result.RowsAffected)
return nil
}
```

三、调用带输入参数的存储过程

```go
// 调用带输入参数的存储过程
func callWithParams(db *gorm.DB) error {
// 方式1:使用 ? 占位符(GORM 会自动转换为 $1, $2)
result := db.Exec(
"CALL sp_user_register(?, ?, ?)",
"john_doe", // 用户名
"john@example.com", // 邮箱
"hashed_password", // 密码
)

// 方式2:使用命名参数(需要 GORM 1.20+)
result = db.Exec(
"CALL sp_user_register(@username, @email, @password)",
sql.Named("username", "jane_doe"),
sql.Named("email", "jane@example.com"),
sql.Named("password", "secret123"),
)

return result.Error
}
```

四、调用带输出参数的存储过程

4.1 使用 `Raw` + `Scan` 获取输出

```sql
-- PostgreSQL 存储过程:带 OUT 参数
CREATE OR REPLACE PROCEDURE sp_get_user_stats(
IN p_user_id INT,
OUT total_orders INT,
OUT total_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*), COALESCE(SUM(amount), 0)
INTO total_orders, total_amount
FROM orders
WHERE user_id = p_user_id;
END;
$$;
```

```go
type UserStats struct {
TotalOrders int `gorm:"column:total_orders"`
TotalAmount float64 `gorm:"column:total_amount"`
}

// 调用带 OUT 参数的存储过程
func callWithOutput(db *gorm.DB, userID int) (*UserStats, error) {
var stats UserStats

// 使用 Raw + Scan 获取 OUT 参数
err := db.Raw(
"CALL sp_get_user_stats(?)",
userID,
).Scan(&stats).Error

if err != nil {
return nil, err
}

return &stats, nil
}
```

4.2 使用 `Row` 获取单个值

```go
// 调用返回单个值的存储过程
func callWithSingleOutput(db *gorm.DB, userID int) (int, error) {
var count int

row := db.Raw("CALL sp_get_order_count(?)", userID).Row()
err := row.Scan(&count)

return count, err
}
```

五、调用返回结果集的存储过程

5.1 返回 TABLE 类型(推荐)

```sql
-- 创建返回 TABLE 的函数(GORM 对函数支持更好)
CREATE OR REPLACE FUNCTION sp_get_users(p_status VARCHAR)
RETURNS TABLE (
id INT,
username VARCHAR,
email VARCHAR,
created_at TIMESTAMP
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT u.id, u.username, u.email, u.created_at
FROM users u
WHERE u.status = p_status;
END;
$$;
```

```go
type User struct {
ID int `gorm:"column:id"`
Username string `gorm:"column:username"`
Email string `gorm:"column:email"`
CreatedAt time.Time `gorm:"column:created_at"`
}

// 调用返回结果集的函数
func callWithResultSet(db *gorm.DB, status string) ([]User, error) {
var users []User

err := db.Raw("SELECT * FROM sp_get_users(?)", status).Scan(&users).Error
if err != nil {
return nil, err
}

return users, nil
}
```

5.2 处理 REFCURSOR(游标)

```sql
-- 返回游标的存储过程
CREATE OR REPLACE PROCEDURE sp_get_users_cursor(
IN p_status VARCHAR,
OUT result REFCURSOR
)
LANGUAGE plpgsql
AS $$
BEGIN
OPEN result FOR
SELECT id, username, email
FROM users
WHERE status = p_status;
END;
$$;
```

```go
// GORM 中处理游标(需要原生 SQL 事务)
func callWithCursor(db *gorm.DB, status string) ([]User, error) {
var users []User

// 使用 Transaction 获取 *sql.Tx
err := db.Transaction(func(tx *gorm.DB) error {
// 获取底层 *sql.DB
sqlDB, err := tx.DB()
if err != nil {
return err
}

// 开启原生事务(游标必须在事务中)
sqlTx, err := sqlDB.Begin()
if err != nil {
return err
}
defer sqlTx.Rollback()

// 调用存储过程获取游标名
var cursorName string
err = sqlTx.QueryRow("CALL sp_get_users_cursor($1)", status).Scan(&cursorName)
if err != nil {
return err
}

// 从游标读取数据
rows, err := sqlTx.Query(fmt.Sprintf("FETCH ALL IN %s", cursorName))
if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Username, &u.Email); err != nil {
return err
}
users = append(users, u)
}

return sqlTx.Commit()
})

return users, err
}
```

六、完整封装层

```go
package repository

import (
"context"
"database/sql"
"fmt"

"gorm.io/gorm"
)

type ProcedureCaller struct {
db *gorm.DB
}

func NewProcedureCaller(db *gorm.DB) *ProcedureCaller {
return &ProcedureCaller{db: db}
}

// Call 通用无返回调用
func (p *ProcedureCaller) Call(procName string, args ...interface{}) error {
placeholders := generatePlaceholders(len(args))
sql := fmt.Sprintf("CALL %s(%s)", procName, placeholders)

return p.db.Exec(sql, args...).Error
}

// CallWithResult 调用并扫描到结构体
func (p *ProcedureCaller) CallWithResult(dest interface{}, procName string, args ...interface{}) error {
placeholders := generatePlaceholders(len(args))
sql := fmt.Sprintf("CALL %s(%s)", procName, placeholders)

return p.db.Raw(sql, args...).Scan(dest).Error
}

// CallInTransaction 事务中调用多个存储过程
func (p *ProcedureCaller) CallInTransaction(fn func(*ProcedureCaller) error) error {
return p.db.Transaction(func(tx *gorm.DB) error {
caller := &ProcedureCaller{db: tx}
return fn(caller)
})
}

// Context 支持上下文
func (p *ProcedureCaller) WithContext(ctx context.Context) *gorm.DB {
return p.db.WithContext(ctx)
}

func generatePlaceholders(n int) string {
if n == 0 {
return ""
}
result := "?"
for i := 1; i < n; i++ {
result += ", ?"
}
return result
}

// ==================== 业务方法 ====================

// UserRegister 用户注册
func (p *ProcedureCaller) UserRegister(username, email, password string) error {
return p.Call("sp_user_register", username, email, password)
}

// GetUserStats 获取用户统计
func (p *ProcedureCaller) GetUserStats(userID int) (*UserStats, error) {
var stats UserStats
err := p.CallWithResult(&stats, "sp_get_user_stats", userID)
return &stats, err
}

// BatchTransfer 批量转账(事务)
func (p *ProcedureCaller) BatchTransfer(fromID, toID int, amount float64) error {
return p.CallInTransaction(func(tx *ProcedureCaller) error {
// 扣款
if err := tx.Call("sp_deduct_balance", fromID, amount); err != nil {
return fmt.Errorf("扣款失败: %w", err)
}

// 加款
if err := tx.Call("sp_add_balance", toID, amount); err != nil {
return fmt.Errorf("加款失败: %w", err)
}

// 记录日志
return tx.Call("sp_transfer_log", fromID, toID, amount)
})
}
```

七、使用示例

```go
func main() {
db := initDB()
caller := repository.NewProcedureCaller(db)

// 1. 简单调用
if err := caller.UserRegister("alice", "alice@example.com", "pass123"); err != nil {
log.Fatal(err)
}

// 2. 获取输出参数
stats, err := caller.GetUserStats(1001)
if err != nil {
log.Fatal(err)
}
fmt.Printf("订单数: %d, 总金额: %.2f\n", stats.TotalOrders, stats.TotalAmount)

// 3. 事务调用
if err := caller.BatchTransfer(1001, 1002, 500.00); err != nil {
log.Fatal("转账失败:", err)
}

// 4. 上下文支持
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err = caller.WithContext(ctx).Exec("CALL sp_long_running()").Error
if err != nil {
log.Fatal(err)
}
}
```

八、注意事项与最佳实践

注意点 解决方案
GORM 不支持 `CALL` 使用 `Exec` 或 `Raw` 直接执行 SQL
输出参数获取 使用 `Raw().Scan()` 或 `Row().Scan()`
事务中的存储过程 使用 `db.Transaction()` 或原生 `sql.Tx`
游标处理 需要降级到原生 `database/sql` 接口
批量操作 使用 GORM 的 `CreateInBatches` 替代存储过程循环
错误处理 PostgreSQL `RAISE EXCEPTION` 会正常返回 error

九、GORM 与原生 SQL 混合使用

```go
// 当 GORM 无法满足时,获取原生 *sql.DB
func hybridExample(db *gorm.DB) {
// 获取原生 *sql.DB
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}

// 使用原生接口调用复杂存储过程
_, err = sqlDB.Exec("CALL sp_complex_procedure($1, $2)", arg1, arg2)

// 继续使用 GORM 进行 ORM 操作
var users []User
db.Where("status = ?", "active").Find(&users)
}
```

需要我针对具体的业务场景(如分页查询、批量导入、复杂报表)提供更详细的 GORM + 存储过程集成方案吗?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/10 18:34:11

不只是SFT:Qwen2.5-7B后续微调路径规划建议

不只是SFT&#xff1a;Qwen2.5-7B后续微调路径规划建议 在大模型落地实践中&#xff0c;很多人把“完成一次LoRA微调”当作终点——输入几条身份数据、跑完训练、验证效果&#xff0c;任务就算完成了。但真实工程场景中&#xff0c;这往往只是起点。当你把微调后的Qwen2.5-7B部…

作者头像 李华
网站建设 2026/4/3 6:36:46

老旧电脑重生记:如何用Tiny11Builder打造轻量级Windows 11系统

老旧电脑重生记&#xff1a;如何用Tiny11Builder打造轻量级Windows 11系统 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder 当你的旧电脑运行Windows 11时频繁卡顿…

作者头像 李华
网站建设 2026/4/9 8:33:56

Qwen3-4B-Instruct教育场景应用:智能答疑系统搭建完整指南

Qwen3-4B-Instruct教育场景应用&#xff1a;智能答疑系统搭建完整指南 1. 为什么教育场景特别需要Qwen3-4B-Instruct&#xff1f; 你有没有遇到过这样的情况&#xff1a;学生在课后反复问同一个知识点&#xff0c;老师却没时间一一回复&#xff1b;在线课程平台的讨论区里&am…

作者头像 李华
网站建设 2026/4/7 17:25:34

Live Avatar gradio_single_gpu.sh脚本解析:单卡运行要点

Live Avatar gradio_single_gpu.sh脚本解析&#xff1a;单卡运行要点 1. Live Avatar模型背景与硬件现实 Live Avatar是由阿里联合高校开源的数字人生成模型&#xff0c;聚焦于高质量、低延迟的实时视频生成能力。它基于14B参数规模的Wan2.2-S2V架构&#xff0c;融合DiT&…

作者头像 李华
网站建设 2026/3/28 9:51:39

GPEN如何控制生成强度?噪声注入参数调节教程

GPEN如何控制生成强度&#xff1f;噪声注入参数调节教程 你有没有试过用GPEN修复一张老照片&#xff0c;结果发现修复后的脸太“光滑”、缺乏真实纹理&#xff0c;或者相反——细节太多、显得生硬不自然&#xff1f;这其实不是模型能力不够&#xff0c;而是你还没掌握那个最关…

作者头像 李华
网站建设 2026/4/11 15:21:06

Qwen3-Embedding-4B显存优化:混合精度推理部署案例

Qwen3-Embedding-4B显存优化&#xff1a;混合精度推理部署案例 1. Qwen3-Embedding-4B介绍 Qwen3 Embedding 模型系列是 Qwen 家族最新推出的专用嵌入模型&#xff0c;不是通用大语言模型的副产品&#xff0c;而是从训练目标、数据配比到架构设计都围绕“向量表征质量”深度定…

作者头像 李华