likes
comments
collection
share

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

作者站长头像
站长
· 阅读数 14

flag:每月至少产出三篇高质量文章~

在之前已经基于 React18+TS4.x+Webpack5 从0到1搭建了一个 React 基本项目架子,并在 npm 上发布了我们的脚手架,具体的步骤见下面四篇:

接下来,我将用几篇文章介绍如何基于 Go 语言搭建一个后端的基础架子。然后前后端同步进行开发,后端服务基于 Gin + Gorm + Casbin,前端则是基于 React + antd,开发一套常见的基于 RBAC 权限控制的前后端分离的全栈管理后台项目,手把手带你入门前后端开发。第一篇:

已经完成,接下来进入第二篇,将实现数据库连接:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

本文相关代码在:gin_common_web_server - branch:cha-02

1、什么是数据库?

这个问题可能对后端同学比较熟悉了,但是对大多数从未接触过后端开发的前端同学来说,可能只停留在了解它的阶段。数据库通常分为层次式数据库、网络式数据库和关系式数据库三种。而不同的数据库是按不同的数据结构来联系和组织的。而在当今的互联网中,最常见的数据库模型主要是两种,即关系型数据库和非关系型数据库。

数据库是一个可以很简单、但也可以很高深的领域,鉴于本系列的目的,下面只介绍几种常见的数据库分类及其使用场景:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

  1. 关系型数据库适合存储结构化数据,比如用户的账号信息等:

    • 这些数据通常需要做结构化查询,比如 join,这时候关系型数据库就要略胜一筹
    • 这些数据的规模、增长的速度通常可以预期
    • 能保证数据的事务性、一致性要求
  2. 非关系型数据库适合存储非结构化数据,比如微博、博客文章、用户评论等:

    • 这些数据通常用于模糊处理,比如全文检索、机器学习
    • 这些数据是海量的,而且增长的速度是难以预期的
    • 通常是无限(至少接近)伸缩性的
    • 按key获取数据效率很高,但对join或其他结构化查询的支持就比较差

接下来,我们将在项目实现 Go 连接 MySQLPostgreSQL 这两种数据库。

2、Go 连接 MySQL

在本项目中,将使用 Gorm 作为 orm,它的特性(来自官方网站):

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

安装:

go get -u gorm.io/gorm 
go get -u gorm.io/driver/sqlite 

同时你还要保证你的电脑已经安装了MySQL数据库,具体的方法我就不详细介绍了,这都是基操了。这里给出两个链接大家自行安装:

2.1 定义MySQL配置

和之前的日志配置一样,在根目录下的 config.yaml 中定义好我们自定义的 MySQL 配置:

# ...

mysql: # MySQL 配置
  host: 127.0.0.1 # 服务器地址
  port: "3306" # 端口
  config: charset=utf8mb4&parseTime=True&loc=Local # 其他配置
  db_name: east_white_admin_server # 数据库名称
  username: root # 数据库用户名
  password: "123456" # 数据库密码
  prefix: "t_" # 全局表前缀,单独定义 TableName 则不生效
  singular: false # 是否开启全局禁用复数,true表示不开启
  engine: "" # 引擎,默认InnoDB
  max_idle_conns: 10 # 最大空闲连接数
  max_open_conns: 100 # 最大连接数
  log_mode: error # 日志级别
  log_zap: false # 是否通过zap写日志文件

接下来,需要用结构体对配置进行解析,然后将其挂载到全局变量上。在 config 下新建 gorm_mysql.go,定义如下的结构体:

package config

type MySQL struct {
   Host         string `mapstructure:"host" json:"host" yaml:"host"`                               // 服务器地址:端口
   Port         string `mapstructure:"port" json:"port" yaml:"port"`                               //:端口
   Config       string `mapstructure:"config" json:"config" yaml:"config"`                         // 高级配置
   Dbname       string `mapstructure:"db_name" json:"db_name" yaml:"db_name"`                      // 数据库名
   Username     string `mapstructure:"username" json:"username" yaml:"username"`                   // 数据库用户名
   Password     string `mapstructure:"password" json:"password" yaml:"password"`                   // 数据库密码
   Prefix       string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`                         // 全局表前缀,单独定义TableName则不生效
   Singular     bool   `mapstructure:"singular" json:"singular" yaml:"singular"`                   // 是否开启全局禁用复数,true表示开启
   Engine       string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"`        // 数据库引擎,默认InnoDB
   MaxIdleConns int    `mapstructure:"max_idle_conns" json:"max_idle_conns" yaml:"max_idle_conns"` // 空闲中的最大连接数
   MaxOpenConns int    `mapstructure:"max_open_conns" json:"max_open_conns" yaml:"max_open_conns"` // 打开到数据库的最大连接数
   LogMode      string `mapstructure:"log_mode" json:"log_mode" yaml:"log_mode"`                   // 是否开启Gorm全局日志
   LogZap       bool   `mapstructure:"log_zap" json:"log_zap" yaml:"log_zap"`                      // 是否通过zap写入日志文件
}

func (m *MySQL) Dsn() string {
   return m.Username + ":" + m.Password + "@tcp(" + m.Host + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config
}

func (m *MySQL) GetLogMode() string {
   return m.LogMode
}

解释一下里面为什么要定义 Dsn 这个方法:它用于返回一个 MySQL 连接的 DSN(数据源名称),包括用户名、密码、主机、端口、数据库以及其他配置信息。这个方法只能通过一个MySQL类型的实例进行调用,因此将其与该类型关联并使用(*MySQL)作为接收器来声明该函数。

这么做的好处是:将这个方法与MySQL 结构体 相关联可以更好地组织代码,并避免对外暴露不必要的细节和实现的复杂性。这还使得代码更加易于维护和扩展,因为它涉及到 MySQL 连接的所有信息都被封装在一个地方了,而不是散落在程序的各个地方。

同理 GetLogMode 方法也类似。

然后也需要和zap一样,将其统一配置在 config.go 中:

package config

type Configuration struct {
   App   App   `mapstructure:"app" json:"app" yaml:"app"`
   Zap   Zap   `mapstructure:"zap" json:"zap" yaml:"zap"`
   MySQL MySQL `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
}

2.2 挂载到全局

为什么要做这个看似多余的操作?随取随用不就好了吗?

*gorm.DB 挂载到全局变量中的原因是为了方便在整个应用程序中共享一个数据库连接,我们可以在项目的任何地方访问该数据库连接,从而避免了频繁地打开和关闭数据库连接的开销,这样可以提高数据库操作的效率和性能。此外,全局变量还可以方便地进行数据库连接的初始化和配置,以及对数据库操作的统一管理和处理。

需要注意的是,在使用全局变量时需要注意多个协程之间对全局变量的并发访问问题,应该使用互斥锁或其他的并发控制机制来确保全局变量的安全性和一致性。

package global

import (
   "ewa_admin_server/config"

   "go.uber.org/zap"
   "gorm.io/gorm"

   "github.com/spf13/viper"
)

var (
   EWA_CONFIG config.Configuration
   EWA_VIPER  *viper.Viper
   EWA_LOG    *zap.Logger
   EWA_DB     *gorm.DB
)

2.3 封装连接数据库方法

我们在根目录下新建一个 initialize 作为一些需要在程序启动时初始化的操作集合。在这个文件中新建 gorm_mysql.go 文件:

package initialize

import (
   "ewa_admin_server/global"
   "ewa_admin_server/initialize/internal"
   "fmt"

   "gorm.io/driver/mysql"
   "gorm.io/gorm"
)

// GormMysql 初始化Mysql数据库
func GormMysql() *gorm.DB {
   m := global.EWA_CONFIG.MySQL
   if m.Dbname == "" {
      return nil
   }

   // 创建 mysql.Config 实例,其中包含了连接数据库所需的信息,比如 DSN (数据源名称),字符串类型字段的默认长度以及自动根据版本进行初始化等参数。
   mysqlConfig := mysql.Config{
      DSN:                       m.Dsn(), // DSN data source name
      DefaultStringSize:         191,     // string 类型字段的默认长度
      SkipInitializeWithVersion: false,   // 根据版本自动配置
   }

   // 打开数据库连接
   db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular))

   // 将引擎设置为我们配置的引擎,并设置每个连接的最大空闲数和最大连接数。
   if err != nil {
      return nil
   } else {
      db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
      sqlDB, _ := db.DB()
      sqlDB.SetMaxIdleConns(m.MaxIdleConns)
      sqlDB.SetMaxOpenConns(m.MaxOpenConns)

      fmt.Println("====3-gorm====: gorm link mysql success")
      return db
   }
}

其中的 gorm 配置,我们抽离并封装成单独的方法了,因为后面其他的数据库连接也会使用到。

initialize/internal 中新建 gorm.go

package internal

import (
   "ewa_admin_server/global"
   "log"
   "os"
   "time"

   "gorm.io/gorm/schema"

   "gorm.io/gorm"
   "gorm.io/gorm/logger"
)

type DBBASE interface {
   GetLogMode() string
}

var Gorm = new(_gorm)

type _gorm struct{}

// Config gorm 自定义配置
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {

   // 将传入的字符串前缀和单复数形式参数应用到 GORM 的命名策略中,并禁用迁移过程中的外键约束,返回最终生成的 GORM 配置信息。
   config := &gorm.Config{
      // 命名策略
      NamingStrategy: schema.NamingStrategy{
         TablePrefix:   prefix,   // 表前缀,在表名前添加前缀,如添加用户模块的表前缀 user_
         SingularTable: singular, // 是否使用单数形式的表名,如果设置为 true,那么 User 模型会对应 users 表
      },
      // 是否在迁移时禁用外键约束,默认为 false,表示会根据模型之间的关联自动生成外键约束语句
      DisableForeignKeyConstraintWhenMigrating: true,
   }

   _default := logger.New(NewWriter(log.New(os.Stdout, "\r\n", log.LstdFlags)), logger.Config{
      SlowThreshold: 200 * time.Millisecond,
      LogLevel:      logger.Warn,
      Colorful:      true,
   })

   var logMode DBBASE
   switch global.EWA_CONFIG.App.DbType {
   case "mysql":
      logMode = &global.EWA_CONFIG.MySQL
   default:
      logMode = &global.EWA_CONFIG.MySQL
   }

   switch logMode.GetLogMode() {
   case "silent", "Silent":
      config.Logger = _default.LogMode(logger.Silent)
   case "error", "Error":
      config.Logger = _default.LogMode(logger.Error)
   case "warn", "Warn":
      config.Logger = _default.LogMode(logger.Warn)
   case "info", "Info":
      config.Logger = _default.LogMode(logger.Info)
   default:
      config.Logger = _default.LogMode(logger.Info)
   }
   return config
}

解释一下这里面的关键代码:

_default := logger.New(NewWriter(log.New(os.Stdout, "\r\n", log.LstdFlags)), logger.Config{
  SlowThreshold: 200 * time.Millisecond,
  LogLevel:      logger.Warn,
  Colorful:      true,
})

这段代码用于创建 GORM 框架的日志记录器,接受一个 io.Writer 类型的参数和一个 logger.Config 类型的配置项作为输入,返回一个 logger.Interface 类型的日志记录器:

  1. log.New(os.Stdout, "\r\n", log.LstdFlags):创建一个输出到标准输出(控制台)的标准 logger,使用 \r\n 作为换行符,打印格式包括日期和时间。

  2. logger.Config{SlowThreshold: 200 * time.Millisecond, LogLevel: logger.Warn, Colorful: true}:创建一个日志记录器的配置项,其中:

    • SlowThreshold 表示慢 SQL 的阈值,超过这个阈值的 SQL 将被视为慢 SQL,单位为毫秒。
    • LogLevel 表示日志级别,只记录级别不低于该值的日志,可选的值包括 SilentErrorWarnInfoTrace 和 Debug,分别对应不输出日志、输出错误日志、输出警告及以上级别的日志、输出信息及以上级别的日志、输出所有日志和输出调试日志。
    • Colorful 表示是否启用彩色日志输出。

主要是创建一个 GORM 框架的日志记录器,并将日志输出到标准输出(控制台),设置了慢 SQL 的阈值为 200 毫秒,只记录警告及以上级别的日志,并启用彩色输出。

日志的 writer 构造函数 抽离出来,封装在 initialize/internal/logger.go 中:

package internal

import (
   "ewa_admin_server/global"
   "fmt"

   "gorm.io/gorm/logger"
)

type writer struct {
   logger.Writer
}

// NewWriter writer 构造函数
func NewWriter(w logger.Writer) *writer {
   return &writer{Writer: w}
}

// Printf 格式化打印日志
func (w *writer) Printf(message string, data ...interface{}) {
   var logZap bool
   switch global.EWA_CONFIG.App.DbType {
   case "mysql":
      logZap = global.EWA_CONFIG.MySQL.LogZap
   }
   if logZap {
      global.EWA_LOG.Info(fmt.Sprintf(message+"\n", data...))
   } else {
      w.Writer.Printf(message, data...)
   }
}

2.4 初始化数据库

准备工作做好之后,我们就可以连接数据库了,在 initialize 中新建一个 gorm.go 来专门处理数据库的连接:

package initialize

import (
   "ewa_admin_server/global"

   "gorm.io/gorm"
)

// Gorm 初始化数据库并产生数据库全局变量
func Gorm() *gorm.DB {
   switch global.EWA_CONFIG.App.DbType {
   case "mysql":
      return GormMysql()
   default:
      return GormMysql()
   }
}

main.go 中:

package main

import (
   "ewa_admin_server/core"
   "ewa_admin_server/global"
   "ewa_admin_server/initialize"

   "go.uber.org/zap"

   "github.com/gin-gonic/gin"
)

const AppMode = "debug" // 运行环境,主要有三种:debug、test、release

func main() {
   gin.SetMode(AppMode)

   // TODO:1.配置初始化
   global.EWA_VIPER = core.InitializeViper()

   // TODO:2.日志
   global.EWA_LOG = core.InitializeZap()
   zap.ReplaceGlobals(global.EWA_LOG)

   global.EWA_LOG.Info("server run success on ", zap.String("zap_log", "zap_log"))

   //  TODO:3.数据库连接
   global.EWA_DB = initialize.Gorm()

   // TODO:4.其他初始化

   // TODO:5.启动服务
   core.RunServer()
}

是不是就可以连接数据库了?试试

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

为什么呢?因为我们还没有建立数据库啊!光有插头,没插座啊~

2.5 新建数据库

我这里使用的数据库图形化管理工具是 Navicat,它支持 MySQLPostgreSQL,你也可以使用其他的工具,最好是支持这两种,因为项目会用到,然后就可以新建一个数据库了:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

重启项目,如果你看到下面的控制台信息,恭喜你,成功了!

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

3、Go 连接 PostgreSQL

PostgreSQL 是一种强大的、开源的关系型数据库管理系统(RDBMS),PostgreSQL 是一个功能强大、高度可靠且安全的数据库平台,因此被广泛用于企业级应用程序和数据管理领域。它具有以下优点:

  1. 可扩展性:PostgreSQL 可以在不停机的情况下进行水平扩展和垂直扩展,通过分片和集群等方式使其支持大型企业级应用程序。

  2. 高度可靠性:PostgreSQL 被认为是一个极其可靠的数据库平台,能够提供容错、恢复能力。因此,它通常被用于需要高度稳定性的企业级系统。

  3. 具有完整的事务性支持:PostgreSQL 支持 ACID(原子性、一致性、隔离性、持久性)事务处理,并保证数据的一致性和正确性。

  4. 开源免费:作为开源软件,PostgreSQL 可以免费使用,没有任何版权或商业限制,同时也有活跃的社区支持。

  5. 丰富的扩展:PostgreSQL 有成千上万的扩展和插件,给予应用程序更多的选择和功能支持,包括数据类型、索引类型、查询函数和存储过程等等。

  6. 安全性高:PostgreSQL 提供了各种安全选项,例如 SSL支持加密连接ACL支持等等。同时,由于其通用性,它还在多个不同的平台上运行,包括各种 Linux发行版WindowsMacOS 等。

按照Gorm官方文档的demo,我们还需要安装一个 driver

go get gorm.io/driver/postgres

同样,我们需要安装 PostgreSQL 数据库:

3.1 定义 PostgreSQL 配置

# ...

pgsql: # PostgreSQL 配置
  host: "localhost" # 服务器地址
  port: "5432" # 端口
  config: "sslmode=disable TimeZone=Asia/Shanghai" # 其他配置
  db_name: east_white_admin_server # 数据库名称
  username: "ian_kevin" # 数据库用户名
  password: "123456" # 数据库密码
  prefix: "t_" # 全局表前缀,单独定义 TableName 则不生效
  singular: false # 是否开启全局禁用复数,true表示不开启
  engine: "" # 引擎,默认InnoDB
  max_idle_conns: 10 # 最大空闲连接数
  max_open_conns: 100 # 最大连接数
  log_mode: error # 日志级别
  log_zap: false # 是否通过zap写日志文件

3.2 结构体和方法

接下来,需要用结构体对配置进行解析,然后将其挂载到全局变量上。在 config 下新建 gorm_pgsql.go,定义如下的结构体:

package config

type PGSQL struct {
   Host         string `mapstructure:"host" json:"host" yaml:"host"`                               // 服务器地址:端口
   Port         string `mapstructure:"port" json:"port" yaml:"port"`                               //:端口
   Config       string `mapstructure:"config" json:"config" yaml:"config"`                         // 高级配置
   Dbname       string `mapstructure:"db_name" json:"db_name" yaml:"db_name"`                      // 数据库名
   Username     string `mapstructure:"username" json:"username" yaml:"username"`                   // 数据库用户名
   Password     string `mapstructure:"password" json:"password" yaml:"password"`                   // 数据库密码
   Prefix       string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`                         // 全局表前缀,单独定义TableName则不生效
   Singular     bool   `mapstructure:"singular" json:"singular" yaml:"singular"`                   // 是否开启全局禁用复数,true表示开启
   Engine       string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"`        // 数据库引擎,默认InnoDB
   MaxIdleConns int    `mapstructure:"max_idle_conns" json:"max_idle_conns" yaml:"max_idle_conns"` // 空闲中的最大连接数
   MaxOpenConns int    `mapstructure:"max_open_conns" json:"max_open_conns" yaml:"max_open_conns"` // 打开到数据库的最大连接数
   LogMode      string `mapstructure:"log_mode" json:"log_mode" yaml:"log_mode"`                   // 是否开启Gorm全局日志
   LogZap       bool   `mapstructure:"log_zap" json:"log_zap" yaml:"log_zap"`                      // 是否通过zap写入日志文件
}

// Dsn 基于配置文件获取 dsn
func (p *PGSQL) Dsn() string {
   return "host=" + p.Host + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config
}

// LinkDsn 根据 dbname 生成 dsn
func (p *PGSQL) LinkDsn(dbname string) string {
   return "host=" + p.Host + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config
}

func (m *PGSQL) GetLogMode() string {
   return m.LogMode
}

然后也需要和 MySQL 一样,将其统一配置在 config.go 中:

package config

type Configuration struct {
   App   App   `mapstructure:"app" json:"app" yaml:"app"`
   Zap   Zap   `mapstructure:"zap" json:"zap" yaml:"zap"`
   MySQL MySQL `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
   Pgsql PGSQL `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"`
}

3.3 封装数据库连接方法

我们在根目录下新建一个 initialize 作为一些需要在程序启动时初始化的操作集合。在这个文件中新建 gorm_pgsql.go 文件:

package initialize

import (
   "ewa_admin_server/global"
   "ewa_admin_server/initialize/internal"
   "fmt"

   "gorm.io/driver/postgres"
   "gorm.io/gorm"
)

// GormPgSql 初始化 Postgresql 数据库
func GormPgSql() *gorm.DB {
   p := global.EWA_CONFIG.Pgsql

   if p.Dbname == "" {
      return nil
   }
   pgsqlConfig := postgres.Config{
      DSN:                  p.Dsn(), // DSN data source name
      PreferSimpleProtocol: false,
   }

   db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular))

   if err != nil {
      return nil
   } else {
      sqlDB, _ := db.DB()
      sqlDB.SetMaxIdleConns(p.MaxIdleConns)
      sqlDB.SetMaxOpenConns(p.MaxOpenConns)

      fmt.Println("====3-gorm====: gorm link PostgreSQL success")

      return db
   }
}

然后在 initialize/gorm.go 中增加使用 PostgreSQL 时的方法:

package initialize

import (
   "ewa_admin_server/global"

   "gorm.io/gorm"
)

// Gorm 初始化数据库并产生数据库全局变量
func Gorm() *gorm.DB {
   switch global.EWA_CONFIG.App.DbType {
   case "mysql":
      return GormMysql()
   case "pgsql":
      return GormPgSql()
   default:
      return GormMysql()
   }
}

如果你更习惯使用其他的数据库,也可以通过类似的思路进行配置。

3.4 PostgreSQL 图形化工具

我这里使用的是 pgAdmin,安装流程就不赘述了。安装完成之后,在 shell 工具中,通过如下的指令建立对应的数据库:

# 查看 PostgreSQL 服务是否启动
brew services list

# 指定数据库连接 PostgreSQL 
psql -d postgres

# 创建用户 
CREATE USER ian_kevin WITH PASSWORD '123456'; 

# 创建数据库 
CREATE DATABASE east_white_admin_server; 

然后再 pgAdmin 中连接到对应的服务,就可以看到里面有了我们创建的数据库了:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

3.5 测试

config.yaml 中的 db_type 改成 pgsql,然后重启项目,如果你看到下面的信息,就说明连接成功了:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库

end~ 这就是链接数据库的全部内容了,下一篇我们将继续完成一些基础的配置,然后开始一些简单的 CRUD