Go 数据库:对比控制器访问 DB 连接的三种方法
用 Golang 开始 web 后端编码,我遇到的最大问题之一就是 —— 如何恰到好处地将数据库连接句柄传递给控制器?本文介绍了 3 种基于应用大小和需求的可用方式。
方法 1: 使用全局变量#
新建子包 sqldb
,创建文件 db.go
。 声明一个持有数据库连接的 *sql.DB
类型全局变量 DB
。 编写一个将数据库连接赋值给该全局变量的函数。
package sqldb
import "database/sql"
// DB 全局变量,存储数据库连接句柄
var DB *sql.DB
// 打开数据库连接
func ConnectDB() {
db, err := sql.Open("mysql", "username:password@/dbname")
if err != nil {
panic(err.Error())
}
DB = db
}
现在在需要使用数据库时,你可以简单地使用全局变量。
package controllers
import (
"fmt"
"net/http"
"github.com/techinscribed/global-db/sqldb"
)
// HelloWorld returns Hello, World
func HelloWorld(w http.ResponseWriter, r *http.Request) {
if err := sqldb.DB.Ping(); err != nil {
fmt.Println("DB Error")
}
w.Write([]byte("Hello, World"))
}
这是将数据库连接 sql.DB 传递给控制器最简单的方法,但是不是一个完美的方法。如果编写一个小型程序,可以继续使用这个方法,如果你要写一个严谨的程序,请不要使用全局变量。
优点:#
1. 快速简单。
缺点:#
1. 应用程序的任何部分都可以访问数据库。
2. 编写测试时很难 mock 一个数据库连接。
3. 程序无法切换到其他数据库。
你可以在 Github 上找到这个演示代码.
方法 2: 创建结构体保存数据库连接#
我们会更新 db.go
的代码内容,用来创建一个新的数据库连接,而不是将连接保存到全局变量。
package sqldb
import "database/sql"
// ConnectDB 方法打开一个数据库连接
func ConnectDB() *sql.DB {
db, err := sql.Open("mysql", "username:password@/dbname")
if err != nil {
panic(err.Error())
}
return db
}
在控制器中,我们可以创建一个 BaseHandler
结构体来保存控制器需要使用的对象,包括数据库连接。然后编写这个结构体的处理请求的方法。
package controllers
import (
"database/sql"
"fmt"
"net/http"
)
// BaseHandler 会保存所有需要使用的对象,例如*sql.DB数据库池
type BaseHandler struct {
db *sql.DB
}
// NewBaseHandler 方法创建一个*BaseHandler对象
func NewBaseHandler(db *sql.DB) *BaseHandler {
return &BaseHandler{
db: db,
}
}
// HelloWorld 方法会返回响应内容是"Hello, World"的http响应。
func (h *BaseHandler) HelloWorld(w http.ResponseWriter, r *http.Request) {
if err := h.db.Ping(); err != nil {
fmt.Println("DB Error")
}
w.Write([]byte("Hello, World"))
}
最后在 main 函数中,我们可以将数据库 sql.DB
和控制器绑定在一起。
package main
import (
"fmt"
"net/http"
"github.com/techinscribed/struct-db/controllers"
"github.com/techinscribed/struct-db/sqldb"
)
func main() {
db := sqldb.ConnectDB()
h := controllers.NewBaseHandler(db)
http.HandleFunc("/", h.HelloWorld)
s := &http.Server{
Addr: fmt.Sprintf("%s:%s", "localhost", "5000"),
}
s.ListenAndServe()
}
优点:#
- 这个请求只能从控制器去访问数据库。
- 没有全局的数据库连接池。
- 可以轻松简单的编写 mock 测试。
缺点:#
- 控制器不能切换成其他的数据库实例连接。
你可以在 Github 上找到这个例子的代码.
方法 3:用接口去操作 Model#
我们可以为每个 Model
定义一个使用接口。
例如:
package models
// User ..
type User struct {
Name string
}
// UserRepository ..
type UserRepository interface {
FindByID(ID int) (*User, error)
Save(user *User) error
}
然后在 BaseHandler
结构体中不使用数据库连接,而是使用 models.UserRepository
接口。
package controllers
import (
"fmt"
"net/http"
"github.com/techinscribed/repository-db/models"
)
// BaseHandler 会报错需要使用的modele对象
type BaseHandler struct {
userRepo models.UserRepository
}
// NewBaseHandler 函数创建一个*BaseHandler对象
func NewBaseHandler(userRepo models.UserRepository) *BaseHandler {
return &BaseHandler{
userRepo: userRepo,
}
}
// HelloWorld 方法返回http处理响应结果。
func (h *BaseHandler) HelloWorld(w http.ResponseWriter, r *http.Request) {
if user, err := h.userRepo.FindByID(1); err != nil {
fmt.Println("Error", user)
}
w.Write([]byte("Hello, World"))
}
然后我们只要实现 models.UserRepository
接口,使 Model 满足接口实现即可,与我们使用哪个数据库都没有影响!
package repositories
import (
"database/sql"
"github.com/techinscribed/repository-db/models"
)
// UserRepo implements models.UserRepository
type UserRepo struct {
db *sql.DB
}
// NewUserRepo ..
func NewUserRepo(db *sql.DB) *UserRepo {
return &UserRepo{
db: db,
}
}
// FindByID ..
func (r *UserRepo) FindByID(ID int) (*models.User, error) {
return &models.User{}, nil
}
// Save ..
func (r *UserRepo) Save(user *models.User) error {
return nil
}
最后在 main 函数中将前面的所有内容都组合起来
package main
import (
"fmt"
"net/http"
"github.com/techinscribed/repository-db/controllers"
"github.com/techinscribed/repository-db/repositories"
"github.com/techinscribed/repository-db/sqldb"
)
func main() {
db := sqldb.ConnectDB()
// Create repos
userRepo := repositories.NewUserRepo(db)
h := controllers.NewBaseHandler(userRepo)
http.HandleFunc("/", h.HelloWorld)
s := &http.Server{
Addr: fmt.Sprintf("%s:%s", "localhost", "5000"),
}
s.ListenAndServe()
}
根据使用环境(测试、开发、生产等),我们可以将不同的 models.UserRepository
传递给我们的控制器。
示例:我们可以根据环境编写一个单独的 models.UserRepository
实现,在测试环境中使用的 JSON/XML 文件实现,而开发和生产环境可以使用 SQL 实现。在以后甚至可以根据需求完全切换成 NoSQL 实现。
优点:#
1. 这个请求只能从控制器去访问数据库。
2. 没有全局的数据库连接池。
3. 可以轻松简单的编写 mock 测试。
4. 轻松切换成其他数据库连接。
缺点:#
1. 需要写更多的代码。
你可以在 Github 上找到这个演示代码.
结论#
像我前面提到的,选择哪种方法根据自己的规模和要求。方法 1 适合小型程序,方法 2 适合 MVC 程序,并且需要不会修改数据库连接,方法 3 适合基于域驱动设计构建的程序,因此您可以为每个控制器绑定定义一个的独立去环境。
如果您知道或使用其他方法,请在评论中告诉我知道。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: