数据库连接在第一个请求后关闭。

huangapple go评论93阅读模式
英文:

Database connection closes after the first request

问题

我在golang上编写了一个API,并遇到了一个错误。在一个请求之后,服务器返回一个错误,表示SQL数据库已关闭。我想通过上下文传递数据库连接。

main.go

func main() {
	app := fiber.New()

	db, err := sqlx.Connect("pgx", os.Getenv("POSTGRESQL_URL"))

	if err != nil {
		panic(err)
	}

	if err = db.Ping(); err != nil {
		panic(err)
	}

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)
	db.SetConnMaxIdleTime(5 * time.Minute)

	defer db.Close()

	configure_router.ConfigureRouter(app, db)

	if err = app.Listen(os.Getenv("PORT")); err != nil {
		log.Fatalln(err)
	}
}

configure_router.go

func ConfigureRouter(app *fiber.App, db *sqlx.DB) {
	//Middlewares
	app.Use(logger.New(logger.Config{
		Format: "[${ip}]:${port} ${time} ${status} - ${method} ${path}\n",
	}))

	app.Use(cors.New(cors.Config{
		//AllowOrigins: "http://localhost:3000",
		AllowHeaders: "Origin, Content-Type, Accept",
	}))

	app.Use("/api", func(ctx *fiber.Ctx) error {
		ctx.Context().SetUserValue("dbConn", db)
		return ctx.Next()
	})

	//Authentication endpoints
	app.Post("api/register", register.Register)
	app.Post("api/auth/login", login.Login)
}

register.go

func Register(ctx *fiber.Ctx) error {
	conn := ctx.Context().UserValue("dbConn").(*sqlx.DB)

	var in In

	if err := ctx.BodyParser(&in); err != nil {
		return make_response.MakeInfoResponse(ctx, fiber.StatusUnprocessableEntity, 1, err.Error())
	}

	if in.Email == "" || in.Password == "" {
		return make_response.MakeInfoResponse(ctx, fiber.StatusBadRequest, 1, "Incorrect data input")
	}

	elementExist := false
	err := conn.Get(&elementExist, "select exists(select email from users where email = $1)", in.Email)

    // 这里程序在第二个请求中出错
    if err != nil {
		return make_response.MakeInfoResponse(ctx, fiber.StatusInternalServerError, 1, err.Error())
	}

	if elementExist {
		return make_response.MakeInfoResponse(ctx, fiber.StatusBadRequest, 1, "User already registered!")
	}

	passwordHash, err := hash_passwords.HashPassword(in.Password)
	if err != nil {
		return err
	}

	_, err = conn.Exec("insert into users (email, password_hash) values ($1, $2)", in.Email, passwordHash)

	if err != nil {
		return make_response.MakeInfoResponse(ctx, fiber.StatusInternalServerError, 1, err.Error())
	}

	return make_response.MakeInfoResponse(ctx, fiber.StatusOK, 0, "Registration was successful!")
}

如果我在/api/register发送请求,并且用户在第一个请求中已经注册,我会得到以下响应:

请求:

{
  "email": "test@gmail.com",
  "password": "123123123"
}

第一个响应:

{
   "error_code": 0,
   "message": "User already registered!"
}

但是,如果我想发送另一个请求,我会得到以下响应:

{
   "error_code": 1,
   "message": "sql: database is closed"
}
英文:

I write APi on golang and i encountered an error. After one request server return error that sql database is closed. I want to transfer the database connection through the context.

main.go

func main() {
	app := fiber.New()

	db, err := sqlx.Connect("pgx", os.Getenv("POSTGRESQL_URL"))

	if err != nil {
		panic(err)
	}

	if err = db.Ping(); err != nil {
		panic(err)
	}

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)
	db.SetConnMaxIdleTime(5 * time.Minute)

	defer db.Close()

	configure_router.ConfigureRouter(app, db)

	if err = app.Listen(os.Getenv("PORT")); err != nil {
		log.Fatalln(err)
	}
}

configure_router.go

func ConfigureRouter(app *fiber.App, db *sqlx.DB) {
	//Middlewares
	app.Use(logger.New(logger.Config{
		Format: "[${ip}]:${port} ${time} ${status} - ${method} ${path}\n",
	}))

	app.Use(cors.New(cors.Config{
		//AllowOrigins: "http://localhost:3000",
		AllowHeaders: "Origin, Content-Type, Accept",
	}))

	app.Use("/api", func(ctx *fiber.Ctx) error {
		ctx.Context().SetUserValue("dbConn", db)
		return ctx.Next()
	})

	//Authentication endpoints
	app.Post("api/register", register.Register)
	app.Post("api/auth/login", login.Login)
}

register.go

func Register(ctx *fiber.Ctx) error {
	conn := ctx.Context().UserValue("dbConn").(*sqlx.DB)

	var in In

	if err := ctx.BodyParser(&in); err != nil {
		return make_response.MakeInfoResponse(ctx, fiber.StatusUnprocessableEntity, 1, err.Error())
	}

	if in.Email == "" || in.Password == "" {
		return make_response.MakeInfoResponse(ctx, fiber.StatusBadRequest, 1, "Incorrect data input")
	}

	elementExist := false
	err := conn.Get(&elementExist, "select exists(select email from users where email = $1)", in.Email)

    // Here programm fall in second request
    if err != nil {
		return make_response.MakeInfoResponse(ctx,fiber.StatusInternalServerError, 1, err.Error())
	}


	if elementExist {
		return make_response.MakeInfoResponse(ctx, fiber.StatusBadRequest, 1, "User already registered!")
	}

	passwordHash, err := hash_passwords.HashPassword(in.Password)
	if err != nil {
		return err
	}

	_, err = conn.Exec("insert into users (email, password_hash) values ($1, $2)", in.Email, passwordHash)

	if err != nil {
		return make_response.MakeInfoResponse(ctx, fiber.StatusInternalServerError, 1, err.Error())
	}

	return make_response.MakeInfoResponse(ctx, fiber.StatusOK, 0, "Registration was successful!")
}

If i send request in /api/register and user already registered in first request i get

Requst:

{
  "email": "test@gmail.com",
  "password": "123123123"
}

First response:

{
   "error_code": 0,
   "message": "User already registered!"
}

But if i will want send another one request i will get:

{
   "error_code": 1,
   "message": "sql: database is closed",
}

答案1

得分: 1

我想通过上下文传递数据库连接。

不要这样做。这不仅是不好的实践,而且实际上是fasthttp.RequestCtx在每个请求之后关闭了你的数据库连接。上下文应该只保存与请求相关的值。一个全局的数据库连接很难是请求特定的。

请参考SetUserValue的文档,特别是最后一段:

在从顶级RequestHandler返回后,所有的值都会从ctx中移除。此外,在从ctx中移除值之前,会调用每个实现了io.Closer接口的值的Close方法。


一个快速的修复方法是在闭包中捕获数据库连接:

func Register(db *sqlx.DB) (fn func(*fiber.Ctx) error) {
    return func(ctx *fiber.Ctx) error {
        // ...
        elementExist := false
        err := db.Get(&elementExist, "select exists(select email from users where email = $1)", in.Email)
        // ...
    }
}

// ...

// 删除或注释掉这部分
// app.Use("/api", func(ctx *fiber.Ctx) error {
//    ctx.Context().SetUserValue("dbConn", db)
//    return ctx.Next()
// })

app.Post("api/register", register.Register(db))
英文:

> I want to transfer the database connection through the context.

Don't. Not only is it bad practice, but it is actually the fasthttp.RequestCtx itself that is closing your db after each request. Contexts should hold ONLY request specific values. A global db connection is hardly request-specific.

See the docs of SetUserValue, specifically the last paragraph:

> All the values are removed from ctx after returning from the top RequestHandler. Additionally, Close method is called on each value implementing io.Closer before removing the value from ctx.


A quick fix would be to capture the db in a closure:

func Register(db *sqlx.DB) (fn func(*fiber.Ctx) error) {
    return func(ctx *fiber.Ctx) error {
        // ...
        elementExist := false
        err := db.Get(&elementExist, "select exists(select email from users where email = $1)", in.Email)
        // ...
    }
}

// ...

// delete this or comment it out
// app.Use("/api", func(ctx *fiber.Ctx) error {
//    ctx.Context().SetUserValue("dbConn", db)
//    return ctx.Next()
// })

app.Post("api/register", register.Register(db))

huangapple
  • 本文由 发表于 2023年3月1日 16:59:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/75601481.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定