![在这里插入图片描述](https://img-blog.csdnimg.cn/20210119120924737.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMDY2MDY2,size_16,color_FFFFFF,t_70#pic_center)
今天是TORM框架的编写的第一天,主要完成几个任务:
- Go-SQL-Driver驱动的简单使用
- 使用 Go 语言标准库 database/sql 连接并操作 MySQL 数据库,并简单封装。
>源代码:在【迈莫coding】中回复关键字「 torm 」获取github地址链接。
后续会为【七天从零实现TORM框架】录制视频,文章+视频+代码
# 驱动选型
Go支持的MYSQL的驱动种类目前非常多,在这里我查阅资料,列出几种mysql驱动,他有些是支持database/sql标准,但有些是采用了自己的实现接口,常见的有以下几种:
- [https://github.com/Go-SQL-Driver/MySQL](https://github.com/Go-SQL-Driver/MySQL) :完全支持database/sql标准,并且是使用go语言写的
- [https://github.com/ziutek/mymysql](https://github.com/ziutek/mymysql) :支持database/sql,也支持自定义接口,也是完全用go语言写的
- [https://github.com/Philio/GoMySQL](https://github.com/Philio/GoMySQL) :不支持database/sql,完全使用go语言编写
在本项目中的话,我们采用第一种方式作为mysql驱动,在此也推荐给大家,主要原因:
- 完全支持database/sql接口
- 支持keepalive,保持长连接和连接稳定
- 维护频率高
# 安装Go-SQL-Driver
执行下面命令:
```go
go get -u github.com/go-sql-driver/mysql
```
# Go-SQL-Driver驱动实战
Go语言中提供了标准库 database/sql 用来与数据库进行交互,接下来简单熟悉以下 Go-SQL-Driver 的使用。
## DML操作:增删改
他有两种操作方式,一种是使用 **Exec 函数**添加,一种是首先使用 **Prepare 获得stmt**,然后调用 Exec 函数添加。
- **使用 Exec 函数添加**
```go
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
```
示例代码:
```go
result, err := db.Exec("select * from user where user_name = ?", "迈莫coding")
```
- **首先使用 Prepare 获得stmt,然后调用 Exec 函数添加**
```go
func (db *DB) Prepare(query string) (*Stmt, error) {
return db.PrepareContext(context.Background(), query)
}
```
示例代码:
```go
stmt,err:=db.Prepare("INSERT INTO user(user_name, age) values(?,?)")
//补充完整sql语句,并执⾏
result,err:=stmt.Exec("迈莫coding",1)
```
预编译语句(Prepared Statement) 提供了诸多好处,因此我们在开发中尽量使⽤它。下⾯列出了使⽤预编译语句所提供的功能:
- `PreparedStatement` 可以实现自定义参数的查询
- `PreparedStatement` 通常⽐手动拼接字符串SQL 语句高效
- `PreparedStatement` 可以防⽌SQL注⼊攻击
## DQL操作:查询
**单条语句查询**
```go
func (db *DB) QueryRow(query string, args ...interface{}) *Row
```
示例代码:
```go
var userName string
var age int
err := db.QueryRow("select user_name,age from user where user_name = ?", "迈莫coding")
err := row.Scan(&userName, &age)
```
**多条语句查询**
Query 获取数据, for xxx.Next() 遍历数据:
⾸先使用Query()方法进⾏查询,如果查询无误,返回Rows,就是所有行的信息,类似结果集。
```go
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
```
我们可以通过 Next() 方法判断是否存在下一条数据,如果有,可以使⽤之前的 Scan() ⽅法读取一行,然后继续判断,继续获取。这个过程的本质就是迭代,所以通常要配合循环使⽤用。
```go
func (rs *Rows) Next() bool
```
每次 db.Query 操作后,都建议调用 rows.Close() 。
因为 db.Query() 会从数据库连接池中获取⼀个连接, 这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时,会发⽣生⼀个内部EOF错误,自动调⽤ rows.Close() ,但如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭,则此连接会一直被占用。
因此通常我们使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中。
# 核心底层结构Session
首先先用 Go-SQL-Driver 驱动实现与数据库的交互,也是TORM最终要与数据库交互的底层代码。今天就要实现通过原始SQL语句与数据库进行交互。代码存放在根目录下 raw.go 文件中。
```go
raw.go
package session
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
log "github.com/sirupsen/logrus"
"strings"
)
// session类 旨于与数据库进行交互
type Session struct {
// 数据库引擎
db *sql.DB
// 事务
tx *sql.Tx
// SQL动态参数
sqlValues []interface{}
// SQL语句
sql strings.Builder
}
// DB return tx if a tx begins otherwise return *sql.DB
func (s *Session) DB() CommonDB {
if s.tx != nil {
return s.tx
}
return s.db
}
// 实例化 Session
func NewSession(db *sql.DB) *Session {
return &Session{db: db}
}
func (s *Session) Clear() {
s.sql.Reset()
s.sqlValues = nil
}
func (s *Session) Raw(sql string, values ...interface{}) *Session {
s.sql.WriteString(sql)
s.sql.WriteString(" ")
s.sqlValues = append(s.sqlValues, values...)
return s
}
```
**代码说明:**
- 第13~22行:创建一个session结构体,作用是torm框架底层与数据库交互。其中包含几个成员变量,解释如下:
- db: 数据库引擎
- tx: 数据库事务
- sqlValues:用于SQL语句中参数的存放位置
- sql:SQL语句的存放位置
- 第25~30行:获取数据库引擎
- 第33~35行:实例化session对象
- 第36~39行:当每次执行完一次SQL操作后,会将sql语句和sql参数清空掉
- 第41行~46行: Raw() 函数用户将SQL语句和请求参数写入到 session 结构体中的 sql 和 sqlValues 字段中
接下来的话,需要进行封装增删改查操作函数, **Exec()** 、 **QueryRow()** 、 **Query()** 这三个函数。
```go
raw.go
// 定义统一接口 便于后续支持多个数据引擎
type CommonDB interface {
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
Exec(query string, args ...interface{}) (sql.Result, error)
}
var _ CommonDB = (*sql.DB)(nil)
var _ CommonDB = (*sql.Tx)(nil)
// 增删改操作
func (s *Session) Exec() (result sql.Result, err error) {
defer s.Clear()
log.Info(s.sql.String(), s.sqlValues)
if result, err = s.DB().Exec(s.sql.String(), s.sqlValues...); err != nil {
log.Error(err)
}
return
}
// 单条查询操作
func (s *Session) QueryRow() *sql.Row {
defer s.Clear()
log.Info(s.sql.String(), s.sqlValues)
return s.DB().QueryRow(s.sql.String(), s.sqlValues...)
}
// 多条查询操作
func (s *Session) Query() (rows *sql.Rows, err error) {
defer s.Clear()
log.Info(s.sql.String(), s.sqlValues)
if rows, err = s.DB().Query(s.sql.String(), s.sqlValues...); err != nil {
log.Error(err)
}
return
}
```
**代码说明:**
- 第4~8行:定义统一接口(增删改查通用操作方法),便于后续适应不同的数据库引擎
- 第10~11行:用户检测 sql.DB 类型和 sql.Tx 类型是否实现了接口 Common 中的所有方法,若未全部实现,则在编译期间报错。
- 第14~21行:增删改通用函数封装
- 第24~28行:单条SQL语句查询函数封装
- 第31行~38行:多条SQL语句查询函数封装
到这里,与数据库交互的功能(增删改查)也基本完成,现在进入测试阶段。
# 测试
TORM的单元测试比较完善,没写完一个模块,都会编写单元测试代码,由于咱们编写完了 raw.go 与数据库交互的代码,那么咱们来进行测试是否可行,代码存放在根目录下 raw_test.go 文件中。
```go
raw_test.go
package session
import (
"database/sql"
"os"
"testing"
log "github.com/sirupsen/logrus"
)
var db *sql.DB
func TestMain(m *testing.M) {
db, _ = sql.Open("mysql", "root:12345678@tcp(127.0.0.1:3306)/po?charset=utf8mb4")
code := m.Run()
_ = db.Close()
os.Exit(code)
}
func New() *Session {
return NewSession(db)
}
func TestSession_QueryRow(t *testing.T) {
s := New()
var userName string
var age int
s = s.Raw("select user_name,age from user where user_name = ?", "迈莫")
res := s.QueryRow()
if err := res.Scan(&userName, &age); err != nil {
t.Fatal("failed to query db", err)
}
log.Info("userName--", userName)
log.Info("age--", age)
}
func TestSession_Exec(t *testing.T) {
s := New()
key := "迈莫"
s = s.Raw("insert into user(user_name, age) values(?, ?)", key, 22)
_, err := s.Exec()
if err != nil {
t.Fatal("failed to insert db", err)
}
}
func TestSession_Query(t *testing.T) {
s := New()
var userName string
var age int
s = s.Raw("select user_name, age from user")
rows, err := s.Query()
if err != nil {
t.Fatal("fialed to query db", err)
}
for rows.Next() {
err = rows.Scan(&userName, &age)
if err != nil {
t.Fatal("fialed to query db", err)
}
log.Info("userName--", userName)
log.Info("age--", age)
}
}
```
**测试结果:**
- `TestSession_QueryRow(t *testing.T)` 方法测试结果
```go
=== RUN TestSession_QueryRow
time="2021-01-15T12:29:57+08:00" level=info msg="select user_name,age from user where user_name = ? [迈莫]"
time="2021-01-15T12:29:57+08:00" level=info msg="userName--迈莫"
time="2021-01-15T12:29:57+08:00" level=info msg=age--1
--- PASS: TestSession_QueryRow (0.00s)
PASS
TestSession_Exec(t *testing.T) 方法测试结果
=== RUN TestSession_Exec
time="2021-01-15T12:32:51+08:00" level=info msg="insert into user(user_name, age) values(?, ?) [迈莫 21]"
--- PASS: TestSession_Exec (0.00s)
PASS
```
- `TestSession_Query(t *testing.T)` 方法测试结果
```go
time="2021-01-15T12:33:35+08:00" level=info msg="select user_name, age from user []"
time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫coding"
time="2021-01-15T12:33:35+08:00" level=info msg=age--1
time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫"
time="2021-01-15T12:33:35+08:00" level=info msg=age--1
time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫"
time="2021-01-15T12:33:35+08:00" level=info msg=age--21
--- PASS: TestSession_Query (0.00s)
```
# 代码目录
```go
torm
|--raw.go // 底层与数据库交互语句
|--raw_test.go
|--go.mod
```
到这里,第一天的任务就编写完成了。回顾一下,第一天主要完成了与数据库交互的代码,也就是 raw.go 代码编写,是TORM框架中与数据库交互的核心功能。
>文章也会持续更新,可以微信搜索「 迈莫coding 」第一时间阅读,回复『1024』领取学习go资料。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210119121305613.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMDY2MDY2,size_16,color_FFFFFF,t_70#pic_center)
有疑问加站长微信联系(非本文作者))