思考并回答以下问题:
实验介绍
在本次实验中,我们将介绍如何使用Gin框架的数据库集成功能来创建和管理数据库连接,执行各种数据库操作,以及使用ORM(对象关系映射)工具来简化与数据库的交互。无论您是初学者还是有经验的开发人员,本章都将帮助您了解如何使用Gin框架的数据库集成功能来构建高效和可扩展的Web应用程序。
知识点
- MongoDB
- MySQL
- Redis
- ORM
MongoDB
MongoDB是一个非关系性数据库,跟关系性数据库相比,它不用管理数据库表结构,更灵活,也支持高性能应用,可扩展性也很好。另外,MongoDB也是开源项目,有强大的社区支持。在Gin框架中,可以使用MongoDB官方的MongoDB Go Driver来对MongoDB进行增删改查操作。我们这里主要介绍一些基础的数据库操作。
在实验之前,我们需要保证MongoDB服务是启动的。
如果在终端中运行mongo出现exiting with code 1错误信息,则需要运行下面的脚本来启动MongoDB;否则,则可跳过此步骤。1
2
3
4
5
6
7# 重新创建数据目录
sudo rm -rf /data/db
sudo mkdir /data/db
sudo chown shiyanlou:staff /data/db
# 启动 MongoDB
sudo mongod >> /tmp/mongod 2>&1 &
启动完成后,可以再次在终端运行mongo,可以显示连接成功。
接下来,我们将通过实验来体验在Gin框架中如何操作MongoDB。
首先,我们需要创建一个用户模型。创建文件夹models,并新建文件user.go,输入以下内容。1
2
3
4
5
6
7
8package models
import "go.mongodb.org/mongo-driver/bson/primitive"
type User struct {
Id primitive.ObjectID `json:"id" bson:"_id"`
Username string `json:"username" bson:"username"`
}
这里,我们定义了用户的ID,在MongoDB中为_id,返回给客户端则为id;另外,我们还定义了用户名Username。
然后,我们需要连接到数据库。咱们可以使用MongoDB Go Driver提供的mongo.Connect()
函数。该函数接收一个上下文参数context.Context
和*options.ClientOptions
,并返回一个mongo.Client
类型的实例MongoClient,表示连接的客户端。我们创建一个新目录db,并创建新文件mongo.go,输入以下内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package db
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func init() {
var err error
// 设置 MongoDB 连接选项
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
// 连接 MongoDB
MongoClient, err = mongo.Connect(context.Background(), clientOptions)
if err != nil {
panic(err)
}
}
var MongoClient *mongo.Client
这里,我们定义了MongoClient这个MongoDB的客户端,供后面的数据库操作。
下面,我们来实现如何新增、查找、修改、删除用户。打开routes/user.go,移除之前不用的逻辑,添加5个接口,代码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160package routes
import (
"context"
"gin-course/db"
"gin-course/models"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"net/http"
)
var userCollection *mongo.Collection
func init() {
userCollection = db.MongoClient.Database("test").Collection("users")
}
// RegisterUsers 注册路由
func RegisterUsers(app *gin.Engine) {
app.GET("/users", getUsers)
app.GET("/users/:id", getUser)
app.POST("/users", postUser)
app.PUT("/users/:id", putUser)
app.DELETE("/users/:id", deleteUser)
}
// 获取用户列表
func getUsers(c *gin.Context) {
// 查询所有用户
cur, err := userCollection.Find(context.Background(), bson.D{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cur.Close(context.Background())
// 将查询结果转为切片
var users []models.User
err = cur.All(context.Background(), &users)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回查询结果
c.JSON(http.StatusOK, users)
}
// 获取用户
func getUser(c *gin.Context) {
// 从 URL 参数中获取用户 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
return
}
// 查询用户
var user models.User
err = userCollection.FindOne(context.Background(), bson.M{"_id": id}).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回查询结果
c.JSON(http.StatusOK, user)
}
// 新增用户
func postUser(c *gin.Context) {
// 解析请求体中的 JSON 数据
var user models.User
err := c.BindJSON(&user)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 插入用户
user.Id = primitive.NewObjectID()
res, err := userCollection.InsertOne(context.Background(), &user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回插入结果
c.JSON(http.StatusCreated, res.InsertedID)
}
// 修改用户
func putUser(c *gin.Context) {
// 获取用户 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
return
}
// 构造查询条件
filter := bson.M{"_id": id}
// 构造更新文档
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
update := bson.M{
"$set": bson.M{
"username": user.Username,
},
}
// 执行更新操作
result, err := userCollection.UpdateOne(context.Background(), filter, update)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result.ModifiedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user updated"})
}
// 删除用户
func deleteUser(c *gin.Context) {
// 获取用户 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
return
}
// 构造查询条件
filter := bson.M{"_id": id}
// 执行删除操作
result, err := userCollection.DeleteOne(context.Background(), filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result.DeletedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}
这样,我们就实现了用户的增删改查基本接口。
下面,我们来测试一下。终端里运行go run main.go
启动程序。
打开ThunderClient插件,构建一个POST请求,URL为http://localhost:8080/users
,Body为{"username":"zhangsan"}
。点击Send,会得到如下响应。
现在我们打开Web服务,URL更换为/users,可以得到如下结果。说明新增成功了,同时也测试了获取用户列表接口。
下面,我们在ThunderClient中构造一个PUT请求来更新用户名称,URL为http://localhost:8080/users/641d72b20cd33b17fa962c68
(641d72b20cd33b17fa962c68是刚刚创建的用户ID,你这里应该是不一样24位16进制ID),Body为{"username":"lisi"}
,点击Send。可以得到如下结果。
然后我们再次请求/users,可以看到username已经修改为lisi。
最后,我们在ThunderClient中构造一个DELETE请求来删除用户,URL为http://localhost:8080/users/641d72b20cd33b17fa962c68
(同样,这里是刚刚创建的用户ID,你的应该会不一样),点击Send,可以得到删除成功。然后我们再次请求/users,会得到空结果。
MySQL
MySQL跟MongoDB不同,是关系性数据库,需要明确定义表结构以及各个字段类型,是非常严格规范的数据库,相对来说欠缺一些灵活性,多一些稳定性。其他主流关系性数据库还包括PostgreSQL、SQL Server、SQLite等。在Gin框架中,我们同样可以用MySQL作为业务数据库来储存我们的业务数据。
这里限于实验环境原因不再进行MySQL的实验操作,跟MongoDB的数据库操作大同小异。这里我们给出MySQL的增删改查实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// db/mysql.go
package db
import (
"database/sql"
"fmt"
)
func init() {
var err error
MysqlDb, err = sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database")
if err != nil {
panic(fmt.Errorf("error opening database connection: %s", err.Error()))
}
if err = MysqlDb.Ping(); err != nil {
panic(fmt.Errorf("error connecting to database: %s", err.Error()))
}
}
var MysqlDb *sql.DB1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182// routes/product.go
package routes
import (
"database/sql"
"net/http"
"strconv"
"gin-course/db"
"gin-course/models"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
var productTable = "products"
// RegisterProducts 注册路由
func RegisterProducts(app *gin.Engine) {
// Register routes
app.GET("/products", getProducts)
app.GET("/products/:id", getProduct)
app.POST("/products", postProduct)
app.PUT("/products/:id", putProduct)
app.DELETE("/products/:id", deleteProduct)
}
// 获取产品列表
func getProducts(c *gin.Context) {
// 执行查询
rows, err := db.MysqlDb.Query("SELECT * FROM " + productTable)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
// 解析结果
var products []models.Product
for rows.Next() {
var product models.Product
err := rows.Scan(&product.Id, &product.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
products = append(products, product)
}
// 返回结果
c.JSON(http.StatusOK, products)
}
// 获取产品
func getProduct(c *gin.Context) {
// 解析参数ID
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}
// 执行查询
row := db.MysqlDb.QueryRow("SELECT * FROM "+productTable+" WHERE id=?", id)
// 解析结果
var product models.Product
err = row.Scan(&product.Id, &product.Name)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回结果
c.JSON(http.StatusOK, product)
}
// 新增产品
func postProduct(c *gin.Context) {
var product models.Product
err := c.ShouldBindJSON(&product)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 插入记录
result, err := db.MysqlDb.Exec(`
INSERT INTO products (name)
VALUES (?, ?)
`, product.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
lastInsertID, err := result.LastInsertId()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": lastInsertID})
}
// 更新产品
func putProduct(c *gin.Context) {
// 获取产品ID
productIdStr := c.Param("id")
productId, err := strconv.Atoi(productIdStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}
// 解析请求体
var product models.Product
if err := c.ShouldBindJSON(&product); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新记录
result, err := db.MysqlDb.Exec(`
UPDATE products SET name=?
WHERE id=?
`, product.Name, productId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "product updated"})
}
// 删除产品
func deleteProduct(c *gin.Context) {
// 获取产品ID
productIdStr := c.Param("id")
productId, err := strconv.Atoi(productIdStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}
// 删除记录
result, err := db.MysqlDb.Exec(`
DELETE FROM products WHERE id=?
`, productId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "product deleted"})
}
在这里,我们用到了go-sql-driver,一种兼容大部分关系性数据库的驱动。因此这段代码写好之后,例如如果后面需要更改MySQL数据库为PostgreSQL,我们只需要更改db.MysqlDb
为对应的连接db.PostgresqlDb
即可,这样保证了关系性数据库的兼容性。
Redis
Redis是一个开源、基于内存的高性能键值存储系统。它支持多种数据结构,包括字符串(strings)、哈希表(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等,同时也提供了丰富的操作这些数据结构的命令。Redis使用单线程模型,但其内部采用了多种优化技术,如异步I/O、事件通知和数据持久化等,使得它在性能方面表现出色。
在Gin框架中,Redis可以作为我们的数据库缓存,特别是应用访问量特别大的时候,我们需要将数据放在内存里以供快速获取数据,从而避免大量磁盘查询而导致IO过高最终应用崩溃的问题。
同样,我们这里给出获取产品控制器getProduct的基于Redis缓存的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54// 获取产品
func getProduct(c *gin.Context) {
// 解析参数ID
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}
// 获取缓存数据
val, err := db.RedisClient.Get(context.Background(), strconv.Itoa(id)).Result()
if err == nil {
// 解析缓存结果
var product models.Product
if err := json.Unmarshal([]byte(val), &product); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回缓存数据
c.JSON(http.StatusOK, product)
return
}
// 执行查询
row := db.MysqlDb.QueryRow("SELECT * FROM "+productTable+" WHERE id=?", id)
// 解析结果
var product models.Product
err = row.Scan(&product.Id, &product.Name)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 保存缓存数据
data, err := json.Marshal(product)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_, err = db.RedisClient.Set(context.Background(), strconv.Itoa(id), string(data), 0).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回结果
c.JSON(http.StatusOK, product)
}
在这里,我们在执行SQL之前先获取Redis缓存中的产品数据,如果能获取到,直接返回给客户端;否则,再执行数据库查询,获取结果后保存在Redis缓存中,供下一次查询使用。这样,我们就在Gin框架中实现了一个简单的基于Redis的缓存系统。
ORM
ORM是“Object-Relational Mapping”的缩写,即对象关系映射,是一种关系型数据库与面向对象编程语言之间的数据转换工具。它提供了一种将数据库中的数据映射到程序中的对象的方法,使得开发人员可以通过操作对象来操作数据库,而不需要直接使用SQL语句。ORM框架的主要目标是简化应用程序开发过程,提高开发效率和可维护性。
在Gin框架中,我们同样可以用ORM来操作数据库。这里用GORM为例,介绍如何使用GORM来完成增删改查。GORM是一个基于Go语言的ORM库,它支持多种关系型数据库,包括MySQL、PostgreSQL、SQLite、Microsoft SQL Server等。GORM提供了一个简单易用的API,使得开发人员可以轻松地进行数据库操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
Email string
}
func main() {
// 连接数据库
dsn := "root:password@tcp(127.0.0.1:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 自动迁移
db.AutoMigrate(&User{})
// 插入数据
db.Create(&User{Name: "Alice", Email: "alice@example.com"})
// 查询数据
var user User
db.First(&user, "name = ?", "Alice")
fmt.Println(user)
// 更新数据
db.Model(&user).Update("Email", "new_email@example.com")
// 删除数据
db.Delete(&user)
}
可以看到,在这里我们用user作为实例直接操作数据库,这样比直接写SQL要方便很多。
实验总结
本次实验主要讲解了如何在Gin框架中操作数据库,包括主流数据库MongoDB、MySQL、Redis等。数据库集成是网络应用中非常重要的逻辑,任何业务数据都需要通过数据库来进行存储、查询和通信,因此掌握数据库集成对我们开发网络应用是非常有帮助的。