2026/4/17 19:38:51
网站建设
项目流程
做汉字词卡的网站,搜seo,建设部网站有建筑施工分包,上海建网站我来详细介绍如何使用 GORM 调用 PostgreSQL 存储过程。GORM 本身不直接支持 CALL 语法#xff0c;但可以通过 Exec 和 Raw 方法实现。
一、基础配置
go package main
import ( fmt log gorm.io/driver/postgres …我来详细介绍如何使用 GORM 调用 PostgreSQL 存储过程。GORM 本身不直接支持 CALL 语法但可以通过 Exec 和 Raw 方法实现。一、基础配置gopackage mainimport (fmtloggorm.io/driver/postgresgorm.io/gormgorm.io/gorm/logger)func initDB() *gorm.DB {dsn : hostlocalhost userpostgres passwordsecret dbnamemydb port5432 sslmodedisabledb, 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 执行 CALLresult : 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, $2result : db.Exec(CALL sp_user_register(?, ?, ?),john_doe, // 用户名johnexample.com, // 邮箱hashed_password, // 密码)// 方式2使用命名参数需要 GORM 1.20result db.Exec(CALL sp_user_register(username, email, password),sql.Named(username, jane_doe),sql.Named(email, janeexample.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 plpgsqlAS $$BEGINSELECT COUNT(*), COALESCE(SUM(amount), 0)INTO total_orders, total_amountFROM ordersWHERE user_id p_user_id;END;$$;gotype UserStats struct {TotalOrders int gorm:column:total_ordersTotalAmount 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).Errorif err ! nil {return nil, err}return stats, nil}4.2 使用 Row 获取单个值go// 调用返回单个值的存储过程func callWithSingleOutput(db *gorm.DB, userID int) (int, error) {var count introw : 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 plpgsqlAS $$BEGINRETURN QUERYSELECT u.id, u.username, u.email, u.created_atFROM users uWHERE u.status p_status;END;$$;gotype User struct {ID int gorm:column:idUsername string gorm:column:usernameEmail string gorm:column:emailCreatedAt time.Time gorm:column:created_at}// 调用返回结果集的函数func callWithResultSet(db *gorm.DB, status string) ([]User, error) {var users []Usererr : db.Raw(SELECT * FROM sp_get_users(?), status).Scan(users).Errorif 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 plpgsqlAS $$BEGINOPEN result FORSELECT id, username, emailFROM usersWHERE status p_status;END;$$;go// GORM 中处理游标需要原生 SQL 事务func callWithCursor(db *gorm.DB, status string) ([]User, error) {var users []User// 使用 Transaction 获取 *sql.Txerr : db.Transaction(func(tx *gorm.DB) error {// 获取底层 *sql.DBsqlDB, err : tx.DB()if err ! nil {return err}// 开启原生事务游标必须在事务中sqlTx, err : sqlDB.Begin()if err ! nil {return err}defer sqlTx.Rollback()// 调用存储过程获取游标名var cursorName stringerr 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 Userif err : rows.Scan(u.ID, u.Username, u.Email); err ! nil {return err}users append(users, u)}return sqlTx.Commit()})return users, err}六、完整封装层gopackage repositoryimport (contextdatabase/sqlfmtgorm.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 UserStatserr : 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)})}七、使用示例gofunc main() {db : initDB()caller : repository.NewProcedureCaller(db)// 1. 简单调用if err : caller.UserRegister(alice, aliceexample.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()).Errorif 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.DBfunc hybridExample(db *gorm.DB) {// 获取原生 *sql.DBsqlDB, err : db.DB()if err ! nil {log.Fatal(err)}// 使用原生接口调用复杂存储过程_, err sqlDB.Exec(CALL sp_complex_procedure($1, $2), arg1, arg2)// 继续使用 GORM 进行 ORM 操作var users []Userdb.Where(status ?, active).Find(users)}需要我针对具体的业务场景如分页查询、批量导入、复杂报表提供更详细的 GORM 存储过程集成方案吗