Bootstrap

Mysql -- database/sql / GORM

Go操作Mysql数据库
使用Go操作MySQL等数据库,一般有两种方式:一是使用database/sql接口,直接在代码里硬编码sql语句;二是使用gorm,即对象关系映射的方式在代码里抽象的操作数据库。一般推荐使用第二种方式。

使用database/sql接口

Go没有内置的驱动支持任何数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动。但缺点是,直接用 github.com/go-sql-driver/mysql 访问数据库都是直接写 sql,取出结果然后自己拼成对象,使用上面不是很方便,可读性也不好。
下载包

go get github.com/go-sql-driver/mysql

通过调用sql.Open函数返回一个sql.DB指针; sql.Open函数原型如下:

func Open(driverName, dataSourceName string) (*DB, error)
  • driverName: 使用的驱动名. 这个名字其实就是数据库驱动注册到 database/sql 时所使用的名字.
  • dataSourceName: 数据库连接信息,这个连接包含了数据库的用户名, 密码, 数据库主机以及需要连接的数据库名等信息.

1.sql.Open并不会立即建立一个数据库的网络连接, 也不会对数据库链接参数的合法性做检验, 它仅仅是初始化一个sql.DB对象. 当真正进行第一次数据库查询操作时, 此时才会真正建立网络连接;
2.sql.DB表示操作数据库的抽象接口的对象,但不是所谓的数据库连接对象,sql.DB对象只有当需要使用时才会创建连接,如果想立即验证连接,需要用Ping()方法;
3.sql.Open返回的sql.DB对象是协程并发安全的.
4.sql.DB的设计就是用来作为长连接使用的。不要频繁Open, Close。比较好的做法是,为每个不同的datastore建一个DB对象,保持这些对象Open。如果需要短连接,那么把DB作为参数传入function,而不要在function中Open, Close。

数据库查询的一般步骤如下:

调用 db.Query 执行 SQL 语句, 此方法会返回一个 Rows 作为查询的结果
通过 rows.Next() 迭代查询数据.
通过rows.Scan() 读取每一行的值
调用 db.Close() 关闭查询

注意:
1.rows.Scan 参数的顺序很重要, 需要和查询的结果的column对应. 例如 “SELECT * From user where age >=20 AND age < 30” 查询的行的 column 顺序是 “id, name, age” 和插入操作顺序相同, 因此 rows.Scan 也需要按照此顺序 rows.Scan(&id, &name, &age), 不然会造成数据读取的错位.
2.因为golang是强类型语言,所以查询数据时先定义数据类型,但是查询数据库中的数据存在三种可能:存在值,存在零值,未赋值NULL 三种状态, 因为可以将待查询的数据类型定义为sql.Nullxxx类型,可以通过判断Valid值来判断查询到的值是否为赋值状态还是未赋值NULL状态.
3.每次db.Query操作后, 都建议调用rows.Close(). 因为 db.Query() 会从数据库连接池中获取一个连接, 这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时,会发生一个内部EOF错误,自动调用rows.Close(),但如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭, 则此连接会一直被占用. 因此通常我们使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中; 不过阅读源码发现rows.Close()操作是幂等操作,即一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同, 所以即便对已关闭的rows再执行close()也没关系.

插入数据:
通过db.Exec()插入数据,通过返回的err可知插入失败的原因,通过返回的restult可以进一步查询本次插入数据影响的行数RowsAffected和最后插入的Id(如果数据库支持查询最后插入Id).

DB.Exec():执行不返回行(row)的查询,比如:insert,update,delete。
DB.QueryRow(): 返回单行的查询。
DB.Prepare() : 返回一个Stmt。Stmt 对象可以执行Exec,Query,QueryRow等操作。
db.Begin():开启事务,返回Tx对象。

在这里插入图片描述

安装好 mysql 驱动之后,我们创建一张user表,并执行CRUD操作(增、删、改、查)。代码如下所示:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "time"
)

//数据库连接信息
const (
	USERNAME = "root"
	PASSWORD = "123456"
	NETWORK = "tcp"
	SERVER = "127.0.0.1"
	PORT = 3306
	DATABASE = "test"
)

//user表结构体定义
type User struct {
	Id int `json:"id" form:"id"`
	Username string `json:"username" form:"username"`
	Password string `json:"password" form:"password"`
	Status int   `json:"status" form:"status"`      // 0 正常状态, 1删除
	Createtime int64 `json:"createtime" form:"createtime"`
}

func main() {
	conn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s",USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
	DB, err := sql.Open("mysql", conn)    //连接数据库
	if err != nil {
		fmt.Println("connection to mysql failed:", err)
		return
	}
   defer DB.Close()
	
    DB.SetConnMaxLifetime(100*time.Second)  //最大连接周期,超时的连接就close
    DB.SetMaxOpenConns(100)                //设置最大连接数
    CreateTable(DB)      //创建表
    InsertData(DB)           //插入数据
    QueryOne(DB)           //查询单行
    QueryMulti(DB)          //查询多行
    UpdateData(DB)        //更新数据
    DeleteData(DB)         //删除数据
}

//创建表
func CreateTable(DB *sql.DB) {
	sql := `CREATE TABLE IF NOT EXISTS users(
	id INT(4) PRIMARY KEY AUTO_INCREMENT NOT NULL,
	username VARCHAR(64),
	password VARCHAR(64),
	status INT(4),
	createtime INT(10)
	); `
	
	if _, err := DB.Exec(sql); err != nil {
        fmt.Println("create table failed:", err)
        return
    }
	fmt.Println("create table successd")
}

//插入数据
func InsertData(DB *sql.DB) {
    result,err := DB.Exec("insert INTO users(username,password) values(?,?)","test","123456")
    if err != nil{
        fmt.Printf("Insert data failed,err:%v", err)
        return
    }
    lastInsertID,err := result.LastInsertId()    //获取插入数据的自增ID
    if err != nil {
        fmt.Printf("Get insert id failed,err:%v", err)
        return
    }
    fmt.Println("Insert data id:", lastInsertID)
   
    rowsaffected,err := result.RowsAffected()  //通过RowsAffected获取受影响的行数
    if err != nil {
        fmt.Printf("Get RowsAffected failed,err:%v",err)
        return
    }
    fmt.Println("Affected rows:", rowsaffected)
}

//查询单行
func QueryOne(DB *sql.DB) {
	user := new(User)   //用new()函数初始化一个结构体对象
	row := DB.QueryRow("select id,username,password from users where id=?", 1)
	//row.scan中的字段必须是按照数据库存入字段的顺序,否则报错
	if err := row.Scan(&user.Id,&user.Username,&user.Password); err != nil {  
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Println("Single row data:", *user)
}

//查询多行
func QueryMulti(DB *sql.DB) {
    user := new(User)
    rows, err := DB.Query("select id,username,password from users where id = ?", 2)
    
    defer func() {
        if rows != nil {
            rows.Close()   //关闭掉未scan的sql连接
        }
    }()
    if err != nil {
        fmt.Printf("Query failed,err:%v\n", err)
        return
    }
    for rows.Next() {
        err = rows.Scan(&user.Id, &user.Username, &user.Password)  //不scan会导致连接不释放
        if err != nil {
            fmt.Printf("Scan failed,err:%v\n", err)
            return
        }
        fmt.Println("scan successd:", *user)
    }
}

//更新数据
func UpdateData(DB *sql.DB){
    result,err := DB.Exec("UPDATE users set password=? where id=?","111111",1)
    if err != nil{
        fmt.Printf("Insert failed,err:%v\n", err)
        return
    }
    fmt.Println("update data successd:", result)
    
    rowsaffected,err := result.RowsAffected()
    if err != nil {
        fmt.Printf("Get RowsAffected failed,err:%v\n",err)
        return
    }
    fmt.Println("Affected rows:", rowsaffected)
}

//删除数据
func DeleteData(DB *sql.DB){
    result,err := DB.Exec("delete from users where id=?",1)
    if err != nil{
        fmt.Printf("Insert failed,err:%v\n",err)
        return
    }
    fmt.Println("delete data successd:", result)
    
    rowsaffected,err := result.RowsAffected()
    if err != nil {
        fmt.Printf("Get RowsAffected failed,err:%v\n",err)
        return
    }
    fmt.Println("Affected rows:", rowsaffected)
}

执行程序,输出结果如下所示:

create table successd
Insert data id: 1
Affected rows: 1
Single row data: {1 test 123456 0 0}
update data successd: {0xc0000a0000 0xc000010280}
Affected rows: 1
delete data successd: {0xc0000a0000 0xc0000102b0}
Affected rows: 1

OK,到这里大家是不是觉得这种实现方式很繁琐,假如要修改某个sql语句需要在代码中修改,这样很麻烦,代码设计也比较糟糕。因此这种方式并不推荐使用。


使用GORM

GORM(Object Relation Mapping),即Go语言中的对象关系映射,实际上就是对数据库的操作进行封装,对上层开发人员屏蔽数据操作的细节,开发人员看到的就是一个个对象,大大简化了开发工作,提高了生产效率。如GORM结合Gin等服务端框架使用可以开发出丰富的Rest API等。
首先,下载包

go get github.com/jinzhu/gorm
go get github.com/gin-gonic/gin

使用Go的Gin框架和Gorm开发简单的CRUD API,代码如下

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"net/http"
)

var MysqlDB *gorm.DB

type User struct {
	Id   int    `gorm:"size:11;primary_key;AUTO_INCREMENT;not null" json:"id"`
	Age  int    `gorm:"size:11;DEFAULT NULL" json:"age"`
	Name string `gorm:"size:255;DEFAULT NULL" json:"name"`
	//gorm后添加约束,json后为对应mysql里的字段
}

func main() {
	MysqlDB, err := gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8")
	if err != nil {
		fmt.Println("failed to connect database:", err)
		return
	}else{
	    fmt.Println("connect database success")
	    MysqlDB.SingularTable(true)
	    MysqlDB.AutoMigrate(&User{}) //自动建表
	    fmt.Println("create table success")
	}
	defer MysqlDB.Close()

	Router()
}

func Router() {
	router := gin.Default()
	//路径映射
	router.GET("/user", InitPage)
	router.POST("/user/create", CreateUser)
	router.GET("/user/list", ListUser)
	router.PUT("/user/update", UpdateUser)
	router.GET("/user/find", GetUser)
	router.DELETE("/user/:id", DeleteUser)
	router.Run(":8080")
}

//每个路由都对应一个具体的函数操作,从而实现了对user的增,删,改,查操作
func InitPage(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "pong",
	})
}

func CreateUser(c *gin.Context) {
	var user User
	c.BindJSON(&user)     //使用bindJSON填充对象
	MysqlDB.Create(&user) //创建对象
	c.JSON(http.StatusOK, &user)    //返回页面
}

func UpdateUser(c *gin.Context) {
	var user User
	id := c.PostForm("id")                //post方法取相应字段
	err := MysqlDB.First(&user, id).Error //数据库查找主键=ID的第一行
	if err != nil {
		c.AbortWithStatus(404)
		fmt.Println(err.Error())
	} else {
		c.BindJSON(&user)
		MysqlDB.Save(&user) //提交更改
		c.JSON(http.StatusOK, &user)
	}
}
func ListUser(c *gin.Context) {
	var user []User
	line := c.Query("line")
	MysqlDB.Limit(line).Find(&user) //限制查找前line行
	c.JSON(http.StatusOK, &user)
}
func GetUser(c *gin.Context) {
	id := c.Query("id")
	var user User
	err := MysqlDB.First(&user, id).Error
	if err != nil {
		c.AbortWithStatus(404)
		fmt.Println(err.Error())
	} else {
		c.JSON(http.StatusOK, &user)
	}
}

func DeleteUser(c *gin.Context)  {
    id := c.Param("id")
    var user User
    MysqlDB.Where("id = ?", id).Delete(&user)
    c.JSON(http.StatusOK, gin.H{
        "data": "this has been deleted!",
    })
}

执行程序,输出结果如下所示:

connect database success
create table success
......
[GIN-debug] GET    /user                     --> main.InitPage (3 handlers)
[GIN-debug] POST   /user/create              --> main.CreateUser (3 handlers)
[GIN-debug] GET    /user/list                --> main.ListUser (3 handlers)
[GIN-debug] PUT    /user/update              --> main.UpdateUser (3 handlers)
[GIN-debug] GET    /user/find                --> main.GetUser (3 handlers)
[GIN-debug] DELETE /user/:id                 --> main.DeleteUser (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080

如果要对上述接口进行测试,可以使用postman等工具.经测试可以实现对user的增,删,改,查操作。
如下,使用GET方法请求/user接口。
在这里插入图片描述

小结
通过非常简短的代码,就可以实现功能强大的restful接口,go语言的优势也是非常明显的.关于gin和gorm还有更加深入的内容.掌握好这两个工具可以轻松的构建web应用。

;