我来详细介绍如何使用 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 + 存储过程集成方案吗?
网硕互联帮助中心







评论前必须登录!
注册