我来详细介绍如何使用 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 + 存储过程集成方案吗?