【七天从零实现ORM框架|Day02:database/sql基础】学完吊打面试官

在这里插入图片描述

今天是TORM框架的编写的第一天,主要完成几个任务:

  • Go-SQL-Driver驱动的简单使用
  • 使用 Go 语言标准库 database/sql 连接并操作 MySQL 数据库,并简单封装。

源代码:在【迈莫coding】中回复关键字「 torm 」获取github地址链接。
后续会为【七天从零实现TORM框架】录制视频,文章+视频+代码

驱动选型

Go支持的MYSQL的驱动种类目前非常多,在这里我查阅资料,列出几种mysql驱动,他有些是支持database/sql标准,但有些是采用了自己的实现接口,常见的有以下几种:

在本项目中的话,我们采用第一种方式作为mysql驱动,在此也推荐给大家,主要原因:

  • 完全支持database/sql接口
  • 支持keepalive,保持长连接和连接稳定
  • 维护频率高

安装Go-SQL-Driver

执行下面命令:

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

Go-SQL-Driver驱动实战

Go语言中提供了标准库 database/sql 用来与数据库进行交互,接下来简单熟悉以下 Go-SQL-Driver 的使用。

DML操作:增删改

他有两种操作方式,一种是使用 Exec 函数添加,一种是首先使用 Prepare 获得stmt,然后调用 Exec 函数添加。

  • 使用 Exec 函数添加
func (db *DB) Exec(query string, args ...interface{
    
    }) (Result, error) 

示例代码:

result, err := db.Exec("select * from user where user_name = ?", "迈莫coding")
  • 首先使用 Prepare 获得stmt,然后调用 Exec 函数添加
func (db *DB) Prepare(query string) (*Stmt, error) {
    
    
  return db.PrepareContext(context.Background(), query) 
}

示例代码:

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操作:查询

单条语句查询

func (db *DB) QueryRow(query string, args ...interface{
    
    }) *Row 

示例代码:

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,就是所有行的信息,类似结果集。

func (db *DB) Query(query string, args ...interface{
    
    }) (*Rows, error)

我们可以通过 Next() 方法判断是否存在下一条数据,如果有,可以使⽤之前的 Scan() ⽅法读取一行,然后继续判断,继续获取。这个过程的本质就是迭代,所以通常要配合循环使⽤用。

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 文件中。

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() 这三个函数。

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 文件中。

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) 方法测试结果
=== 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) 方法测试结果
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)

代码目录

torm
|--raw.go                  // 底层与数据库交互语句
|--raw_test.go
|--go.mod

到这里,第一天的任务就编写完成了。回顾一下,第一天主要完成了与数据库交互的代码,也就是 raw.go 代码编写,是TORM框架中与数据库交互的核心功能。

文章也会持续更新,可以微信搜索「 迈莫coding 」第一时间阅读,回复『1024』领取学习go资料。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_41066066/article/details/112806382