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

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

这份整理对我的帮助很大。

3年前 评论

:+1: 第三种方法,和Java的写法很相似

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!