6、Go Web 编程 - 打通 Web 开发的最后一步,操作数据库
hi,我是温新,一名 PHPer
我们使用 MySQL 作为持久化存储数据的地方,因此开始本章之前,请确保你的电脑已经安装了 MySQL。
我使用的是 MySQL8.0.30。
Gorm
可以使用 SQL 来操作数据库,也可以使用 ORM 来操作数据库。MySQL 作为一个关系型数据库,而 Gorm 就是对关系数据的一个映射,可以很好的把数据用关系的形式映射到数据库中。
熟悉 Laravel 的开发者,对 ORM 肯定很熟悉,操作起来爽的不得了,有了它即使不会数据库也能进行相关操作。
本次开发就是使用 Gorm 关系映射来操作数据库。按照以往,会先把建表语句写出来,然后进行后续的操作。现在我们将不再给出 SQL 建表语句,请按照顺序一步一步操作,就一定能够成功。为了便于核对数据表,文章的最后我还是会把建表语句写出来。
关于步骤问题,纯属为了方便阅读。
第一步:准备工作
1)创建数据库
$ mysql -u root -p
mysql> create DATABASE goweb-blog;
2)引入第三包
$ pwd
/home/www-go/blog/4
$ go get -u github.com/jinzhu/gorm
$ go get -u github.com/spf13/viper
$ go get -u github.com/go-sql-driver/mysql
- 
gorm包:用于简化与关系型数据库交互的 Go 语言对象关系映射(Object-Relational Mapping, ORM)框架。 - 
go-sql-driver/mysql包: MySQL 数据库交互的驱动程序。 - 
github.com/spf13/viper包:用于配置文件管理的库。用它解析JSON、YAML和TOML格式的配置文件,并提供了一些方便的方法来访问配置数据。 
3)创建 MySQL 配置信息文件
4/config/config.yml
mysql:
  charset: utf8
  db: goweb-blog
  host: localhost
  user: root
  password: 123456
4)连接数据库程序
4/config/baseConfig.go
package config
import (
	"fmt"
	"github.com/spf13/viper"
)
// init 函数在包加载时自动执行,用于初始化 viper 配置管理器
func init() {
	getConfig()
}
// getConfig 函数负责从配置文件中读取配置信息,并将它们存储到 viper 中
func getConfig() {
	// 添加配置文件路径
	viper.AddConfigPath("/home/www-go/blog/4/config/")
    // 读取配置文件
    err := viper.ReadInConfig()
	if err != nil {
		panic(fmt.Errorf("配置文件出错: %s", err))
	}
}
// GetMysqlConnect 函数用于从 viper 中获取 MySQL 连接字符串
func GetMysqlConnect() string {
	// 从 viper 中获取 MySQL 用户名、密码、主机地址、数据库名称和字符集设置
	user := viper.GetString("mysql.user")
	password := viper.GetString("mysql.password")
	host := viper.GetString("mysql.host")
	db := viper.GetString("mysql.db")
	charset := viper.GetString("mysql.charset")
	// 根据获取的信息构建 MySQL 连接字符串
	return fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=%s&parseTime=true", user, password, host, db, charset)
}
我们在这里是尽量贴近真实的开发,因此没有直接把 MySQL 的连接直接写在程序中,而是单独分离了出来,因此,你看到了它的配置信息是存放在一个单独的文件中。然后使用 viper 进行内容解析。
第二步:使用关系映射创建数据表
1)创建 Userinfo
models/user.go
package models
// UserInfo 结构体定义了用户信息
type UserInfo struct {
	// ID 是主键
	ID int `gorm:"primary_key"`
	// Username 是用户名,类型为varchar(80)
	Username string `gorm:"type:varchar(80)"`
	// Email 是电子邮件地址,类型为varchar(80)
	Email string `gorm:"type:varchar(80)"`
	// Password 是密码,类型为varchar(150)
	Password string `gorm:"type:varchar(150)"`
    
	// Posts 字段是关联的Post结构体切片,表示用户创建的所有文章
	Posts []Post
	// Followers 字段是一个UserInfo指针切片,通过many2many关系与另一个UserInfo表建立联系
	Followers []*UserInfo `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
}
UserInfo 结构体会被映射为 user_info 数据表,Followers 会被映射为 follower 数据表,它与 user_info 是一个多对多关系。一个用户可以有多篇文章,也就是 post。
修改之后,如果 IDE 中提示 views/index.go 报错,暂时不用管。
2)创建 Post
models/post.go
package models
// Post 结构体定义了文章信息
type Post struct {
	// ID 是主键
	ID int `gorm:"primary_key"`
	// UserInfoID 表示文章所属用户的信息ID
	UserInfoID int
	// UserInfo 字段是关联的UserInfo结构体,表示文章所属用户的信息
	UserInfo UserInfo
	// Title 是文章标题,类型为varchar(180)
	Title string `gorm:"type:varchar(180)"`
	// Content 是文章内容,类型为text
	Content string `gorm:"type:text"`
}
3)创建基础模型
models/base.go
package models
import (
	"4/config"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
	"log"
)
// db 变量用于存储数据库连接实例
var db *gorm.DB
// SetDB 函数将传入的数据库连接实例设置为全局变量db
func SetDB(database *gorm.DB) {
	db = database
}
// ConnectDB 函数负责建立与MySQL数据库的连接,并返回连接实例
func ConnectDB() *gorm.DB {
	// 获取配置文件中的MySQL连接信息
	connectMysql := config.GetMysqlConnect()
	log.Println("连接数据库 ...")
    
	// 使用 GORM 库打开数据库连接
	db, err := gorm.Open("mysql", connectMysql)
	if err != nil {
		// 如果连接失败,则输出错误信息并终止程序运行
		panic(fmt.Sprintf("数据库连接失败..., %s", err))
	}
	// 设置表名不使用复数形式
	db.SingularTable(true)
	return db
}
4)自定义一个主程序用于创建数据表
$ pwd
/home/www-go/blog/4
$ mkdir -p cmd/db_init
cmd/db_init/main.go
package main
import (
	"4/models"
	"log"
)
// main 函数是程序的入口点
func main() {
	log.Println("数据库初始化...")
	// 建立数据库连接
	db := models.ConnectDB()
	defer db.Close()
	// 设置全局数据库连接实例
	models.SetDB(db)
	// 如果表已经存在,则删除它们
	db.DropTableIfExists(models.UserInfo{}, models.Post{})
	// 创建表
	db.CreateTable(models.UserInfo{}, models.Post{})
	// 定义用户信息数据
	userinfos := []models.UserInfo{
		{
			Username: "test",
			Password: models.GeneratePasswordHash("123123"),
			Email:    "test@qq.com",
			Posts: []models.Post{
				{
					Title:   "我是王美丽",
					Content: "我正在学习 Go Web 编程呀",
				},
			},
		},
		{
			Username: "lili",
			Password: models.GeneratePasswordHash("123123"),
			Email:    "lili@qq.com",
			Posts: []models.Post{
				{
					Title:   "我是丽丽",
					Content: "我正在吹风呀",
				},
			},
		},
	}
	// 循环遍历并插入用户信息到数据库
	for _, u := range userinfos {
		db.Debug().Create(&u)
	}
}
执行程序
$ go run cmd/db_init/main.go
程序运行后,请查看 goweb-blog 数据库,会生成 user_info、post、follower 三张数据表,且 user_info 和 post 数据表都有数据。
下面是建表语句
CREATE TABLE `user_info` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(80) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `email` varchar(80) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `password` varchar(150) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `post` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_info_id` int DEFAULT NULL,
  `title` varchar(180) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `content` text COLLATE utf8mb4_general_ci,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `follower` (
  `user_info_id` int NOT NULL,
  `follower_id` int NOT NULL,
  PRIMARY KEY (`user_info_id`,`follower_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
修改模板
上面改动后,数据已经发生了一点小变化,现在我们来修改模板。
1)修改首页模板
views/index.go
package views
import "4/models"
// IndexView 结构体表示首页视图,包含基础视图、用户信息和帖子列表
type IndexView struct {
	// BaseView 基础视图结构体,包含标题等基本信息
	BaseView
	// UserInfo 用户信息结构体,包含用户名、密码、电子邮件等信息
	models.UserInfo
	// Posts 帖子列表结构体,包含帖子的标题和内容等信息
	Posts []models.Post
}
// IndexViewData 结构体表示首页视图数据,用于获取首页视图
type IndexViewData struct {
}
// GetIndex 方法返回一个 IndexView 类型的对象,包含首页视图的信息
func (IndexViewData) GetIndex() IndexView {
	// 获取用户名为 "test" 的用户信息
	userinfo, _ := models.GetUserinfoByUsername("test")
	// 获取该用户的所有帖子
	posts, _ := models.GetPostByUserId(userinfo.ID)
	// 创建并返回一个新的 IndexView 对象,包含基础视图、用户信息和帖子列表
	return IndexView{BaseView{Title: "Go Web 开发 Blog"}, *userinfo, *posts}
}
2)主程序初始化数据库连接
main.go
package main
import (
	"4/controllers"
	"4/models"
	"net/http"
)
func main() {
	// 连接数据库
	db := models.ConnectDB()
	// 延迟关闭数据库连接
	defer db.Close()
	// 设置数据库
	models.SetDB(db)
	// 启动控制器
	controllers.Startup()
	// 启动 HTTP 服务器,监听本地的 8888 端口
	http.ListenAndServe(":8888", nil)
}
3)修改模板视图
templates/content/index.html
{{define "content"}}
    <div>
        <h1>姓名: {{ .UserInfo.Username }}</h1>
    </div>
    <div>
        <h1>文章列表</h1>
        <ul>
            {{ range .Posts }}
            <li>{{ .Title }} -- {{ .Content }}</li>
            {{ end }}
        </ul>
    </div>
{{end}}
4)启动程序
$ go run main.go 
2023/12/14 23:34:37 连接数据库 ...
浏览器中通过 http://localhost:8888/ 访问,看到的页面显示如下内容,就已经改造成功。
Go Blog:首页 登录
姓名: test
文章列表
- 我是王美丽 -- 我正在学习 Go Web 编程呀
总结
到这里,MVC 开发流程我们已经完成,且成功与数据库进行了交互,从数据库中读取数据并在模板中显示了出来。后续的所有操作基本都是这个流程。从控制器到模型取数据再到模板显示数据。