云计算百科
云计算领域专业知识百科平台

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 + 存储过程集成方案吗?

赞(0)
未经允许不得转载:网硕互联帮助中心 » gorm调用postgresql存贮过程
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!