Go 数据库:对比控制器访问 DB 连接的三种方法

Go

用 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()

}

优点:#

  1. 这个请求只能从控制器去访问数据库。
  2. 没有全局的数据库连接池。
  3. 可以轻松简单的编写 mock 测试。

缺点:#

  1. 控制器不能切换成其他的数据库实例连接。

你可以在 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://techinscribed.com/different-appr...

译文地址:https://learnku.com/go/t/50981

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议